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 { 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 { 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 => { 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 { 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; } };