Merge branch 'main' into deploy

This commit is contained in:
Serge Farny 2024-07-09 10:07:34 +02:00
commit b42750b674
21 changed files with 2091 additions and 352 deletions

1
.gitignore vendored
View File

@ -11,6 +11,7 @@ programs/mango-v4/src/lib-expanded.rs
dist
node_modules
yarn-error.log
.yarn
.idea
.vscode

View File

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

770
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

13
cd/switchboard-crank.toml Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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