Merge branch 'main' into deploy
This commit is contained in:
commit
b42750b674
|
@ -11,6 +11,7 @@ programs/mango-v4/src/lib-expanded.rs
|
|||
dist
|
||||
node_modules
|
||||
yarn-error.log
|
||||
.yarn
|
||||
|
||||
.idea
|
||||
.vscode
|
||||
|
|
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -4,7 +4,17 @@ Update this for each program release and mainnet deployment.
|
|||
|
||||
## not on mainnet
|
||||
|
||||
### v0.24.0, 2024-4-
|
||||
|
||||
### v0.24.1, 2024-7-
|
||||
|
||||
- Support for switchboard on demand oracle (#974)
|
||||
|
||||
- Sip bad oracle in token update index and rate (#975)
|
||||
|
||||
|
||||
## mainnet
|
||||
|
||||
### v0.24.0, 2024-4-18
|
||||
|
||||
- Allow skipping banks and invalid oracles when computing health (#891)
|
||||
|
||||
|
@ -26,8 +36,6 @@ Update this for each program release and mainnet deployment.
|
|||
|
||||
Assert that a transaction was emitted and run with a correct view of the current mango state.
|
||||
|
||||
## mainnet
|
||||
|
||||
### v0.23.0, 2024-3-8
|
||||
|
||||
Deployment: Mar 8, 2024 at 12:10:52 Central European Standard Time, https://explorer.solana.com/tx/6MXGookZoYGMYb7tWrrmgZzVA13HJimHNqwHRVFeqL9YpQD7YasH1pQn4MSQTK1o13ixKTGFxwZsviUzmHzzP9m
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -109,7 +109,6 @@ pub async fn runner(
|
|||
.values()
|
||||
.filter(|t| !t.closed)
|
||||
.map(|t| &t.token_index)
|
||||
// TODO: grouping tokens whose oracle might have less confidencen e.g. ORCA with the rest, fails whole ix
|
||||
// TokenUpdateIndexAndRate is known to take max 71k cu
|
||||
// from cargo test-bpf local tests
|
||||
// chunk size of 8 seems to be max before encountering "VersionedTransaction too large" issues
|
||||
|
@ -129,8 +128,8 @@ pub async fn runner(
|
|||
.perp_markets
|
||||
.values()
|
||||
.filter(|perp|
|
||||
// MNGO-PERP-OLD
|
||||
perp.perp_market_index != 1)
|
||||
// MNGO-PERP-OLD
|
||||
perp.perp_market_index != 1)
|
||||
.map(|perp| {
|
||||
loop_consume_events(
|
||||
mango_client.clone(),
|
||||
|
@ -146,8 +145,8 @@ pub async fn runner(
|
|||
.perp_markets
|
||||
.values()
|
||||
.filter(|perp|
|
||||
// MNGO-PERP-OLD
|
||||
perp.perp_market_index != 1)
|
||||
// MNGO-PERP-OLD
|
||||
perp.perp_market_index != 1)
|
||||
.map(|perp| {
|
||||
loop_update_funding(
|
||||
mango_client.clone(),
|
||||
|
@ -219,7 +218,7 @@ pub async fn loop_update_index_and_rate(
|
|||
None,
|
||||
),
|
||||
data: anchor_lang::InstructionData::data(
|
||||
&mango_v4::instruction::TokenUpdateIndexAndRate {},
|
||||
&mango_v4::instruction::TokenUpdateIndexAndRateResilient {},
|
||||
),
|
||||
};
|
||||
let mut banks = banks_for_a_token
|
||||
|
@ -232,7 +231,6 @@ pub async fn loop_update_index_and_rate(
|
|||
.collect::<Vec<_>>();
|
||||
|
||||
ix.accounts.append(&mut banks);
|
||||
|
||||
let pix = PreparedInstructions::from_single(
|
||||
ix,
|
||||
client
|
||||
|
@ -240,22 +238,8 @@ pub async fn loop_update_index_and_rate(
|
|||
.compute_estimates
|
||||
.cu_token_update_index_and_rates,
|
||||
);
|
||||
let sim_result = match client.simulate(pix.clone().to_instructions()).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.append(pix);
|
||||
}
|
||||
|
||||
let pre = Instant::now();
|
||||
let sig_result = client
|
||||
.send_and_confirm_permissionless_tx(instructions.to_instructions())
|
||||
|
@ -378,10 +362,10 @@ pub async fn loop_consume_events(
|
|||
ix,
|
||||
client.context.compute_estimates.cu_perp_consume_events_base
|
||||
+ num_of_events
|
||||
* client
|
||||
.context
|
||||
.compute_estimates
|
||||
.cu_perp_consume_events_per_event,
|
||||
* client
|
||||
.context
|
||||
.compute_estimates
|
||||
.cu_perp_consume_events_per_event,
|
||||
);
|
||||
let sig_result = client
|
||||
.send_and_confirm_permissionless_tx(ixs.to_instructions())
|
||||
|
@ -438,6 +422,7 @@ pub async fn loop_update_funding(
|
|||
),
|
||||
data: anchor_lang::InstructionData::data(&mango_v4::instruction::PerpUpdateFunding {}),
|
||||
};
|
||||
|
||||
let ixs = PreparedInstructions::from_single(
|
||||
ix,
|
||||
client.context.compute_estimates.cu_perp_update_funding,
|
||||
|
@ -506,7 +491,7 @@ pub async fn loop_charge_collateral_fees(
|
|||
collateral_fee_interval,
|
||||
max_cu_when_batching,
|
||||
)
|
||||
.await
|
||||
.await
|
||||
{
|
||||
Ok(()) => {}
|
||||
Err(err) => {
|
||||
|
@ -574,7 +559,7 @@ async fn charge_collateral_fees_inner(
|
|||
&ix_to_send,
|
||||
max_cu_when_batching,
|
||||
)
|
||||
.await;
|
||||
.await;
|
||||
info!("charge collateral fees: {:?}", txsigs);
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
app = "switchboard-crank"
|
||||
kill_signal = "SIGINT"
|
||||
kill_timeout = 5
|
||||
|
||||
[build]
|
||||
dockerfile = "../ts/client/scripts/Dockerfile.scripts"
|
||||
|
||||
[experimental]
|
||||
cmd = ["yarn", "tsx", "ts/client/scripts/sb-on-demand-crank.ts"]
|
||||
|
||||
[[vm]]
|
||||
size = "shared-cpu-1x"
|
||||
memory = "512mb"
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "0.24.0",
|
||||
"version": "0.24.1",
|
||||
"name": "mango_v4",
|
||||
"instructions": [
|
||||
{
|
||||
|
@ -1277,6 +1277,36 @@
|
|||
],
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "tokenUpdateIndexAndRateResilient",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "group",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "mintInfo",
|
||||
"isMut": false,
|
||||
"isSigner": false,
|
||||
"relations": [
|
||||
"oracle",
|
||||
"group"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "oracle",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "instructions",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
}
|
||||
],
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "accountCreate",
|
||||
"accounts": [
|
||||
|
@ -11133,6 +11163,9 @@
|
|||
},
|
||||
{
|
||||
"name": "RaydiumCLMM"
|
||||
},
|
||||
{
|
||||
"name": "SwitchboardOnDemand"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
16
package.json
16
package.json
|
@ -53,6 +53,7 @@
|
|||
"secp256k1": "5.0.0",
|
||||
"ts-mocha": "^10.0.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsx": "^4.15.7",
|
||||
"tweetnacl": "1.0.3",
|
||||
"typedoc": "^0.22.5",
|
||||
"typescript": "^5.4.5"
|
||||
|
@ -67,8 +68,11 @@
|
|||
"@coral-xyz/anchor": "^0.28.1-beta.2",
|
||||
"@project-serum/serum": "0.13.65",
|
||||
"@pythnetwork/client": "~2.14.0",
|
||||
"@iarna/toml": "2.2.5",
|
||||
"@raydium-io/raydium-sdk": "^1.3.1-beta.57",
|
||||
"@solana/spl-token": "0.3.7",
|
||||
"@solana/web3.js": "^1.78.2",
|
||||
"@switchboard-xyz/on-demand": "^1.1.26",
|
||||
"@switchboard-xyz/sbv2-lite": "^0.1.6",
|
||||
"@switchboard-xyz/solana.js": "^2.5.4",
|
||||
"big.js": "^6.1.1",
|
||||
|
@ -78,13 +82,9 @@
|
|||
"dotenv": "^16.0.3",
|
||||
"fast-copy": "^3.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"node-kraken-api": "^2.2.2"
|
||||
"node-kraken-api": "^2.2.2",
|
||||
"switchboard-anchor": "npm:@coral-xyz/anchor@0.30.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"@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",
|
||||
"**/@blockworks-foundation/mangolana/node-fetch": "npm:@blockworks-foundation/node-fetch@2.6.11"
|
||||
},
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"packageManager": "yarn@4.3.1"
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "mango-v4"
|
||||
version = "0.24.0"
|
||||
version = "0.24.1"
|
||||
description = "Created with Anchor"
|
||||
edition = "2021"
|
||||
|
||||
|
@ -50,6 +50,7 @@ static_assertions = "1.1"
|
|||
# note: switchboard-common 0.8.19 is broken - use 0.8.18 instead
|
||||
switchboard-program = "0.2"
|
||||
switchboard-v2 = { package = "switchboard-solana", version = "0.28" }
|
||||
switchboard-on-demand = { version = "0.1.11" }
|
||||
|
||||
|
||||
openbook-v2 = { git = "https://github.com/openbook-dex/openbook-v2.git", features = [
|
||||
|
|
Binary file not shown.
|
@ -18,7 +18,10 @@ pub mod compute_budget {
|
|||
declare_id!("ComputeBudget111111111111111111111111111111");
|
||||
}
|
||||
|
||||
pub fn token_update_index_and_rate(ctx: Context<TokenUpdateIndexAndRate>) -> Result<()> {
|
||||
pub fn token_update_index_and_rate(
|
||||
ctx: Context<TokenUpdateIndexAndRate>,
|
||||
early_exit_on_invalid_oracle: bool,
|
||||
) -> Result<()> {
|
||||
{
|
||||
let ixs = ctx.accounts.instructions.as_ref();
|
||||
|
||||
|
@ -71,6 +74,26 @@ pub fn token_update_index_and_rate(ctx: Context<TokenUpdateIndexAndRate>) -> Res
|
|||
{
|
||||
let mut some_bank = ctx.remaining_accounts[0].load_mut::<Bank>()?;
|
||||
|
||||
let oracle_ref = &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?;
|
||||
let price = some_bank.oracle_price(
|
||||
&OracleAccountInfos::from_reader(oracle_ref),
|
||||
Some(clock.slot),
|
||||
);
|
||||
|
||||
// Early exit if oracle is invalid
|
||||
// Warning: do not change any state before this check
|
||||
let price = match price {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
return if early_exit_on_invalid_oracle {
|
||||
msg!("Invalid oracle state: {}", e);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Limit the maximal time interval that interest is applied for. This means we won't use
|
||||
// a fixed interest rate for a very long time period in exceptional circumstances, like
|
||||
// when there is a solana downtime or the security council disables this instruction.
|
||||
|
@ -89,12 +112,6 @@ pub fn token_update_index_and_rate(ctx: Context<TokenUpdateIndexAndRate>) -> Res
|
|||
now_ts,
|
||||
);
|
||||
|
||||
let oracle_ref = &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?;
|
||||
let price = some_bank.oracle_price(
|
||||
&OracleAccountInfos::from_reader(oracle_ref),
|
||||
Some(clock.slot),
|
||||
)?;
|
||||
|
||||
some_bank
|
||||
.stable_price_model
|
||||
.update(now_ts as u64, price.to_num());
|
||||
|
|
|
@ -324,7 +324,15 @@ pub mod mango_v4 {
|
|||
|
||||
pub fn token_update_index_and_rate(ctx: Context<TokenUpdateIndexAndRate>) -> Result<()> {
|
||||
#[cfg(feature = "enable-gpl")]
|
||||
instructions::token_update_index_and_rate(ctx)?;
|
||||
instructions::token_update_index_and_rate(ctx, false)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn token_update_index_and_rate_resilient(
|
||||
ctx: Context<TokenUpdateIndexAndRate>,
|
||||
) -> Result<()> {
|
||||
#[cfg(feature = "enable-gpl")]
|
||||
instructions::token_update_index_and_rate(ctx, true)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
@ -1,19 +1,17 @@
|
|||
use std::mem::size_of;
|
||||
|
||||
use crate::accounts_zerocopy::*;
|
||||
use crate::error::*;
|
||||
use crate::state::load_orca_pool_state;
|
||||
use anchor_lang::prelude::*;
|
||||
use anchor_lang::{AnchorDeserialize, Discriminator};
|
||||
use derivative::Derivative;
|
||||
use fixed::types::I80F48;
|
||||
|
||||
use static_assertions::const_assert_eq;
|
||||
use switchboard_on_demand::PullFeedAccountData;
|
||||
use switchboard_program::FastRoundResultAccountData;
|
||||
use switchboard_v2::AggregatorAccountData;
|
||||
|
||||
use crate::accounts_zerocopy::*;
|
||||
|
||||
use crate::error::*;
|
||||
use crate::state::load_orca_pool_state;
|
||||
|
||||
use super::{load_raydium_pool_state, orca_mainnet_whirlpool, raydium_mainnet};
|
||||
|
||||
const DECIMAL_CONSTANT_ZERO_INDEX: i8 = 12;
|
||||
|
@ -61,6 +59,15 @@ pub mod switchboard_v2_mainnet_oracle {
|
|||
declare_id!("DtmE9D2CSB4L5D6A15mraeEjrGMm6auWVzgaD8hK2tZM");
|
||||
}
|
||||
|
||||
pub mod switchboard_on_demand_devnet_oracle {
|
||||
use solana_program::declare_id;
|
||||
declare_id!("SBondMDrcV3K4kxZR1HNVT7osZxAHVHgYXL5Ze1oMUv");
|
||||
}
|
||||
pub mod switchboard_on_demand_mainnet_oracle {
|
||||
use solana_program::declare_id;
|
||||
declare_id!("SBondMDrcV3K4kxZR1HNVT7osZxAHVHgYXL5Ze1oMUv");
|
||||
}
|
||||
|
||||
pub mod pyth_mainnet_usdc_oracle {
|
||||
use solana_program::declare_id;
|
||||
declare_id!("Gnt27xtC473ZT2Mw5u8wZ68Z3gULkSTb5DuxJy7eJotD");
|
||||
|
@ -114,10 +121,11 @@ impl OracleConfigParams {
|
|||
pub enum OracleType {
|
||||
Pyth,
|
||||
Stub,
|
||||
SwitchboardV1,
|
||||
SwitchboardV1, // Obsolete
|
||||
SwitchboardV2,
|
||||
OrcaCLMM,
|
||||
RaydiumCLMM,
|
||||
SwitchboardOnDemand,
|
||||
}
|
||||
|
||||
pub struct OracleState {
|
||||
|
@ -194,6 +202,10 @@ pub fn determine_oracle_type(acc_info: &impl KeyedAccountReader) -> Result<Oracl
|
|||
|| acc_info.owner() == &switchboard_v2_mainnet_oracle::ID
|
||||
{
|
||||
return Ok(OracleType::SwitchboardV1);
|
||||
} else if acc_info.owner() == &switchboard_on_demand_devnet_oracle::ID
|
||||
|| acc_info.owner() == &switchboard_on_demand_mainnet_oracle::ID
|
||||
{
|
||||
return Ok(OracleType::SwitchboardOnDemand);
|
||||
} else if acc_info.owner() == &orca_mainnet_whirlpool::ID {
|
||||
return Ok(OracleType::OrcaCLMM);
|
||||
} else if acc_info.owner() == &raydium_mainnet::ID {
|
||||
|
@ -407,6 +419,35 @@ fn oracle_state_unchecked_inner<T: KeyedAccountReader>(
|
|||
oracle_type: OracleType::SwitchboardV1,
|
||||
}
|
||||
}
|
||||
OracleType::SwitchboardOnDemand => {
|
||||
fn from_foreign_error(e: impl std::fmt::Display) -> Error {
|
||||
error_msg!("{}", e)
|
||||
}
|
||||
let feed = bytemuck::from_bytes::<PullFeedAccountData>(&data[8..]);
|
||||
let ui_price: f64 = feed
|
||||
.value()
|
||||
.ok_or_else(|| error_msg!("missing price"))?
|
||||
.try_into()
|
||||
.map_err(from_foreign_error)?;
|
||||
let ui_deviation: f64 = feed
|
||||
.std_dev()
|
||||
.ok_or_else(|| error_msg!("missing deviation"))?
|
||||
.try_into()
|
||||
.map_err(from_foreign_error)?;
|
||||
let last_update_slot = feed.result.min_slot;
|
||||
|
||||
let decimals = QUOTE_DECIMALS - (base_decimals as i8);
|
||||
let decimal_adj = power_of_ten(decimals);
|
||||
let price = I80F48::from_num(ui_price) * decimal_adj;
|
||||
let deviation = I80F48::from_num(ui_deviation) * decimal_adj;
|
||||
require_gte!(price, 0);
|
||||
OracleState {
|
||||
price,
|
||||
last_update_slot,
|
||||
deviation,
|
||||
oracle_type: OracleType::SwitchboardOnDemand,
|
||||
}
|
||||
}
|
||||
OracleType::OrcaCLMM => {
|
||||
let whirlpool = load_orca_pool_state(oracle_info)?;
|
||||
let clmm_price = whirlpool.get_clmm_price();
|
||||
|
@ -484,6 +525,11 @@ mod tests {
|
|||
OracleType::OrcaCLMM,
|
||||
orca_mainnet_whirlpool::ID,
|
||||
),
|
||||
(
|
||||
"EtbG8PSDCyCSmDH8RE4Nf2qTV9d6P6zShzHY2XWvjFJf",
|
||||
OracleType::SwitchboardOnDemand,
|
||||
switchboard_on_demand_mainnet_oracle::ID,
|
||||
),
|
||||
];
|
||||
|
||||
for fixture in fixtures {
|
||||
|
@ -524,6 +570,49 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_switchboard_on_demand_price() -> Result<()> {
|
||||
// add ability to find fixtures
|
||||
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
d.push("resources/test");
|
||||
|
||||
let fixtures = vec![(
|
||||
"EtbG8PSDCyCSmDH8RE4Nf2qTV9d6P6zShzHY2XWvjFJf",
|
||||
OracleType::SwitchboardOnDemand,
|
||||
switchboard_on_demand_mainnet_oracle::ID,
|
||||
6,
|
||||
)];
|
||||
|
||||
for fixture in fixtures {
|
||||
let file = format!("resources/test/{}.bin", fixture.0);
|
||||
let mut data = read_file(find_file(&file).unwrap());
|
||||
let data = RefCell::new(&mut data[..]);
|
||||
let ai = &AccountInfoRef {
|
||||
key: &Pubkey::from_str(fixture.0).unwrap(),
|
||||
owner: &fixture.2,
|
||||
data: data.borrow(),
|
||||
};
|
||||
let base_decimals = fixture.3;
|
||||
|
||||
let sw_ais = OracleAccountInfos {
|
||||
oracle: ai,
|
||||
fallback_opt: None,
|
||||
usdc_opt: None,
|
||||
sol_opt: None,
|
||||
};
|
||||
let sw = oracle_state_unchecked(&sw_ais, base_decimals).unwrap();
|
||||
|
||||
match fixture.1 {
|
||||
OracleType::SwitchboardOnDemand => {
|
||||
assert_eq!(sw.price, I80F48::from_num(61200.109991665549598697))
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_clmm_prices() -> Result<()> {
|
||||
// add ability to find fixtures
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
FROM node:18 as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json yarn.lock ./
|
||||
RUN yarn install
|
||||
|
||||
COPY ts ts
|
||||
|
||||
RUN ls
|
||||
|
||||
# scripts are run with tsx, no upfront build needed
|
||||
# RUN yarn build
|
||||
|
||||
FROM node:18-slim as run
|
||||
|
||||
LABEL fly_launch_runtime="nodejs"
|
||||
|
||||
COPY --from=builder /app /app
|
||||
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV production
|
|
@ -31,17 +31,30 @@ async function decodePrice(conn, ai, pk): Promise<void> {
|
|||
|
||||
async function main(): Promise<void> {
|
||||
try {
|
||||
const oraclePk1 = new PublicKey(
|
||||
'4SZ1qb4MtSUrZcoeaeQ3BDzVCyqxw3VwSFpPiMTmn4GE',
|
||||
);
|
||||
const conn = new Connection(MB_CLUSTER_URL!);
|
||||
let ai = await conn.getAccountInfo(oraclePk1);
|
||||
decodePrice(conn, ai, oraclePk1);
|
||||
const oraclePk2 = new PublicKey(
|
||||
'8ihFLu5FimgTQ1Unh4dVyEHUGodJ5gJQCrQf4KUVB9bN',
|
||||
);
|
||||
ai = await conn.getAccountInfo(oraclePk2);
|
||||
decodePrice(conn, ai, oraclePk2);
|
||||
// {
|
||||
// const oraclePk1 = new PublicKey(
|
||||
// '4SZ1qb4MtSUrZcoeaeQ3BDzVCyqxw3VwSFpPiMTmn4GE',
|
||||
// );
|
||||
// const conn = new Connection(MB_CLUSTER_URL!);
|
||||
// let ai = await conn.getAccountInfo(oraclePk1);
|
||||
// decodePrice(conn, ai, oraclePk1);
|
||||
|
||||
// const oraclePk2 = new PublicKey(
|
||||
// '8ihFLu5FimgTQ1Unh4dVyEHUGodJ5gJQCrQf4KUVB9bN',
|
||||
// );
|
||||
// ai = await conn.getAccountInfo(oraclePk2);
|
||||
// decodePrice(conn, ai, oraclePk2);
|
||||
// }
|
||||
|
||||
{
|
||||
// https://ondemand.switchboard.xyz/solana/devnet/feed/23QLa7R2hDhcXDVKyUSt2rvBPtuAAbY44TrqMVoPpk1C
|
||||
const oraclePk3 = new PublicKey(
|
||||
'EtbG8PSDCyCSmDH8RE4Nf2qTV9d6P6zShzHY2XWvjFJf',
|
||||
);
|
||||
const devnetConn = new Connection(process.env.DEVNET_CLUSTER_URL!);
|
||||
const ai = await devnetConn.getAccountInfo(oraclePk3);
|
||||
decodePrice(devnetConn, ai, oraclePk3);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,486 @@
|
|||
import {
|
||||
AccountInfo,
|
||||
Cluster,
|
||||
Connection,
|
||||
Keypair,
|
||||
PublicKey,
|
||||
TransactionInstruction,
|
||||
} from '@solana/web3.js';
|
||||
import {
|
||||
CrossbarClient,
|
||||
Oracle,
|
||||
PullFeed,
|
||||
SB_ON_DEMAND_PID,
|
||||
} from '@switchboard-xyz/on-demand';
|
||||
import fs from 'fs';
|
||||
import chunk from 'lodash/chunk';
|
||||
import uniqWith from 'lodash/uniqWith';
|
||||
import { Program as Anchor30Program, Idl } from 'switchboard-anchor';
|
||||
|
||||
import { SequenceType } from '@blockworks-foundation/mangolana/lib/globalTypes';
|
||||
import { sendSignAndConfirmTransactions } from '@blockworks-foundation/mangolana/lib/transactions';
|
||||
import { AnchorProvider, Wallet } from 'switchboard-anchor';
|
||||
import { Group } from '../src/accounts/group';
|
||||
import { parseSwitchboardOracle } from '../src/accounts/oracle';
|
||||
import { MangoClient } from '../src/client';
|
||||
import { MANGO_V4_ID, MANGO_V4_MAIN_GROUP } from '../src/constants';
|
||||
import { ZERO_I80F48 } from '../src/numbers/I80F48';
|
||||
import { createComputeBudgetIx } from '../src/utils/rpc';
|
||||
|
||||
const CLUSTER: Cluster =
|
||||
(process.env.CLUSTER_OVERRIDE as Cluster) || 'mainnet-beta';
|
||||
const CLUSTER_URL =
|
||||
process.env.CLUSTER_URL_OVERRIDE || process.env.MB_CLUSTER_URL;
|
||||
const USER_KEYPAIR =
|
||||
process.env.USER_KEYPAIR_OVERRIDE || process.env.MB_PAYER_KEYPAIR;
|
||||
const GROUP = process.env.GROUP_OVERRIDE || MANGO_V4_MAIN_GROUP.toBase58();
|
||||
const SLEEP_MS = Number(process.env.SLEEP_MS) || 50_000; // 100s
|
||||
|
||||
console.log(`Starting with ${SLEEP_MS}`);
|
||||
console.log(`${CLUSTER_URL}`);
|
||||
|
||||
// TODO use mangolana to send txs
|
||||
|
||||
interface OracleInterface {
|
||||
oracle: {
|
||||
oraclePk: PublicKey;
|
||||
name: string;
|
||||
};
|
||||
decodedPullFeed: any;
|
||||
ai: AccountInfo<Buffer> | null;
|
||||
}
|
||||
|
||||
(async function main(): Promise<never> {
|
||||
const { group, client, connection, user, userProvider } = await setupMango();
|
||||
|
||||
const { sbOnDemandProgram, crossbarClient, queue } = await setupSwitchboard(
|
||||
client,
|
||||
);
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
try {
|
||||
// periodically check if we have new candidates on the group
|
||||
const filteredOracles = await prepareCandidateOracles(group, client);
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const slot = await client.connection.getSlot('finalized');
|
||||
|
||||
await updateFilteredOraclesAis(
|
||||
client.connection,
|
||||
sbOnDemandProgram,
|
||||
filteredOracles,
|
||||
);
|
||||
|
||||
const staleOracles = await filterForStaleOracles(
|
||||
filteredOracles,
|
||||
client,
|
||||
slot,
|
||||
);
|
||||
|
||||
const crossBarSims = await Promise.all(
|
||||
filteredOracles.map(
|
||||
async (fo) =>
|
||||
await crossbarClient.simulateFeeds([
|
||||
new Buffer(fo.decodedPullFeed.feedHash).toString('hex'),
|
||||
]),
|
||||
),
|
||||
);
|
||||
const varianceThresholdCrossedOracles =
|
||||
await filterForVarianceThresholdOracles(
|
||||
filteredOracles,
|
||||
client,
|
||||
crossBarSims,
|
||||
);
|
||||
const oraclesToCrank: OracleInterface[] = uniqWith(
|
||||
[...staleOracles, ...varianceThresholdCrossedOracles],
|
||||
function (a, b) {
|
||||
return a.oracle.oraclePk.equals(b.oracle.oraclePk);
|
||||
},
|
||||
);
|
||||
|
||||
console.log(
|
||||
`- round candidates | Stale: ${staleOracles
|
||||
.map((o) => o.oracle.name)
|
||||
.join(', ')} | Variance: ${varianceThresholdCrossedOracles
|
||||
.map((o) => o.oracle.name)
|
||||
.join(', ')}`,
|
||||
);
|
||||
|
||||
// todo use chunk
|
||||
// todo use luts
|
||||
|
||||
// const [pullIxs, luts] = await PullFeed.fetchUpdateManyIx(
|
||||
// sbOnDemandProgram,
|
||||
// {
|
||||
// feeds: oraclesToCrank.map((o) => new PublicKey(o.oracle.oraclePk)),
|
||||
// numSignatures: 3,
|
||||
// },
|
||||
// );
|
||||
|
||||
// console.log(
|
||||
// oraclesToCrank
|
||||
// .map((o) => new PublicKey(o.oracle.oraclePk))
|
||||
// .toString(),
|
||||
// );
|
||||
|
||||
const pullIxs: TransactionInstruction[] = [];
|
||||
const lutOwners: (PublicKey | Oracle)[] = [];
|
||||
for (const oracle of oraclesToCrank) {
|
||||
await preparePullIx(
|
||||
sbOnDemandProgram,
|
||||
oracle,
|
||||
queue,
|
||||
lutOwners,
|
||||
pullIxs,
|
||||
);
|
||||
}
|
||||
|
||||
const ixsChunks = chunk(pullIxs, 2, false);
|
||||
try {
|
||||
// use mangolana
|
||||
await sendSignAndConfirmTransactions({
|
||||
connection,
|
||||
wallet: new Wallet(user),
|
||||
transactionInstructions: ixsChunks.map((txChunk) => ({
|
||||
instructionsSet: [
|
||||
{
|
||||
signers: [],
|
||||
transactionInstruction: createComputeBudgetIx(80000),
|
||||
},
|
||||
...txChunk.map((tx) => ({
|
||||
signers: [],
|
||||
transactionInstruction: tx,
|
||||
})),
|
||||
],
|
||||
sequenceType: SequenceType.Sequential,
|
||||
})),
|
||||
config: {
|
||||
maxRetries: 5,
|
||||
autoRetry: true,
|
||||
maxTxesInBatch: 20,
|
||||
logFlowInfo: false,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(`Error in sending tx, ${JSON.stringify(error)}`);
|
||||
}
|
||||
|
||||
await new Promise((r) => setTimeout(r, SLEEP_MS));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
async function preparePullIx(
|
||||
sbOnDemandProgram,
|
||||
oracle: OracleInterface,
|
||||
queue: PublicKey,
|
||||
lutOwners: (PublicKey | Oracle)[],
|
||||
pullIxs: TransactionInstruction[],
|
||||
): Promise<void> {
|
||||
const pullFeed = new PullFeed(
|
||||
sbOnDemandProgram as any,
|
||||
new PublicKey(oracle.oracle.oraclePk),
|
||||
);
|
||||
|
||||
const conf = {
|
||||
numSignatures: 2,
|
||||
feed: oracle.oracle.oraclePk,
|
||||
};
|
||||
// TODO use fetchUpdateMany
|
||||
const [pullIx, responses, success] = await pullFeed.fetchUpdateIx(conf);
|
||||
|
||||
if (pullIx === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO
|
||||
// > Mitch | Switchboard:
|
||||
// there can be more oracles that join a queue over time
|
||||
// all oracles and feeds carry their own LUT as im sure you noticed
|
||||
// > Mitch | Switchboard:
|
||||
// the feed ones are easy to predict though
|
||||
// > Mitch | Switchboard:
|
||||
// but you dont know which oracles the gateway will select for you so best you can do is pack all oracle accounts into 1lut
|
||||
|
||||
const lutOwners_ = [...responses.map((x) => x.oracle), pullFeed.pubkey];
|
||||
lutOwners.push(...lutOwners_);
|
||||
|
||||
pullIxs.push(pullIx!);
|
||||
}
|
||||
|
||||
async function filterForVarianceThresholdOracles(
|
||||
filteredOracles: OracleInterface[],
|
||||
client: MangoClient,
|
||||
crossBarSims,
|
||||
): Promise<OracleInterface[]> {
|
||||
const varianceThresholdCrossedOracles = new Array<OracleInterface>();
|
||||
for (const [index, item] of filteredOracles.entries()) {
|
||||
const res = await parseSwitchboardOracle(
|
||||
item.oracle.oraclePk,
|
||||
item.ai!,
|
||||
client.connection,
|
||||
);
|
||||
// console.log(`${item.oracle.name} ${JSON.stringify(res)}`);
|
||||
|
||||
const crossBarSim = crossBarSims[index];
|
||||
|
||||
const simPrice =
|
||||
crossBarSim[0].results.reduce((a, b) => a + b, 0) /
|
||||
crossBarSim[0].results.length;
|
||||
|
||||
const changePct = (Math.abs(res.price - simPrice) * 100) / res.price;
|
||||
const changeBps = changePct * 100;
|
||||
if (changePct > item.decodedPullFeed.maxVariance / 1000000000) {
|
||||
console.log(
|
||||
`- ${item.oracle.name}, candidate, ${
|
||||
item.decodedPullFeed.maxVariance / 1000000000
|
||||
}, ${simPrice}, ${res.price}, ${changePct}`,
|
||||
);
|
||||
varianceThresholdCrossedOracles.push(item);
|
||||
} else {
|
||||
console.log(
|
||||
`- ${item.oracle.name}, non-candidate, ${
|
||||
item.decodedPullFeed.maxVariance / 1000000000
|
||||
}, ${simPrice}, ${res.price}, ${changePct}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return varianceThresholdCrossedOracles;
|
||||
}
|
||||
|
||||
async function filterForStaleOracles(
|
||||
filteredOracles: OracleInterface[],
|
||||
client: MangoClient,
|
||||
slot: number,
|
||||
): Promise<OracleInterface[]> {
|
||||
const staleOracles = new Array<OracleInterface>();
|
||||
for (const item of filteredOracles) {
|
||||
const res = await parseSwitchboardOracle(
|
||||
item.oracle.oraclePk,
|
||||
item.ai!,
|
||||
client.connection,
|
||||
);
|
||||
|
||||
const diff = slot - res.lastUpdatedSlot;
|
||||
if (
|
||||
slot > res.lastUpdatedSlot &&
|
||||
slot - res.lastUpdatedSlot > item.decodedPullFeed.maxStaleness
|
||||
) {
|
||||
console.log(
|
||||
`- ${item.oracle.name}, candidate, ${item.decodedPullFeed.maxStaleness}, ${slot}, ${res.lastUpdatedSlot}, ${diff}`,
|
||||
);
|
||||
staleOracles.push(item);
|
||||
} else {
|
||||
console.log(
|
||||
`- ${item.oracle.name}, non-candidate, ${item.decodedPullFeed.maxStaleness}, ${slot}, ${res.lastUpdatedSlot}, ${diff}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return staleOracles;
|
||||
}
|
||||
|
||||
async function prepareCandidateOracles(
|
||||
group: Group,
|
||||
client: MangoClient,
|
||||
): Promise<OracleInterface[]> {
|
||||
const oracles = getOraclesForMangoGroup(group);
|
||||
oracles.push(...extendOraclesManually(CLUSTER));
|
||||
|
||||
const ais = (
|
||||
await Promise.all(
|
||||
chunk(
|
||||
oracles.map((item) => item.oraclePk),
|
||||
50,
|
||||
false,
|
||||
).map(
|
||||
async (chunk) =>
|
||||
await client.program.provider.connection.getMultipleAccountsInfo(
|
||||
chunk,
|
||||
),
|
||||
),
|
||||
)
|
||||
).flat();
|
||||
|
||||
for (const [idx, ai] of ais.entries()) {
|
||||
if (ai == null || ai.data == null) {
|
||||
throw new Error(
|
||||
`AI returned null for ${oracles[idx].name} ${oracles[idx].oraclePk}!`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (ais.length != oracles.length) {
|
||||
throw new Error(
|
||||
`Expected ${oracles.length}, but gMA returned ${ais.length}!`,
|
||||
);
|
||||
}
|
||||
|
||||
const filteredOracles = oracles
|
||||
.map((o, i) => {
|
||||
return { oracle: o, ai: ais[i], decodedPullFeed: undefined };
|
||||
})
|
||||
.filter((item) => item.ai?.owner.equals(SB_ON_DEMAND_PID));
|
||||
|
||||
return filteredOracles;
|
||||
}
|
||||
|
||||
function extendOraclesManually(cluster: Cluster): {
|
||||
oraclePk: PublicKey;
|
||||
name: string;
|
||||
}[] {
|
||||
if (cluster == 'devnet') {
|
||||
return [
|
||||
{
|
||||
oraclePk: new PublicKey('EtbG8PSDCyCSmDH8RE4Nf2qTV9d6P6zShzHY2XWvjFJf'),
|
||||
name: 'BTC/USD',
|
||||
},
|
||||
];
|
||||
}
|
||||
return [
|
||||
['DIGITSOL', '2A7aqNLy26ZBSMWP2Ekxv926hj16tCA47W1sHWVqaLii'],
|
||||
['JLP', '65J9bVEMhNbtbsNgArNV1K4krzcsomjho4bgR51sZXoj'],
|
||||
['INF', 'AZcoqpWhMJUaKEDUfKsfzCr3Y96gSQwv43KSQ6KpeyQ1'],
|
||||
['GUAC', 'Ai2GsLRioGKwVgWX8dtbLF5rJJEZX17SteGEDqrpzBv3'],
|
||||
['RAY', 'AJkAFiXdbMonys8rTXZBrRnuUiLcDFdkyoPuvrVKXhex'],
|
||||
['JUP', '2F9M59yYc28WMrAymNWceaBEk8ZmDAjUAKULp8seAJF3'],
|
||||
].map((item) => {
|
||||
return {
|
||||
oraclePk: new PublicKey(item[1]),
|
||||
name: item[0],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function setupMango(): Promise<{
|
||||
group: Group;
|
||||
client: MangoClient;
|
||||
connection: Connection;
|
||||
user: Keypair;
|
||||
userProvider: AnchorProvider;
|
||||
}> {
|
||||
const options = AnchorProvider.defaultOptions();
|
||||
const connection = new Connection(CLUSTER_URL!, options);
|
||||
const user = Keypair.fromSecretKey(
|
||||
Buffer.from(
|
||||
JSON.parse(
|
||||
process.env.KEYPAIR || fs.readFileSync(USER_KEYPAIR!, 'utf-8'),
|
||||
),
|
||||
),
|
||||
);
|
||||
const userWallet = new Wallet(user);
|
||||
const userProvider = new AnchorProvider(connection, userWallet, options);
|
||||
const client = await MangoClient.connect(
|
||||
userProvider,
|
||||
CLUSTER,
|
||||
MANGO_V4_ID[CLUSTER],
|
||||
{
|
||||
idsSource: 'get-program-accounts',
|
||||
},
|
||||
);
|
||||
|
||||
const group = await client.getGroup(new PublicKey(GROUP));
|
||||
await group.reloadAll(client);
|
||||
return { group, client, connection, user, userProvider };
|
||||
}
|
||||
|
||||
function getOraclesForMangoGroup(
|
||||
group: Group,
|
||||
): { oraclePk: PublicKey; name: string }[] {
|
||||
// oracles for tokens
|
||||
const oracles1 = Array.from(group.banksMapByName.values())
|
||||
.filter(
|
||||
(b) =>
|
||||
!(
|
||||
b[0].nativeDeposits().eq(ZERO_I80F48()) &&
|
||||
b[0].nativeBorrows().eq(ZERO_I80F48()) &&
|
||||
b[0].reduceOnly == 1
|
||||
),
|
||||
)
|
||||
.map((b) => {
|
||||
return {
|
||||
oraclePk: b[0].oracle,
|
||||
|
||||
name: b[0].name,
|
||||
};
|
||||
});
|
||||
|
||||
// oracles for perp markets
|
||||
const oracles2 = Array.from(group.perpMarketsMapByName.values()).map((pM) => {
|
||||
return {
|
||||
oraclePk: pM.oracle,
|
||||
|
||||
name: pM.name,
|
||||
};
|
||||
});
|
||||
|
||||
// fallback oracles for tokens
|
||||
const oracles3 = Array.from(group.banksMapByName.values())
|
||||
.filter(
|
||||
(b) =>
|
||||
!(
|
||||
b[0].nativeDeposits().eq(ZERO_I80F48()) &&
|
||||
b[0].nativeBorrows().eq(ZERO_I80F48()) &&
|
||||
b[0].reduceOnly == 1
|
||||
),
|
||||
)
|
||||
.map((b) => {
|
||||
return {
|
||||
oraclePk: b[0].oracle,
|
||||
|
||||
name: b[0].name,
|
||||
};
|
||||
})
|
||||
.filter((item) => !item.oraclePk.equals(PublicKey.default));
|
||||
const oracles = oracles1.concat(oracles2).concat(oracles3);
|
||||
return oracles;
|
||||
}
|
||||
|
||||
async function setupSwitchboard(client: MangoClient): Promise<{
|
||||
sbOnDemandProgram: Anchor30Program<Idl>;
|
||||
crossbarClient: CrossbarClient;
|
||||
queue: PublicKey;
|
||||
}> {
|
||||
const idl = await Anchor30Program.fetchIdl(
|
||||
SB_ON_DEMAND_PID,
|
||||
client.program.provider,
|
||||
);
|
||||
const sbOnDemandProgram = new Anchor30Program(idl!, client.program.provider);
|
||||
let queue = new PublicKey('A43DyUGA7s8eXPxqEjJY6EBu1KKbNgfxF8h17VAHn13w');
|
||||
if (CLUSTER == 'devnet') {
|
||||
queue = new PublicKey('FfD96yeXs4cxZshoPPSKhSPgVQxLAJUT3gefgh84m1Di');
|
||||
}
|
||||
const crossbarClient = new CrossbarClient(
|
||||
'https://crossbar.switchboard.xyz',
|
||||
false,
|
||||
);
|
||||
return { sbOnDemandProgram, crossbarClient, queue };
|
||||
}
|
||||
|
||||
async function updateFilteredOraclesAis(
|
||||
connection: Connection,
|
||||
sbOnDemandProgram: Anchor30Program<Idl>,
|
||||
filteredOracles: OracleInterface[],
|
||||
): Promise<void> {
|
||||
const ais = (
|
||||
await Promise.all(
|
||||
chunk(
|
||||
filteredOracles.map((item) => item.oracle.oraclePk),
|
||||
50,
|
||||
false,
|
||||
).map(async (chunk) => await connection.getMultipleAccountsInfo(chunk)),
|
||||
)
|
||||
).flat();
|
||||
|
||||
filteredOracles.forEach((fo, idx) => {
|
||||
fo.ai = ais[idx];
|
||||
|
||||
const decodedPullFeed = sbOnDemandProgram.coder.accounts.decode(
|
||||
'pullFeedAccountData',
|
||||
fo.ai!.data,
|
||||
);
|
||||
fo.decodedPullFeed = decodedPullFeed;
|
||||
});
|
||||
}
|
|
@ -0,0 +1,700 @@
|
|||
import {
|
||||
LISTING_PRESETS,
|
||||
LISTING_PRESETS_KEY,
|
||||
tierSwitchboardSettings,
|
||||
tierToSwitchboardJobSwapValue,
|
||||
} from '@blockworks-foundation/mango-v4-settings/lib/helpers/listingTools';
|
||||
import {
|
||||
Cluster,
|
||||
Commitment,
|
||||
Connection,
|
||||
Keypair,
|
||||
PublicKey,
|
||||
} from '@solana/web3.js';
|
||||
|
||||
import { decodeString } from '@switchboard-xyz/common';
|
||||
import {
|
||||
asV0Tx,
|
||||
CrossbarClient,
|
||||
OracleJob,
|
||||
PullFeed,
|
||||
Queue,
|
||||
SB_ON_DEMAND_PID,
|
||||
} from '@switchboard-xyz/on-demand';
|
||||
import fs from 'fs';
|
||||
import {
|
||||
Program as Anchor30Program,
|
||||
AnchorProvider,
|
||||
Wallet,
|
||||
} from 'switchboard-anchor';
|
||||
import { struct, u8, publicKey, u64, option } from '@raydium-io/raydium-sdk';
|
||||
import * as toml from '@iarna/toml';
|
||||
import { toNative } from '../src/utils';
|
||||
|
||||
// Configuration
|
||||
const TIER: LISTING_PRESETS_KEY = 'asset_250';
|
||||
const TOKEN_MINT = 'JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN';
|
||||
|
||||
// Tier based variables
|
||||
const swapValue = tierToSwitchboardJobSwapValue[TIER];
|
||||
const settingFromLib = tierSwitchboardSettings[TIER];
|
||||
const maxVariance = LISTING_PRESETS[TIER].oracleConfFilter * 100;
|
||||
const minResponses = settingFromLib!.minRequiredOracleResults;
|
||||
const numSignatures = settingFromLib!.minRequiredOracleResults + 1;
|
||||
const minSampleSize = settingFromLib!.minRequiredOracleResults;
|
||||
const maxStaleness =
|
||||
LISTING_PRESETS[TIER].maxStalenessSlots === -1
|
||||
? 10000
|
||||
: LISTING_PRESETS[TIER].maxStalenessSlots;
|
||||
|
||||
// Constants
|
||||
const JUPITER_PRICE_API_MAINNET = 'https://price.jup.ag/v4/';
|
||||
const JUPITER_TOKEN_API_MAINNET = 'https://token.jup.ag/all';
|
||||
const WRAPPED_SOL_MINT = 'So11111111111111111111111111111111111111112';
|
||||
const PYTH_SOL_ORACLE = 'H6ARHf6YXhGYeQfUzQNGk6rDNnLBQKrenN712K4AQJEG';
|
||||
const USDC_MINT = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
|
||||
const PYTH_USDC_ORACLE = 'Gnt27xtC473ZT2Mw5u8wZ68Z3gULkSTb5DuxJy7eJotD';
|
||||
const SWITCHBOARD_USDC_ORACLE = 'FwYfsmj5x8YZXtQBNo2Cz8TE7WRCMFqA6UTffK4xQKMH';
|
||||
const CLUSTER: Cluster =
|
||||
(process.env.CLUSTER_OVERRIDE as Cluster) || 'mainnet-beta';
|
||||
const CLUSTER_URL =
|
||||
process.env.CLUSTER_URL_OVERRIDE || process.env.MB_CLUSTER_URL;
|
||||
const USER_KEYPAIR =
|
||||
process.env.USER_KEYPAIR_OVERRIDE || process.env.MB_PAYER_KEYPAIR;
|
||||
|
||||
async function setupAnchor() {
|
||||
const options = AnchorProvider.defaultOptions();
|
||||
const connection = new Connection(CLUSTER_URL!, options);
|
||||
const user = Keypair.fromSecretKey(
|
||||
Buffer.from(
|
||||
JSON.parse(
|
||||
fs.readFileSync(USER_KEYPAIR!, {
|
||||
encoding: 'utf-8',
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
const userWallet = new Wallet(user);
|
||||
const userProvider = new AnchorProvider(connection, userWallet, options);
|
||||
|
||||
return { userProvider, connection, user };
|
||||
}
|
||||
|
||||
async function getTokenPrice(mint: string): Promise<number> {
|
||||
const priceInfo = await (
|
||||
await fetch(`${JUPITER_PRICE_API_MAINNET}price?ids=${mint}`)
|
||||
).json();
|
||||
//Note: if listing asset that don't have price on jupiter remember to edit this 0 to real price
|
||||
//in case of using 0 openbook market can be wrongly configured ignore if openbook market is existing
|
||||
const price = priceInfo.data[mint]?.price || 0;
|
||||
if (!price) {
|
||||
console.log('Token price not found');
|
||||
throw 'Token price not found';
|
||||
}
|
||||
return price;
|
||||
}
|
||||
|
||||
async function getTokenInfo(mint: string): Promise<Token | undefined> {
|
||||
const response = await fetch(JUPITER_TOKEN_API_MAINNET);
|
||||
const data: Token[] = await response.json();
|
||||
const tokenInfo = data.find((x) => x.address === mint);
|
||||
if (!tokenInfo) {
|
||||
console.log('Token info not found');
|
||||
throw 'Token info not found';
|
||||
}
|
||||
return data.find((x) => x.address === mint);
|
||||
}
|
||||
|
||||
async function getPool(mint: string): Promise<
|
||||
| {
|
||||
pool: string;
|
||||
poolSource: 'raydium' | 'orca';
|
||||
isSolPool: boolean;
|
||||
isReveredSolPool: boolean;
|
||||
}
|
||||
| undefined
|
||||
> {
|
||||
const dex = await fetch(
|
||||
`https://api.dexscreener.com/latest/dex/search?q=${mint}`,
|
||||
);
|
||||
const resp = await dex.json();
|
||||
|
||||
if (!resp?.pairs?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pairs = resp.pairs.filter(
|
||||
(x) => x.dexId.includes('raydium') || x.dexId.includes('orca'),
|
||||
);
|
||||
|
||||
const bestUsdcPool = pairs.find(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(x: any) => x.quoteToken.address === USDC_MINT,
|
||||
);
|
||||
|
||||
const bestSolPool = pairs.find(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(x: any) => x.quoteToken.address === WRAPPED_SOL_MINT,
|
||||
);
|
||||
|
||||
const bestReversedSolPool = pairs.find(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(x: any) => x.baseToken.address === WRAPPED_SOL_MINT,
|
||||
);
|
||||
|
||||
if (bestUsdcPool) {
|
||||
return {
|
||||
pool: bestUsdcPool.pairAddress,
|
||||
poolSource: bestUsdcPool.dexId.includes('raydium') ? 'raydium' : 'orca',
|
||||
isSolPool: false,
|
||||
isReveredSolPool: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (bestSolPool) {
|
||||
return {
|
||||
pool: bestSolPool.pairAddress,
|
||||
poolSource: bestSolPool.dexId.includes('raydium') ? 'raydium' : 'orca',
|
||||
isSolPool: true,
|
||||
isReveredSolPool: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (bestSolPool) {
|
||||
return {
|
||||
pool: bestReversedSolPool.pairAddress,
|
||||
poolSource: bestReversedSolPool.dexId.includes('raydium')
|
||||
? 'raydium'
|
||||
: 'orca',
|
||||
isSolPool: true,
|
||||
isReveredSolPool: true,
|
||||
};
|
||||
}
|
||||
|
||||
console.log('No orca or raydium pool found');
|
||||
throw 'No orca or raydium pool found';
|
||||
}
|
||||
|
||||
const getLstStakePool = async (
|
||||
connection: Connection,
|
||||
mint: string,
|
||||
): Promise<string> => {
|
||||
try {
|
||||
let poolAddress = '';
|
||||
let addresses: string[] = [];
|
||||
try {
|
||||
const tomlFile = await fetch(
|
||||
`https://raw.githubusercontent.com/${'igneous-labs'}/${'sanctum-lst-list'}/master/sanctum-lst-list.toml`,
|
||||
);
|
||||
|
||||
const tomlText = await tomlFile.text();
|
||||
const tomlData = toml.parse(tomlText) as unknown as {
|
||||
sanctum_lst_list: { pool: { pool: string } }[];
|
||||
};
|
||||
addresses = [
|
||||
...tomlData.sanctum_lst_list
|
||||
.map((x) => tryGetPubKey(x.pool.pool)?.toBase58())
|
||||
.filter((x) => x),
|
||||
] as string[];
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
//remove duplicates
|
||||
const possibleStakePoolsAddresses = [...new Set(addresses)].map(
|
||||
(x) => new PublicKey(x),
|
||||
);
|
||||
|
||||
const accounts = await connection.getMultipleAccountsInfo(
|
||||
possibleStakePoolsAddresses,
|
||||
);
|
||||
for (const idx in accounts) {
|
||||
try {
|
||||
const acc = accounts[idx];
|
||||
const stakeAddressPk = possibleStakePoolsAddresses[idx];
|
||||
if (acc?.data) {
|
||||
const decoded = StakePoolLayout.decode(acc?.data);
|
||||
if (decoded.poolMint.toBase58() === mint && stakeAddressPk) {
|
||||
poolAddress = stakeAddressPk?.toBase58();
|
||||
break;
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
return poolAddress;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const LSTExactIn = (
|
||||
inMint: string,
|
||||
nativeInAmount: string,
|
||||
stakePoolAddress: string,
|
||||
): string => {
|
||||
const template = `tasks:
|
||||
- conditionalTask:
|
||||
attempt:
|
||||
- httpTask:
|
||||
url: https://api.sanctum.so/v1/swap/quote?input=${inMint}&outputLstMint=So11111111111111111111111111111111111111112&amount=${nativeInAmount}&mode=ExactIn
|
||||
- jsonParseTask:
|
||||
path: $.outAmount
|
||||
- divideTask:
|
||||
scalar: ${nativeInAmount}
|
||||
onFailure:
|
||||
- splStakePoolTask:
|
||||
pubkey: ${stakePoolAddress}
|
||||
- cacheTask:
|
||||
cacheItems:
|
||||
- variableName: poolTokenSupply
|
||||
job:
|
||||
tasks:
|
||||
- jsonParseTask:
|
||||
path: $.uiPoolTokenSupply
|
||||
aggregationMethod: NONE
|
||||
- variableName: totalStakeLamports
|
||||
job:
|
||||
tasks:
|
||||
- jsonParseTask:
|
||||
path: $.uiTotalLamports
|
||||
aggregationMethod: NONE
|
||||
- valueTask:
|
||||
big: \${totalStakeLamports}
|
||||
- divideTask:
|
||||
big: \${poolTokenSupply}
|
||||
- multiplyTask:
|
||||
job:
|
||||
tasks:
|
||||
- oracleTask:
|
||||
pythAddress: H6ARHf6YXhGYeQfUzQNGk6rDNnLBQKrenN712K4AQJEG
|
||||
pythAllowedConfidenceInterval: 10`;
|
||||
return template;
|
||||
};
|
||||
|
||||
const LSTExactOut = (
|
||||
inMint: string,
|
||||
nativeOutSolAmount: string,
|
||||
stakePoolAddress: string,
|
||||
): string => {
|
||||
const template = `tasks:
|
||||
- conditionalTask:
|
||||
attempt:
|
||||
- cacheTask:
|
||||
cacheItems:
|
||||
- variableName: QTY
|
||||
job:
|
||||
tasks:
|
||||
- httpTask:
|
||||
url: https://api.sanctum.so/v1/swap/quote?input=${inMint}&outputLstMint=So11111111111111111111111111111111111111112&amount=${nativeOutSolAmount}&mode=ExactOut
|
||||
- jsonParseTask:
|
||||
path: $.inAmount
|
||||
- httpTask:
|
||||
url: https://api.sanctum.so/v1/swap/quote?input=${inMint}&outputLstMint=So11111111111111111111111111111111111111112&amount=\${QTY}&mode=ExactIn
|
||||
- jsonParseTask:
|
||||
path: $.outAmount
|
||||
- divideTask:
|
||||
big: \${QTY}
|
||||
onFailure:
|
||||
- splStakePoolTask:
|
||||
pubkey: ${stakePoolAddress}
|
||||
- cacheTask:
|
||||
cacheItems:
|
||||
- variableName: poolTokenSupply
|
||||
job:
|
||||
tasks:
|
||||
- jsonParseTask:
|
||||
path: $.uiPoolTokenSupply
|
||||
aggregationMethod: NONE
|
||||
- variableName: totalStakeLamports
|
||||
job:
|
||||
tasks:
|
||||
- jsonParseTask:
|
||||
path: $.uiTotalLamports
|
||||
aggregationMethod: NONE
|
||||
- valueTask:
|
||||
big: \${totalStakeLamports}
|
||||
- divideTask:
|
||||
big: \${poolTokenSupply}
|
||||
- multiplyTask:
|
||||
job:
|
||||
tasks:
|
||||
- oracleTask:
|
||||
pythAddress: H6ARHf6YXhGYeQfUzQNGk6rDNnLBQKrenN712K4AQJEG
|
||||
pythAllowedConfidenceInterval: 10`;
|
||||
return template;
|
||||
};
|
||||
|
||||
async function setupSwitchboard(userProvider: AnchorProvider) {
|
||||
const idl = await Anchor30Program.fetchIdl(SB_ON_DEMAND_PID, userProvider);
|
||||
const sbOnDemandProgram = new Anchor30Program(idl!, userProvider);
|
||||
let queue = new PublicKey('A43DyUGA7s8eXPxqEjJY6EBu1KKbNgfxF8h17VAHn13w');
|
||||
if (CLUSTER == 'devnet') {
|
||||
queue = new PublicKey('FfD96yeXs4cxZshoPPSKhSPgVQxLAJUT3gefgh84m1Di');
|
||||
}
|
||||
const crossbarClient = new CrossbarClient(
|
||||
'https://crossbar.switchboard.xyz',
|
||||
true,
|
||||
);
|
||||
return { sbOnDemandProgram, crossbarClient, queue };
|
||||
}
|
||||
|
||||
(async function main(): Promise<void> {
|
||||
const { userProvider, connection, user } = await setupAnchor();
|
||||
const [
|
||||
{ sbOnDemandProgram, crossbarClient, queue },
|
||||
poolInfo,
|
||||
price,
|
||||
tokeninfo,
|
||||
lstPool,
|
||||
] = await Promise.all([
|
||||
setupSwitchboard(userProvider),
|
||||
getPool(TOKEN_MINT),
|
||||
getTokenPrice(TOKEN_MINT),
|
||||
getTokenInfo(TOKEN_MINT),
|
||||
getLstStakePool(connection, TOKEN_MINT),
|
||||
]);
|
||||
|
||||
const FALLBACK_POOL_NAME: 'orcaPoolAddress' | 'raydiumPoolAddress' = `${
|
||||
poolInfo?.poolSource || 'raydium'
|
||||
}PoolAddress`;
|
||||
const FALLBACK_POOL = poolInfo?.pool;
|
||||
const TOKEN_SYMBOL = tokeninfo!.symbol.toUpperCase();
|
||||
|
||||
const queueAccount = new Queue(sbOnDemandProgram, queue);
|
||||
try {
|
||||
await queueAccount.loadData();
|
||||
} catch (err) {
|
||||
console.error('Queue not found, ensure you are using devnet in your env');
|
||||
return;
|
||||
}
|
||||
|
||||
let onFailureTaskDesc: { [key: string]: any }[];
|
||||
if (!poolInfo?.isReveredSolPool) {
|
||||
onFailureTaskDesc = [
|
||||
{
|
||||
lpExchangeRateTask: {
|
||||
[FALLBACK_POOL_NAME]: FALLBACK_POOL,
|
||||
},
|
||||
},
|
||||
];
|
||||
if (poolInfo?.isSolPool) {
|
||||
onFailureTaskDesc.push({
|
||||
multiplyTask: {
|
||||
job: {
|
||||
tasks: [
|
||||
{
|
||||
oracleTask: {
|
||||
pythAddress: PYTH_SOL_ORACLE,
|
||||
pythAllowedConfidenceInterval: 10,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
onFailureTaskDesc = [
|
||||
{
|
||||
valueTask: {
|
||||
big: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
divideTask: {
|
||||
job: {
|
||||
tasks: [
|
||||
{
|
||||
lpExchangeRateTask: {
|
||||
[FALLBACK_POOL_NAME]: FALLBACK_POOL,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
if (poolInfo.isSolPool) {
|
||||
onFailureTaskDesc.push({
|
||||
multiplyTask: {
|
||||
job: {
|
||||
tasks: [
|
||||
{
|
||||
oracleTask: {
|
||||
pythAddress: PYTH_SOL_ORACLE,
|
||||
pythAllowedConfidenceInterval: 10,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const txOpts = {
|
||||
commitment: 'finalized' as Commitment,
|
||||
skipPreflight: true,
|
||||
maxRetries: 0,
|
||||
};
|
||||
|
||||
const conf = {
|
||||
name: `${TOKEN_SYMBOL}/USD`, // the feed name (max 32 bytes)
|
||||
queue, // the queue of oracles to bind to
|
||||
maxVariance: maxVariance!, // allow 1% variance between submissions and jobs
|
||||
minResponses: minResponses!, // minimum number of responses of jobs to allow
|
||||
numSignatures: numSignatures!, // number of signatures to fetch per update
|
||||
minSampleSize: minSampleSize!, // minimum number of responses to sample
|
||||
maxStaleness: maxStaleness!, // maximum staleness of responses in seconds to sample
|
||||
};
|
||||
|
||||
console.log('Initializing new data feed');
|
||||
// Generate the feed keypair
|
||||
const [pullFeed, feedKp] = PullFeed.generate(sbOnDemandProgram);
|
||||
const jobs = [
|
||||
lstPool
|
||||
? OracleJob.fromYaml(
|
||||
LSTExactIn(
|
||||
TOKEN_MINT,
|
||||
toNative(
|
||||
Math.ceil(Number(swapValue) / price),
|
||||
tokeninfo!.decimals,
|
||||
).toString(),
|
||||
lstPool,
|
||||
),
|
||||
)
|
||||
: OracleJob.fromObject({
|
||||
tasks: [
|
||||
{
|
||||
conditionalTask: {
|
||||
attempt: [
|
||||
{
|
||||
valueTask: {
|
||||
big: swapValue,
|
||||
},
|
||||
},
|
||||
{
|
||||
divideTask: {
|
||||
job: {
|
||||
tasks: [
|
||||
{
|
||||
jupiterSwapTask: {
|
||||
inTokenAddress: USDC_MINT,
|
||||
outTokenAddress: TOKEN_MINT,
|
||||
baseAmountString: swapValue,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
onFailure: onFailureTaskDesc,
|
||||
},
|
||||
},
|
||||
{
|
||||
conditionalTask: {
|
||||
attempt: [
|
||||
{
|
||||
multiplyTask: {
|
||||
job: {
|
||||
tasks: [
|
||||
{
|
||||
oracleTask: {
|
||||
pythAddress: PYTH_USDC_ORACLE,
|
||||
pythAllowedConfidenceInterval: 10,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
onFailure: [
|
||||
{
|
||||
multiplyTask: {
|
||||
job: {
|
||||
tasks: [
|
||||
{
|
||||
oracleTask: {
|
||||
switchboardAddress: SWITCHBOARD_USDC_ORACLE,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
lstPool
|
||||
? OracleJob.fromYaml(
|
||||
LSTExactOut(
|
||||
TOKEN_MINT,
|
||||
toNative(
|
||||
Math.ceil(Number(swapValue) / price),
|
||||
tokeninfo!.decimals,
|
||||
).toString(),
|
||||
lstPool,
|
||||
),
|
||||
)
|
||||
: OracleJob.fromObject({
|
||||
tasks: [
|
||||
{
|
||||
conditionalTask: {
|
||||
attempt: [
|
||||
{
|
||||
cacheTask: {
|
||||
cacheItems: [
|
||||
{
|
||||
variableName: 'QTY',
|
||||
job: {
|
||||
tasks: [
|
||||
{
|
||||
jupiterSwapTask: {
|
||||
inTokenAddress: USDC_MINT,
|
||||
outTokenAddress: TOKEN_MINT,
|
||||
baseAmountString: swapValue,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
jupiterSwapTask: {
|
||||
inTokenAddress: TOKEN_MINT,
|
||||
outTokenAddress: USDC_MINT,
|
||||
baseAmountString: '${QTY}',
|
||||
},
|
||||
},
|
||||
{
|
||||
divideTask: {
|
||||
big: '${QTY}',
|
||||
},
|
||||
},
|
||||
],
|
||||
onFailure: onFailureTaskDesc,
|
||||
},
|
||||
},
|
||||
{
|
||||
conditionalTask: {
|
||||
attempt: [
|
||||
{
|
||||
multiplyTask: {
|
||||
job: {
|
||||
tasks: [
|
||||
{
|
||||
oracleTask: {
|
||||
pythAddress: PYTH_USDC_ORACLE,
|
||||
pythAllowedConfidenceInterval: 10,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
onFailure: [
|
||||
{
|
||||
multiplyTask: {
|
||||
job: {
|
||||
tasks: [
|
||||
{
|
||||
oracleTask: {
|
||||
switchboardAddress: SWITCHBOARD_USDC_ORACLE,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
];
|
||||
const decodedFeedHash = await crossbarClient
|
||||
.store(queue.toBase58(), jobs)
|
||||
.then((resp) => decodeString(resp.feedHash));
|
||||
console.log('Feed hash:', decodedFeedHash);
|
||||
|
||||
const tx = await asV0Tx({
|
||||
connection: sbOnDemandProgram.provider.connection,
|
||||
ixs: [await pullFeed.initIx({ ...conf, feedHash: decodedFeedHash! })],
|
||||
payer: user.publicKey,
|
||||
signers: [user, feedKp],
|
||||
computeUnitPrice: 75_000,
|
||||
computeUnitLimitMultiple: 1.3,
|
||||
});
|
||||
console.log('Sending initialize transaction');
|
||||
const sim = await connection.simulateTransaction(tx, txOpts);
|
||||
const sig = await connection.sendTransaction(tx, txOpts);
|
||||
console.log(`Feed ${feedKp.publicKey} initialized: ${sig}`);
|
||||
})();
|
||||
|
||||
export type Token = {
|
||||
address: string;
|
||||
chainId: number;
|
||||
decimals: number;
|
||||
name: string;
|
||||
symbol: string;
|
||||
logoURI: string;
|
||||
extensions: {
|
||||
coingeckoId?: string;
|
||||
};
|
||||
tags: string[];
|
||||
};
|
||||
|
||||
const feeFields = [u64('denominator'), u64('numerator')];
|
||||
const StakePoolLayout = struct([
|
||||
u8('accountType'),
|
||||
publicKey('manager'),
|
||||
publicKey('staker'),
|
||||
publicKey('stakeDepositAuthority'),
|
||||
u8('stakeWithdrawBumpSeed'),
|
||||
publicKey('validatorList'),
|
||||
publicKey('reserveStake'),
|
||||
publicKey('poolMint'),
|
||||
publicKey('managerFeeAccount'),
|
||||
publicKey('tokenProgramId'),
|
||||
u64('totalLamports'),
|
||||
u64('poolTokenSupply'),
|
||||
u64('lastUpdateEpoch'),
|
||||
struct(
|
||||
[u64('unixTimestamp'), u64('epoch'), publicKey('custodian')],
|
||||
'lockup',
|
||||
),
|
||||
struct(feeFields, 'epochFee'),
|
||||
option(struct(feeFields), 'nextEpochFee'),
|
||||
option(publicKey(), 'preferredDepositValidatorVoteAddress'),
|
||||
option(publicKey(), 'preferredWithdrawValidatorVoteAddress'),
|
||||
struct(feeFields, 'stakeDepositFee'),
|
||||
struct(feeFields, 'stakeWithdrawalFee'),
|
||||
option(struct(feeFields), 'nextStakeWithdrawalFee'),
|
||||
u8('stakeReferralFee'),
|
||||
option(publicKey(), 'solDepositAuthority'),
|
||||
struct(feeFields, 'solDepositFee'),
|
||||
u8('solReferralFee'),
|
||||
option(publicKey(), 'solWithdrawAuthority'),
|
||||
struct(feeFields, 'solWithdrawalFee'),
|
||||
option(struct(feeFields), 'nextSolWithdrawalFee'),
|
||||
u64('lastEpochPoolTokenSupply'),
|
||||
u64('lastEpochTotalLamports'),
|
||||
]);
|
||||
|
||||
const tryGetPubKey = (pubkey: string | string[]) => {
|
||||
try {
|
||||
return new PublicKey(pubkey);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
};
|
|
@ -1,8 +1,12 @@
|
|||
import { AnchorProvider, Wallet } from '@coral-xyz/anchor';
|
||||
import { Magic as PythMagic } from '@pythnetwork/client';
|
||||
import { AccountInfo, Connection, PublicKey } from '@solana/web3.js';
|
||||
import { AccountInfo, Connection, Keypair, PublicKey } from '@solana/web3.js';
|
||||
import { SB_ON_DEMAND_PID } from '@switchboard-xyz/on-demand';
|
||||
import SwitchboardProgram from '@switchboard-xyz/sbv2-lite';
|
||||
import Big from 'big.js';
|
||||
import BN from 'bn.js';
|
||||
import { Program as Anchor30Program } from 'switchboard-anchor';
|
||||
|
||||
import { I80F48, I80F48Dto } from '../numbers/I80F48';
|
||||
|
||||
const SBV1_DEVNET_PID = new PublicKey(
|
||||
|
@ -33,6 +37,7 @@ export const SOL_MINT_MAINNET = new PublicKey(
|
|||
|
||||
let sbv2DevnetProgram;
|
||||
let sbv2MainnetProgram;
|
||||
let sbOnDemandProgram;
|
||||
|
||||
export enum OracleProvider {
|
||||
Pyth,
|
||||
|
@ -126,24 +131,83 @@ export function parseSwitchboardOracleV2(
|
|||
);
|
||||
|
||||
return { price, lastUpdatedSlot, uiDeviation: stdDeviation.toNumber() };
|
||||
//if oracle is badly configured or didn't publish price at least once
|
||||
//decodeLatestAggregatorValue can throw (0 switchboard rounds).
|
||||
// if oracle is badly configured or didn't publish price at least once
|
||||
// decodeLatestAggregatorValue can throw (0 switchboard rounds).
|
||||
} catch (e) {
|
||||
console.log(`Unable to parse Switchboard Oracle V2: ${oracle}`, e);
|
||||
return { price: 0, lastUpdatedSlot: 0, uiDeviation: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param accountInfo
|
||||
* @returns ui price
|
||||
*/
|
||||
export function parseSwitchboardOnDemandOracle(
|
||||
program: any,
|
||||
accountInfo: AccountInfo<Buffer>,
|
||||
oracle: PublicKey,
|
||||
): { price: number; lastUpdatedSlot: number; uiDeviation: number } {
|
||||
try {
|
||||
const decodedPullFeed = program.coder.accounts.decode(
|
||||
'pullFeedAccountData',
|
||||
accountInfo.data,
|
||||
);
|
||||
|
||||
// useful for development
|
||||
// console.log(decodedPullFeed);
|
||||
// console.log(decodedPullFeed.submissions);
|
||||
|
||||
// Use custom code instead of toFeedValue from sb on demand sdk
|
||||
// Custom code which has uses min sample size
|
||||
// const feedValue = toFeedValue(decodedPullFeed.submissions, new BN(0));
|
||||
let values = decodedPullFeed.submissions.slice(
|
||||
0,
|
||||
decodedPullFeed.minSampleSize,
|
||||
);
|
||||
if (values.length === 0) {
|
||||
return { price: 0, lastUpdatedSlot: 0, uiDeviation: 0 };
|
||||
}
|
||||
values = values.sort((x, y) => (x.value.lt(y.value) ? -1 : 1));
|
||||
const feedValue = values[Math.floor(values.length / 2)];
|
||||
const price = new Big(feedValue.value.toString()).div(1e18);
|
||||
const lastUpdatedSlot = feedValue.slot.toNumber();
|
||||
const stdDeviation = 0; // TODO the 0
|
||||
return { price, lastUpdatedSlot, uiDeviation: stdDeviation };
|
||||
|
||||
// old block, we prefer above block since we want raw data, .result is often empty
|
||||
// const price = new Big(decodedPullFeed.result.value.toString()).div(1e18);
|
||||
// const lastUpdatedSlot = decodedPullFeed.result.slot.toNumber();
|
||||
// const stdDeviation = decodedPullFeed.result.stdDev.toNumber();
|
||||
// return { price, lastUpdatedSlot, uiDeviation: stdDeviation };
|
||||
} catch (e) {
|
||||
console.log(
|
||||
`Unable to parse Switchboard On-Demand Oracle V2: ${oracle}`,
|
||||
e,
|
||||
);
|
||||
return { price: 0, lastUpdatedSlot: 0, uiDeviation: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
export async function parseSwitchboardOracle(
|
||||
oracle: PublicKey,
|
||||
accountInfo: AccountInfo<Buffer>,
|
||||
connection: Connection,
|
||||
): Promise<{ price: number; lastUpdatedSlot: number; uiDeviation: number }> {
|
||||
if (accountInfo.owner.equals(SB_ON_DEMAND_PID)) {
|
||||
if (!sbOnDemandProgram) {
|
||||
const options = AnchorProvider.defaultOptions();
|
||||
const provider = new AnchorProvider(
|
||||
connection,
|
||||
new Wallet(new Keypair()),
|
||||
options,
|
||||
);
|
||||
const idl = await Anchor30Program.fetchIdl(SB_ON_DEMAND_PID, provider);
|
||||
sbOnDemandProgram = new Anchor30Program(idl!, provider);
|
||||
}
|
||||
return parseSwitchboardOnDemandOracle(
|
||||
sbOnDemandProgram,
|
||||
accountInfo,
|
||||
oracle,
|
||||
);
|
||||
}
|
||||
|
||||
if (accountInfo.owner.equals(SwitchboardProgram.devnetPid)) {
|
||||
if (!sbv2DevnetProgram) {
|
||||
sbv2DevnetProgram = await SwitchboardProgram.loadDevnet(connection);
|
||||
|
@ -173,7 +237,8 @@ export function isSwitchboardOracle(accountInfo: AccountInfo<Buffer>): boolean {
|
|||
accountInfo.owner.equals(SBV1_DEVNET_PID) ||
|
||||
accountInfo.owner.equals(SBV1_MAINNET_PID) ||
|
||||
accountInfo.owner.equals(SwitchboardProgram.devnetPid) ||
|
||||
accountInfo.owner.equals(SwitchboardProgram.mainnetPid)
|
||||
accountInfo.owner.equals(SwitchboardProgram.mainnetPid) ||
|
||||
accountInfo.owner.equals(SB_ON_DEMAND_PID)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -4636,7 +4636,7 @@ export class MangoClient {
|
|||
|
||||
if (buyTokenPriceImpact <= 0 || sellTokenPriceImpact <= 0) {
|
||||
throw new Error(
|
||||
`Error compitong slippage/premium for token conditional swap!`,
|
||||
`Error computing slippage/premium for token conditional swap!`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
export type MangoV4 = {
|
||||
"version": "0.24.0",
|
||||
"version": "0.24.1",
|
||||
"name": "mango_v4",
|
||||
"instructions": [
|
||||
{
|
||||
|
@ -1277,6 +1277,36 @@ export type MangoV4 = {
|
|||
],
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "tokenUpdateIndexAndRateResilient",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "group",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "mintInfo",
|
||||
"isMut": false,
|
||||
"isSigner": false,
|
||||
"relations": [
|
||||
"oracle",
|
||||
"group"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "oracle",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "instructions",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
}
|
||||
],
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "accountCreate",
|
||||
"accounts": [
|
||||
|
@ -11133,6 +11163,9 @@ export type MangoV4 = {
|
|||
},
|
||||
{
|
||||
"name": "RaydiumCLMM"
|
||||
},
|
||||
{
|
||||
"name": "SwitchboardOnDemand"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -14452,7 +14485,7 @@ export type MangoV4 = {
|
|||
};
|
||||
|
||||
export const IDL: MangoV4 = {
|
||||
"version": "0.24.0",
|
||||
"version": "0.24.1",
|
||||
"name": "mango_v4",
|
||||
"instructions": [
|
||||
{
|
||||
|
@ -15730,6 +15763,36 @@ export const IDL: MangoV4 = {
|
|||
],
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "tokenUpdateIndexAndRateResilient",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "group",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "mintInfo",
|
||||
"isMut": false,
|
||||
"isSigner": false,
|
||||
"relations": [
|
||||
"oracle",
|
||||
"group"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "oracle",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "instructions",
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
}
|
||||
],
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "accountCreate",
|
||||
"accounts": [
|
||||
|
@ -25586,6 +25649,9 @@ export const IDL: MangoV4 = {
|
|||
},
|
||||
{
|
||||
"name": "RaydiumCLMM"
|
||||
},
|
||||
{
|
||||
"name": "SwitchboardOnDemand"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -125,7 +125,7 @@ export async function buildFetch(): Promise<
|
|||
> {
|
||||
let fetch = globalThis?.fetch;
|
||||
if (!fetch && process?.versions?.node) {
|
||||
fetch = (await import('node-fetch')).default;
|
||||
fetch = (await import('node-fetch')).default as any;
|
||||
}
|
||||
return fetch;
|
||||
}
|
||||
|
|
|
@ -9,6 +9,14 @@
|
|||
"skipLibCheck": true,
|
||||
"strictNullChecks": true,
|
||||
"target": "esnext",
|
||||
// "paths": {
|
||||
// "C-v1": [
|
||||
// "node_modules/C@1.0.0"
|
||||
// ],
|
||||
// "C-v2": [
|
||||
// "node_modules/C@2.0.0"
|
||||
// ]
|
||||
// }
|
||||
},
|
||||
"ts-node": {
|
||||
// these options are overrides used only by ts-node
|
||||
|
|
Loading…
Reference in New Issue