diff --git a/ts/client/scripts/sb-on-demand-crank.ts b/ts/client/scripts/sb-on-demand-crank.ts index 387bf292f..a5e534479 100644 --- a/ts/client/scripts/sb-on-demand-crank.ts +++ b/ts/client/scripts/sb-on-demand-crank.ts @@ -1,4 +1,4 @@ -import { AnchorProvider, BN, Wallet } from '@coral-xyz/anchor'; +import { AnchorProvider, Wallet } from '@coral-xyz/anchor'; import { AccountInfo, Cluster, @@ -22,6 +22,7 @@ import uniq from 'lodash/uniq'; import { Program as Anchor30Program } from 'switchboard-anchor'; import { OracleConfig } from '../src/accounts/bank'; +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'; @@ -35,11 +36,223 @@ const USER_KEYPAIR = process.env.USER_KEYPAIR_OVERRIDE || process.env.MB_PAYER_KEYPAIR; const GROUP = process.env.GROUP_OVERRIDE || MANGO_V4_MAIN_GROUP.toBase58(); -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type (async function main() { - /// - /// Wallet+Client setup - /// + const { group, client, connection, user } = await setupMango(); + + const { sbOnDemandProgram, crossbarClient, queue } = await setupSwitchboard( + client, + ); + + // TODO reload group once in a while + const filteredOracles = await prepareCandidateOracles(group, client); + + // eslint-disable-next-line no-constant-condition + while (true) { + const slot = await client.connection.getSlot(); + + const staleOracles = await filterForStaleOracles( + filteredOracles, + client, + slot, + ); + + const varianceThresholdCrossedOracles = + await filterForVarianceThresholdOracles( + filteredOracles, + client, + sbOnDemandProgram, + crossbarClient, + ); + + const oraclesToCrank = uniq( + [...staleOracles, ...varianceThresholdCrossedOracles], + function (item) { + return item.oracle.oraclePk.toString(); + }, + ); + + const pullIxs: TransactionInstruction[] = []; + const lutOwners: (PublicKey | Oracle)[] = []; + for (const oracle of oraclesToCrank) { + await preparePullIx(sbOnDemandProgram, oracle, queue, lutOwners, pullIxs); + } + + for (const c of chunk(pullIxs, 5)) { + const tx = await asV0Tx({ + connection, + ixs: [...c], + signers: [user], + computeUnitPrice: 200_000, + computeUnitLimitMultiple: 1.3, + lookupTables: await loadLookupTables(lutOwners), + }); + + const txOpts = { + commitment: 'processed' as Commitment, + skipPreflight: true, + maxRetries: 0, + }; + + const sim = await client.connection.simulateTransaction(tx, txOpts); + const sig = await client.connection.sendTransaction(tx, txOpts); + console.log(`updated in ${sig}`); // TODO add token names + } + + await new Promise((r) => setTimeout(r, 5000)); + } +})(); + +async function preparePullIx( + sbOnDemandProgram, + oracle: any, + queue: PublicKey, + lutOwners: (PublicKey | Oracle)[], + pullIxs: TransactionInstruction[], +) { + const pullFeed = new PullFeed( + sbOnDemandProgram as any, + new PublicKey(oracle.oracle.oraclePk), + ); + + const decodedPullFeed = sbOnDemandProgram.coder.accounts.decode( + 'pullFeedAccountData', + oracle.ai.data, + ); + + const conf = { + queue: queue, + maxVariance: decodedPullFeed.maxVariance.toNumber(), + minResponses: decodedPullFeed.minResponses, + numSignatures: 3, // TODO hardcoded + minSampleSize: decodedPullFeed.minSampleSize, + maxStaleness: decodedPullFeed.maxStaleness, + }; + const [pullIx, responses, success] = await pullFeed.fetchUpdateIx(conf); + + const lutOwners_ = [...responses.map((x) => x.oracle), pullFeed.pubkey]; + lutOwners.push(...lutOwners_); + pullIxs.push(pullIx!); +} + +async function filterForVarianceThresholdOracles( + filteredOracles: { + oracle: { oraclePk: PublicKey; oracleConfig: OracleConfig; name: string }; + ai: AccountInfo | null; + }[], + client: MangoClient, + sbOnDemandProgram, + crossbarClient: CrossbarClient, +) { + const varianceThresholdCrossedOracles = new Array<{ + oracle: { + oraclePk: PublicKey; + oracleConfig: OracleConfig; + }; + ai: AccountInfo | null; + }>(); + for (const item of filteredOracles) { + const res = await parseSwitchboardOracle( + item.oracle.oraclePk, + item.ai!, + client.connection, + ); + + const decodedPullFeed = sbOnDemandProgram.coder.accounts.decode( + 'pullFeedAccountData', + item.ai!.data, + ); + + const crossBarSim = await crossbarClient.simulateFeeds([ + new Buffer(decodedPullFeed.feedHash).toString('hex'), + ]); + + const simPrice = + crossBarSim[0].results.reduce((a, b) => a + b, 0) / + crossBarSim[0].results.length; + + if ((res.price - simPrice) / res.price > 0.01) { + varianceThresholdCrossedOracles.push(item); + } + } + return varianceThresholdCrossedOracles; +} + +async function filterForStaleOracles( + filteredOracles: { + oracle: { oraclePk: PublicKey; oracleConfig: OracleConfig; name: string }; + ai: AccountInfo | null; + }[], + client: MangoClient, + slot: number, +) { + const staleOracles = new Array<{ + oracle: { + oraclePk: PublicKey; + oracleConfig: OracleConfig; + }; + ai: AccountInfo | null; + }>(); + for (const item of filteredOracles) { + const res = await parseSwitchboardOracle( + item.oracle.oraclePk, + item.ai!, + client.connection, + ); + + if (slot > res.lastUpdatedSlot) { + if ( + slot - res.lastUpdatedSlot > + item.oracle.oracleConfig.maxStalenessSlots.toNumber() + ) { + staleOracles.push(item); + } + } + } + return staleOracles; +} + +async function prepareCandidateOracles(group: Group, client: MangoClient) { + const oracles = getOraclesForMangoGroup(group); + oracles.push(...extendOraclesManually()); + + const ais = await client.program.provider.connection.getMultipleAccountsInfo( + oracles.map((item) => item.oraclePk), + ); + 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] }; + }) + .filter((item) => item.ai?.owner.equals(SB_ON_DEMAND_PID)); + return filteredOracles; +} + +function extendOraclesManually() { + return [ + { + oraclePk: new PublicKey('EtbG8PSDCyCSmDH8RE4Nf2qTV9d6P6zShzHY2XWvjFJf'), + oracleConfig: { + confFilter: I80F48.fromString('0.1'), + maxStalenessSlots: new BN(5), + }, + name: 'BTC/USD', + }, + ]; +} + +async function setupMango() { const options = AnchorProvider.defaultOptions(); const connection = new Connection(CLUSTER_URL!, options); const user = Keypair.fromSecretKey( @@ -64,13 +277,10 @@ const GROUP = process.env.GROUP_OVERRIDE || MANGO_V4_MAIN_GROUP.toBase58(); const group = await client.getGroup(new PublicKey(GROUP)); await group.reloadAll(client); + return { group, client, connection, user }; +} - /// - /// Prepare all oracles we want to crank - /// - - // TODO reload group once in a while - +function getOraclesForMangoGroup(group: Group) { // oracles for tokens const oracles1 = Array.from(group.banksMapByName.values()) .filter( @@ -117,42 +327,10 @@ const GROUP = process.env.GROUP_OVERRIDE || MANGO_V4_MAIN_GROUP.toBase58(); }) .filter((item) => !item.oraclePk.equals(PublicKey.default)); const oracles = oracles1.concat(oracles2).concat(oracles3); + return oracles; +} - /// Manually exclude some - // TODO - - /// Manually include some - oracles.push({ - oraclePk: new PublicKey('EtbG8PSDCyCSmDH8RE4Nf2qTV9d6P6zShzHY2XWvjFJf'), - oracleConfig: { - confFilter: I80F48.fromString('0.1'), - maxStalenessSlots: new BN(5), - }, - name: 'BTC/USD', - }); - - /// Maybe support more than one mango group - // TODO - - /// Maybe support additional via csv env param - // TODO - - /// - /// Filter for sb on demand oracles - /// - // TODO ensure ai is not null - const ais = await client.program.provider.connection.getMultipleAccountsInfo( - oracles.map((item) => item.oraclePk), - ); - const filteredOracles = oracles - .map((o, i) => { - return { oracle: o, ai: ais[i] }; - }) - .filter((item) => item.ai?.owner.equals(SB_ON_DEMAND_PID)); - - /// - /// sb - /// +async function setupSwitchboard(client: MangoClient) { const idl = await Anchor30Program.fetchIdl( SB_ON_DEMAND_PID, client.program.provider, @@ -164,135 +342,7 @@ const GROUP = process.env.GROUP_OVERRIDE || MANGO_V4_MAIN_GROUP.toBase58(); } const crossbarClient = new CrossbarClient( 'https://crossbar.switchboard.xyz', - /* verbose= */ true, + true, ); - - /// - /// Loop indefinitely - /// - // eslint-disable-next-line no-constant-condition - while (true) { - const slot = await client.connection.getSlot(); - - // filter candidates for this iteration - - // 1. stale - const staleOracles = new Array<{ - oracle: { - oraclePk: PublicKey; - oracleConfig: OracleConfig; - }; - ai: AccountInfo | null; - }>(); - for (const item of filteredOracles) { - const res = await parseSwitchboardOracle( - item.oracle.oraclePk, - item.ai!, - client.connection, - ); - - if (slot > res.lastUpdatedSlot) { - if ( - slot - res.lastUpdatedSlot > - item.oracle.oracleConfig.maxStalenessSlots.toNumber() - ) { - staleOracles.push(item); - } - } - } - - // 2. variance - const varianceThresholdCrossedOracles = new Array<{ - oracle: { - oraclePk: PublicKey; - oracleConfig: OracleConfig; - }; - ai: AccountInfo | null; - }>(); - for (const item of filteredOracles) { - const res = await parseSwitchboardOracle( - item.oracle.oraclePk, - item.ai!, - client.connection, - ); - - const decodedPullFeed = sbOnDemandProgram.coder.accounts.decode( - 'pullFeedAccountData', - item.ai!.data, - ); - - const crossBarSim = await crossbarClient.simulateFeeds([ - new Buffer(decodedPullFeed.feedHash).toString('hex'), - ]); - - const simPrice = - crossBarSim[0].results.reduce((a, b) => a + b, 0) / - crossBarSim[0].results.length; - - if ((res.price - simPrice) / res.price > 0.01) { - varianceThresholdCrossedOracles.push(item); - } - } - - // 3. stale or variance - // TODO verify this works - const oraclesToCrank = uniq( - [...staleOracles, ...varianceThresholdCrossedOracles], - function (item) { - return item.oracle.oraclePk.toString(); - }, - ); - - /// Build pull ixs - const pullIxs: TransactionInstruction[] = []; - const lutOwners: (PublicKey | Oracle)[] = []; - for (const oracle of oraclesToCrank) { - const pullFeed = new PullFeed( - sbOnDemandProgram as any, - new PublicKey(oracle.oracle.oraclePk), - ); - - const decodedPullFeed = sbOnDemandProgram.coder.accounts.decode( - 'pullFeedAccountData', - oracle.ai.data, - ); - - const conf = { - queue: queue, - maxVariance: decodedPullFeed.maxVariance.toNumber(), - minResponses: decodedPullFeed.minResponses, - numSignatures: 3, // TODO hardcoded - minSampleSize: decodedPullFeed.minSampleSize, - maxStaleness: decodedPullFeed.maxStaleness, - }; - const [pullIx, responses, success] = await pullFeed.fetchUpdateIx(conf); - - const lutOwners_ = [...responses.map((x) => x.oracle), pullFeed.pubkey]; - lutOwners.push(...lutOwners_); - pullIxs.push(pullIx!); - } - - for (const c of chunk(pullIxs, 5)) { - const tx = await asV0Tx({ - connection, - ixs: [...c], - signers: [user], - computeUnitPrice: 200_000, - computeUnitLimitMultiple: 1.3, - lookupTables: await loadLookupTables(lutOwners), - }); - - const txOpts = { - commitment: 'processed' as Commitment, - skipPreflight: true, - maxRetries: 0, - }; - - const sim = await client.connection.simulateTransaction(tx, txOpts); - const sig = await client.connection.sendTransaction(tx, txOpts); - console.log(`updated in ${sig}`); // TODO add token names - } - - await new Promise((r) => setTimeout(r, 5000)); - } -})(); + return { sbOnDemandProgram, crossbarClient, queue }; +}