Skip to content

Commit

Permalink
sbod crank: dont update if main oracle is not stale
Browse files Browse the repository at this point in the history
Signed-off-by: microwavedcola1 <[email protected]>
  • Loading branch information
microwavedcola1 committed Aug 22, 2024
1 parent 83c7efa commit e223e4b
Show file tree
Hide file tree
Showing 6 changed files with 233 additions and 67 deletions.
34 changes: 28 additions & 6 deletions ts/client/scripts/sb-on-demand-crank-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,26 @@ import { PublicKey } from '@solana/web3.js';
import { Group } from '../src/accounts/group';
import { ZERO_I80F48 } from '../src/numbers/I80F48';

export interface OraclesFromMangoGroupInterface {
oraclePk: PublicKey;
name: string;
fallbackForOracle: PublicKey | undefined;
tokenIndex: number | undefined; // todo remove
perpMarketIndex: number | undefined;
// todo: add tier when program mango-v4 24.3 is released
}

/**
* scans mango group for all oracles that need updating
* includes bank oracle, fallback oracle and perp market oracles
*/
export function getOraclesForMangoGroup(
group: Group,
): { oraclePk: PublicKey; name: string }[] {
): OraclesFromMangoGroupInterface[] {
// oracles for tokens
const oracles1 = Array.from(group.banksMapByName.values())
const oracles1: OraclesFromMangoGroupInterface[] = Array.from(
group.banksMapByName.values(),
)
.filter(
(b) =>
!(
Expand All @@ -22,22 +33,30 @@ export function getOraclesForMangoGroup(
.map((b) => {
return {
oraclePk: b[0].oracle,

name: b[0].name,
fallbackForOracle: undefined,
tokenIndex: b[0].tokenIndex,
perpMarketIndex: undefined,
};
});

// oracles for perp markets
const oracles2 = Array.from(group.perpMarketsMapByName.values()).map((pM) => {
const oracles2: OraclesFromMangoGroupInterface[] = Array.from(
group.perpMarketsMapByName.values(),
).map((pM) => {
return {
oraclePk: pM.oracle,

name: pM.name,
fallbackForOracle: undefined,
tokenIndex: undefined,
perpMarketIndex: pM.perpMarketIndex,
};
});

// fallback oracles for tokens
const oracles3 = Array.from(group.banksMapByName.values())
const oracles3: OraclesFromMangoGroupInterface[] = Array.from(
group.banksMapByName.values(),
)
.filter(
(b) =>
!(
Expand All @@ -50,6 +69,9 @@ export function getOraclesForMangoGroup(
return {
oraclePk: b[0].fallbackOracle,
name: b[0].name,
fallbackForOracle: b[0].oracle,
tokenIndex: b[0].tokenIndex,
perpMarketIndex: undefined,
};
})
.filter((item) => !item.oraclePk.equals(PublicKey.default));
Expand Down
163 changes: 135 additions & 28 deletions ts/client/scripts/sb-on-demand-crank.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,28 @@ import fs from 'fs';
import chunk from 'lodash/chunk';
import shuffle from 'lodash/shuffle';
import uniqWith from 'lodash/uniqWith';
import zipWith from 'lodash/zipWith';
import { Program as Anchor30Program, BN, Idl } from 'switchboard-anchor';

import { SequenceType } from '@blockworks-foundation/mangolana/lib/globalTypes';
import { sendSignAndConfirmTransactions } from '@blockworks-foundation/mangolana/lib/transactions';
import { BorshAccountsCoder } from '@coral-xyz/anchor';
import { AnchorProvider, Wallet } from 'switchboard-anchor';
import { TokenIndex } from '../src/accounts/bank';
import { Group } from '../src/accounts/group';
import { parseSwitchboardOracle } from '../src/accounts/oracle';
import {
isOracleStaleOrUnconfident,
parseSwitchboardOracle,
} from '../src/accounts/oracle';
import { PerpMarketIndex } from '../src/accounts/perp';
import { MangoClient } from '../src/client';
import { MANGO_V4_ID, MANGO_V4_MAIN_GROUP } from '../src/constants';
import { createComputeBudgetIx } from '../src/utils/rpc';
import { manageFeeWebSocket } from './manageFeeWs';
import { getOraclesForMangoGroup } from './sb-on-demand-crank-utils';
import {
getOraclesForMangoGroup,
OraclesFromMangoGroupInterface,
} from './sb-on-demand-crank-utils';

const CLUSTER: Cluster =
(process.env.CLUSTER_OVERRIDE as Cluster) || 'mainnet-beta';
Expand Down Expand Up @@ -59,10 +69,14 @@ try {
console.error('[start]', error);
}

interface OracleInterface {
interface OracleMetaInterface {
oracle: {
oraclePk: PublicKey;
name: string;
fallbackForOracle: PublicKey | undefined;
tokenIndex: TokenIndex | undefined;
perpMarketIndex: PerpMarketIndex | undefined;
isOracleStaleOrUnconfident: boolean;
};
ai: AccountInfo<Buffer> | null;
decodedPullFeed: any;
Expand All @@ -85,7 +99,7 @@ async function setupBackgroundRefresh(
group: Group,
sbOnDemandProgram: Anchor30Program<Idl>,
crossbarClient: CrossbarClient,
): Promise<{ oracles: OracleInterface[] }> {
): Promise<{ oracles: OracleMetaInterface[] }> {
// note: group was already reloaded before
const oracles = await prepareCandidateOracles(
client,
Expand Down Expand Up @@ -143,19 +157,29 @@ async function setupBackgroundRefresh(
]);

await updateFilteredOraclesAis(
client.connection,
client,
sbOnDemandProgram,
group,
slot,
oracles,
);

const onlyEssentialOracles = oracles.filter(
(o) => o.oracle.isOracleStaleOrUnconfident,
);

const aisUpdatedAt = Date.now();

const staleOracles = await filterForStaleOracles(oracles, client, slot);
const staleOracles = await filterForStaleOracles(
onlyEssentialOracles,
client,
slot,
);

const staleFilteredAt = Date.now();

const crossBarSims = await Promise.all(
oracles.map((o) =>
onlyEssentialOracles.map((o) =>
crossbarClient.simulateFeeds([
new Buffer(o.parsedConfigs.feedHash).toString('hex'),
]),
Expand All @@ -165,11 +189,15 @@ async function setupBackgroundRefresh(
const simulatedAt = Date.now();

const varianceThresholdCrossedOracles =
await filterForVarianceThresholdOracles(oracles, client, crossBarSims);
await filterForVarianceThresholdOracles(
onlyEssentialOracles,
client,
crossBarSims,
);

const varianceFilteredAt = Date.now();

const oraclesToCrank: OracleInterface[] = uniqWith(
const oraclesToCrank: OracleMetaInterface[] = uniqWith(
[...staleOracles, ...varianceThresholdCrossedOracles],
function (a, b) {
return a.oracle.oraclePk.equals(b.oracle.oraclePk);
Expand Down Expand Up @@ -298,7 +326,7 @@ async function setupBackgroundRefresh(
*/
async function preparePullIx(
sbOnDemandProgram,
oracle: OracleInterface,
oracle: OracleMetaInterface,
recentSlothashes?: Array<[BN, string]>,
): Promise<TransactionInstruction | undefined | null> {
const pullFeed = new PullFeed(
Expand All @@ -314,8 +342,8 @@ async function preparePullIx(
};
// TODO use fetchUpdateMany

try {
const [pullIx] = await pullFeed.fetchUpdateIx(conf, recentSlothashes);
try {
const [pullIx] = await pullFeed.fetchUpdateIx(conf, recentSlothashes);
return pullIx;
} catch (error) {
console.log(`[preparePullIx] ${oracle.oracle.name} error ${error}`);
Expand All @@ -324,11 +352,11 @@ async function preparePullIx(
}

async function filterForVarianceThresholdOracles(
filteredOracles: OracleInterface[],
filteredOracles: OracleMetaInterface[],
client: MangoClient,
crossBarSims,
): Promise<OracleInterface[]> {
const varianceThresholdCrossedOracles = new Array<OracleInterface>();
): Promise<OracleMetaInterface[]> {
const varianceThresholdCrossedOracles = new Array<OracleMetaInterface>();
for (const [index, item] of filteredOracles.entries()) {
const res = await parseSwitchboardOracle(
item.oracle.oraclePk,
Expand Down Expand Up @@ -363,11 +391,11 @@ async function filterForVarianceThresholdOracles(
}

async function filterForStaleOracles(
filteredOracles: OracleInterface[],
filteredOracles: OracleMetaInterface[],
client: MangoClient,
slot: number,
): Promise<OracleInterface[]> {
const staleOracles = new Array<OracleInterface>();
): Promise<OracleMetaInterface[]> {
const staleOracles = new Array<OracleMetaInterface>();
for (const item of filteredOracles) {
const res = await parseSwitchboardOracle(
item.oracle.oraclePk,
Expand Down Expand Up @@ -404,7 +432,7 @@ async function prepareCandidateOracles(
group: Group,
sbOnDemandProgram: Anchor30Program<Idl>,
crossbarClient: CrossbarClient,
): Promise<OracleInterface[]> {
): Promise<OracleMetaInterface[]> {
// collect
const oracles = getOraclesForMangoGroup(group);
oracles.push(...extendOraclesManually(CLUSTER));
Expand Down Expand Up @@ -444,8 +472,8 @@ async function prepareCandidateOracles(
.map((o, i) => {
return { oracle: o, ai: ais[i] };
})
.filter((item) => item.ai?.owner.equals(SB_ON_DEMAND_PID));
.filter((item) => item.ai?.owner.equals(SB_ON_DEMAND_PID));

// parse account info data
const parsedOracles = sbodOracles.map((item) => {
const d = sbOnDemandProgram.coder.accounts.decode(
Expand Down Expand Up @@ -487,15 +515,17 @@ async function prepareCandidateOracles(
}));
}

function extendOraclesManually(cluster: Cluster): {
oraclePk: PublicKey;
name: string;
}[] {
function extendOraclesManually(
cluster: Cluster,
): OraclesFromMangoGroupInterface[] {
if (cluster == 'devnet') {
return [
{
oraclePk: new PublicKey('EtbG8PSDCyCSmDH8RE4Nf2qTV9d6P6zShzHY2XWvjFJf'),
name: 'BTC/USD',
fallbackForOracle: undefined,
tokenIndex: undefined,
perpMarketIndex: undefined,
},
];
}
Expand All @@ -512,6 +542,9 @@ function extendOraclesManually(cluster: Cluster): {
return {
oraclePk: new PublicKey(item[1]),
name: item[0],
fallbackForOracle: undefined,
tokenIndex: undefined,
perpMarketIndex: undefined,
};
});
}
Expand Down Expand Up @@ -575,17 +608,21 @@ async function setupSwitchboard(client: MangoClient): Promise<{
* reloads the account states for each oracle passed through the provided connection
*/
async function updateFilteredOraclesAis(
connection: Connection,
client: MangoClient,
sbOnDemandProgram: Anchor30Program<Idl>,
filteredOracles: OracleInterface[],
group: Group,
nowSlot: number,
filteredOracles: OracleMetaInterface[],
): Promise<void> {
const ais = (
await Promise.all(
chunk(
filteredOracles.map((item) => item.oracle.oraclePk),
50,
false,
).map((chunk) => connection.getMultipleAccountsInfo(chunk)),
).map((chunk) =>
client.program.provider.connection.getMultipleAccountsInfo(chunk),
),
)
).flat();

Expand All @@ -598,4 +635,74 @@ async function updateFilteredOraclesAis(
);
fo.decodedPullFeed = decodedPullFeed;
});

// ais for oracles whose fallbacks are sbod oracles
{
const publicKeysWithIndices = filteredOracles
.map((item, idx) => {
return { publicKey: item.oracle.fallbackForOracle, idx: idx };
})
.filter((item) => item.publicKey !== undefined);
const ais = (
await Promise.all(
chunk(
publicKeysWithIndices.map((item) => item.publicKey),
50,
false,
).map((chunk_) =>
client.program.provider.connection.getMultipleAccountsInfo(chunk_),
),
)
).flat();

const coder = new BorshAccountsCoder(client.program.idl);

await Promise.all(
zipWith(publicKeysWithIndices, ais, function (publicKeyWithIndex, ai) {
return { publicKeyWithIndex, ai };
}).map(async (item) => {
const filteredOracle = filteredOracles[item.publicKeyWithIndex.idx];

let mintDecimals, maxStalenessSlots, confFilter;
if (filteredOracle.oracle.tokenIndex !== undefined) {
const bank = group.getFirstBankByTokenIndex(
filteredOracle.oracle.tokenIndex,
);
mintDecimals = bank.mintDecimals;
maxStalenessSlots = bank.oracleConfig.maxStalenessSlots;
confFilter = bank.oracleConfig.confFilter;
} else if (filteredOracle.oracle.perpMarketIndex !== undefined) {
const pm = group.getPerpMarketByMarketIndex(
filteredOracle.oracle.perpMarketIndex,
);
mintDecimals = pm.baseDecimals;
maxStalenessSlots = pm.oracleConfig.maxStalenessSlots;
confFilter = pm.oracleConfig.confFilter;
} else {
return;
}

const result = await Group.decodePriceFromOracleAi(
group,
coder,
filteredOracle.oracle.oraclePk,
item.ai,
mintDecimals,
client,
);

filteredOracle.oracle.isOracleStaleOrUnconfident =
isOracleStaleOrUnconfident(
nowSlot,
maxStalenessSlots.toNumber(),
result.lastUpdatedSlot,
result.deviation,
confFilter,
result.price,
true,
`${filteredOracle.oracle.name} fallback-main-oracle-check`,
);
}),
);
}
}
Loading

0 comments on commit e223e4b

Please sign in to comment.