mango-v4/ts/client/scripts/sb-on-demand-create-feed.ts

707 lines
20 KiB
TypeScript

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 fs from 'fs';
import * as toml from '@iarna/toml';
import { option, publicKey, struct, u64, u8 } from '@raydium-io/raydium-sdk';
import { decodeString } from '@switchboard-xyz/common';
import {
asV0Tx,
CrossbarClient,
OracleJob,
PullFeed,
Queue,
SB_ON_DEMAND_PID,
} from '@switchboard-xyz/on-demand';
import {
Program as Anchor30Program,
AnchorProvider,
Wallet,
} from 'switchboard-anchor';
import { sendSignAndConfirmTransactions } from '@blockworks-foundation/mangolana/lib/transactions';
import { SequenceType } from '@blockworks-foundation/mangolana/lib/globalTypes';
import { createComputeBudgetIx } from '../src/utils/rpc';
// Configuration
const TIER: LISTING_PRESETS_KEY = 'asset_10';
const TOKEN_MINT = 'MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac';
// 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 =
'ef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d';
const USDC_MINT = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
const PYTH_USDC_ORACLE =
'eaa020c61cc479712813461ce153894a96a6c00b21ed0cfc2798d1f9a9e9c94a';
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',
}),
),
),
);
//@ts-ignore
const userWallet = new Wallet(user);
//@ts-ignore
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, uiAmountIn: string): string => {
const template = `tasks:
- conditionalTask:
attempt:
- sanctumLstPriceTask:
lstMint: ${inMint}
- conditionalTask:
attempt:
- valueTask:
big: ${uiAmountIn}
- divideTask:
job:
tasks:
- jupiterSwapTask:
inTokenAddress: So11111111111111111111111111111111111111112
outTokenAddress: ${inMint}
baseAmountString: ${uiAmountIn}
- conditionalTask:
attempt:
- multiplyTask:
job:
tasks:
- oracleTask:
pythAddress: ef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d
pythAllowedConfidenceInterval: 10
onFailure:
- multiplyTask:
job:
tasks:
- oracleTask:
switchboardAddress: AEcJSgRBkU9WnKCBELj66TPFfzhKWBWa4tL7JugnonUa`;
return template;
};
const LSTExactOut = (inMint: string, uiOutSolAmount: string): string => {
const template = `tasks:
- conditionalTask:
attempt:
- sanctumLstPriceTask:
lstMint: ${inMint}
- conditionalTask:
attempt:
- cacheTask:
cacheItems:
- variableName: QTY
job:
tasks:
- jupiterSwapTask:
inTokenAddress: So11111111111111111111111111111111111111112
outTokenAddress: ${inMint}
baseAmountString: ${uiOutSolAmount}
- jupiterSwapTask:
inTokenAddress: ${inMint}
outTokenAddress: So11111111111111111111111111111111111111112
baseAmountString: \${QTY}
- divideTask:
big: \${QTY}
- conditionalTask:
attempt:
- multiplyTask:
job:
tasks:
- oracleTask:
pythAddress: ef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d
pythAllowedConfidenceInterval: 10
onFailure:
- multiplyTask:
job:
tasks:
- oracleTask:
switchboardAddress: AEcJSgRBkU9WnKCBELj66TPFfzhKWBWa4tL7JugnonUa`;
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,
Math.ceil(Number(swapValue) / price).toString(),
),
)
: 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,
Math.ceil(Number(swapValue) / price).toString(),
),
)
: 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({
//@ts-ignore
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);
sendSignAndConfirmTransactions({
connection,
//@ts-ignore
wallet: new Wallet(user),
backupConnections: [],
transactionInstructions: [
{
instructionsSet: [
{
signers: [],
transactionInstruction: createComputeBudgetIx(500000),
},
...[
await pullFeed.initIx({ ...conf, feedHash: decodedFeedHash! }),
].map((tx) => ({
signers: [user, feedKp],
transactionInstruction: tx,
})),
],
sequenceType: SequenceType.Sequential,
},
],
config: {
maxTxesInBatch: 10,
maxRetries: 1,
autoRetry: true,
logFlowInfo: false,
useVersionedTransactions: true,
},
});
console.log(`Feed ${feedKp.publicKey}`);
})();
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;
}
};