ts: sb on demand crank refactor
Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>
This commit is contained in:
parent
71ac3d2e8a
commit
61fa97c5fc
|
@ -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<Buffer> | null;
|
||||
}[],
|
||||
client: MangoClient,
|
||||
sbOnDemandProgram,
|
||||
crossbarClient: CrossbarClient,
|
||||
) {
|
||||
const varianceThresholdCrossedOracles = new Array<{
|
||||
oracle: {
|
||||
oraclePk: PublicKey;
|
||||
oracleConfig: OracleConfig;
|
||||
};
|
||||
ai: AccountInfo<Buffer> | 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<Buffer> | null;
|
||||
}[],
|
||||
client: MangoClient,
|
||||
slot: number,
|
||||
) {
|
||||
const staleOracles = new Array<{
|
||||
oracle: {
|
||||
oraclePk: PublicKey;
|
||||
oracleConfig: OracleConfig;
|
||||
};
|
||||
ai: AccountInfo<Buffer> | 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<Buffer> | 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<Buffer> | 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 };
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue