import mangoStore, { CLUSTER } from '@store/mangoStore' import { ModalProps } from '../../types/modal' import Modal from '../shared/Modal' import { useWallet } from '@solana/wallet-adapter-react' import { CrankAccount, QueueAccount, SwitchboardProgram, } from '@switchboard-xyz/solana.js' import { OracleJob } from '@switchboard-xyz/common' import Button from '@components/shared/Button' import { MANGO_DAO_WALLET } from 'utils/governance/constants' import { USDC_MINT } from 'utils/constants' import { Connection, PublicKey } from '@solana/web3.js' import chunk from 'lodash/chunk' import { useTranslation } from 'next-i18next' import { notify } from 'utils/notifications' import { isMangoError } from 'types' import { useCallback, useState } from 'react' import Loading from '@components/shared/Loading' import { WhirlpoolContext, buildWhirlpoolClient } from '@orca-so/whirlpools-sdk' import { LIQUIDITY_STATE_LAYOUT_V4 } from '@raydium-io/raydium-sdk' import { LISTING_PRESETS, LISTING_PRESETS_KEY, tierSwitchboardSettings, tierToSwitchboardJobSwapValue, } from '@blockworks-foundation/mango-v4-settings/lib/helpers/listingTools' import { WRAPPED_SOL_MINT } from '@metaplex-foundation/js' import { createComputeBudgetIx, toNative, } from '@blockworks-foundation/mango-v4' import { LSTExactIn, LSTExactOut } from 'utils/switchboardTemplates/templates' import { SequenceType, TransactionInstructionWithSigners, } from '@blockworks-foundation/mangolana/lib/globalTypes' import { TransactionInstructionWithType, sendSignAndConfirmTransactions, } from '@blockworks-foundation/mangolana/lib/transactions' import { LITE_RPC_URL } from '@components/settings/RpcSettings' const poolAddressError = 'no-pool-address-found' const wrongTierPassedForCreation = 'Wrong tier passed for creation of oracle' const SWITCHBOARD_PERMISSIONLESS_QUE = '5JYwqvKkqp35w8Nq3ba4z1WYUeJQ1rB36V8XvaGp6zn1' const SWITCHBOARD_PERMISSIONLESS_CRANK = 'BKtF8yyQsj3Ft6jb2nkfpEKzARZVdGgdEPs6mFmZNmbA' const pythSolOracle = 'H6ARHf6YXhGYeQfUzQNGk6rDNnLBQKrenN712K4AQJEG' type BaseProps = ModalProps & { openbookMarketPk: string baseTokenPk: string baseTokenName: string tierKey: LISTING_PRESETS_KEY isSolPool: boolean stakePoolAddress: string tokenPrice: number tokenDecimals: number onClose: (oraclePk?: PublicKey) => void } type RaydiumProps = BaseProps & { raydiumPoolAddress: string orcaPoolAddress?: string } type OrcaProps = BaseProps & { raydiumPoolAddress?: string orcaPoolAddress: string } const CreateSwitchboardOracleModal = ({ isOpen, onClose, baseTokenPk, baseTokenName, raydiumPoolAddress, orcaPoolAddress, tierKey, tokenDecimals, tokenPrice, isSolPool, stakePoolAddress, }: RaydiumProps | OrcaProps) => { const { t } = useTranslation(['governance']) const connection = mangoStore((s) => s.connection) const client = mangoStore((s) => s.client) const wallet = useWallet() const quoteTokenName = 'USD' const pythUsdOracle = 'Gnt27xtC473ZT2Mw5u8wZ68Z3gULkSTb5DuxJy7eJotD' const switchboardUsdDaoOracle = 'FwYfsmj5x8YZXtQBNo2Cz8TE7WRCMFqA6UTffK4xQKMH' const [creatingOracle, setCreatingOracle] = useState(false) const isPoolReversed = async ( type: 'orca' | 'raydium', poolPk: string, baseMint: string, ) => { if (type === 'orca') { const context = WhirlpoolContext.from( connection, wallet as never, new PublicKey('whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc'), ) const whirlPoolClient = buildWhirlpoolClient(context) const whirlpool = await whirlPoolClient.getPool(new PublicKey(poolPk)) return whirlpool.getTokenAInfo().mint.toBase58() == baseMint || false } if (type === 'raydium') { const info = await connection.getAccountInfo(new PublicKey(poolPk)) const poolState = LIQUIDITY_STATE_LAYOUT_V4.decode(info!.data) return poolState.baseMint.toBase58() === baseMint || false } return false } const create = useCallback(async () => { try { const swapValue = tierToSwitchboardJobSwapValue[tierKey] setCreatingOracle(true) const payer = wallet!.publicKey! if (!orcaPoolAddress && !raydiumPoolAddress && !stakePoolAddress) { throw poolAddressError } const poolPropertyName = orcaPoolAddress ? 'orcaPoolAddress' : 'raydiumPoolAddress' const poolAddress = orcaPoolAddress ? orcaPoolAddress : raydiumPoolAddress const isReversePool = !stakePoolAddress ? await isPoolReversed( orcaPoolAddress ? 'orca' : 'raydium', poolAddress!, !isSolPool ? USDC_MINT : WRAPPED_SOL_MINT.toBase58(), ) : false const program = await SwitchboardProgram.load(CLUSTER, connection) const [[queueAccount], [crankAccount]] = await Promise.all([ QueueAccount.load(program, SWITCHBOARD_PERMISSIONLESS_QUE), CrankAccount.load(program, SWITCHBOARD_PERMISSIONLESS_CRANK), ]) // eslint-disable-next-line @typescript-eslint/no-explicit-any let onFailureTaskDesc: { [key: string]: any }[] if (!isReversePool) { onFailureTaskDesc = [ { lpExchangeRateTask: { [poolPropertyName]: poolAddress, }, }, ] if (isSolPool) { onFailureTaskDesc.push({ multiplyTask: { job: { tasks: [ { oracleTask: { pythAddress: pythSolOracle, pythAllowedConfidenceInterval: 10, }, }, ], }, }, }) } } else { onFailureTaskDesc = [ { valueTask: { big: 1, }, }, { divideTask: { job: { tasks: [ { lpExchangeRateTask: { [poolPropertyName]: poolAddress, }, }, ], }, }, }, ] if (isSolPool) { onFailureTaskDesc.push({ multiplyTask: { job: { tasks: [ { oracleTask: { pythAddress: pythSolOracle, pythAllowedConfidenceInterval: 10, }, }, ], }, }, }) } } const settingFromLib = tierSwitchboardSettings[tierKey] if (!settingFromLib) { throw wrongTierPassedForCreation } console.log( OracleJob.fromObject({ tasks: [ { conditionalTask: { attempt: [ { valueTask: { big: swapValue, }, }, { divideTask: { job: { tasks: [ { jupiterSwapTask: { inTokenAddress: USDC_MINT, outTokenAddress: baseTokenPk, baseAmountString: swapValue, }, }, ], }, }, }, ], onFailure: onFailureTaskDesc, }, }, { conditionalTask: { attempt: [ { multiplyTask: { job: { tasks: [ { oracleTask: { pythAddress: pythUsdOracle, pythAllowedConfidenceInterval: 10, }, }, ], }, }, }, ], onFailure: [ { multiplyTask: { job: { tasks: [ { oracleTask: { switchboardAddress: switchboardUsdDaoOracle, }, }, ], }, }, }, ], }, }, ], }).toJSON(), ) const [aggregatorAccount, txArray1] = await queueAccount.createFeedInstructions(payer, { name: `${baseTokenName}/${quoteTokenName}`, batchSize: settingFromLib.batchSize, minRequiredOracleResults: settingFromLib.minRequiredOracleResults, minRequiredJobResults: 2, minUpdateDelaySeconds: settingFromLib.minUpdateDelaySeconds, forceReportPeriod: 60 * 60, withdrawAuthority: MANGO_DAO_WALLET, authority: payer, crankDataBuffer: crankAccount.dataBuffer?.publicKey, crankPubkey: crankAccount.publicKey, fundAmount: settingFromLib.fundAmount, slidingWindow: true, disableCrank: false, maxPriorityFeeMultiplier: 5, priorityFeeBumpPeriod: 10, priorityFeeBump: 1000, basePriorityFee: 1000, jobs: [ { weight: 1, data: OracleJob.encodeDelimited( stakePoolAddress ? OracleJob.fromYaml( LSTExactIn( baseTokenPk, toNative( Math.ceil(Number(swapValue) / tokenPrice), tokenDecimals, ).toString(), stakePoolAddress, ), ) : OracleJob.fromObject({ tasks: [ { conditionalTask: { attempt: [ { valueTask: { big: swapValue, }, }, { divideTask: { job: { tasks: [ { jupiterSwapTask: { inTokenAddress: USDC_MINT, outTokenAddress: baseTokenPk, baseAmountString: swapValue, }, }, ], }, }, }, ], onFailure: onFailureTaskDesc, }, }, { conditionalTask: { attempt: [ { multiplyTask: { job: { tasks: [ { oracleTask: { pythAddress: pythUsdOracle, pythAllowedConfidenceInterval: 10, }, }, ], }, }, }, ], onFailure: [ { multiplyTask: { job: { tasks: [ { oracleTask: { switchboardAddress: switchboardUsdDaoOracle, }, }, ], }, }, }, ], }, }, ], }), ).finish(), }, { weight: 1, data: OracleJob.encodeDelimited( stakePoolAddress ? OracleJob.fromYaml( LSTExactOut( baseTokenPk, toNative( Math.ceil(Number(swapValue) / tokenPrice), tokenDecimals, ).toString(), stakePoolAddress, ), ) : OracleJob.fromObject({ tasks: [ { conditionalTask: { attempt: [ { cacheTask: { cacheItems: [ { variableName: 'QTY', job: { tasks: [ { jupiterSwapTask: { inTokenAddress: USDC_MINT, outTokenAddress: baseTokenPk, baseAmountString: swapValue, }, }, ], }, }, ], }, }, { jupiterSwapTask: { inTokenAddress: baseTokenPk, outTokenAddress: USDC_MINT, baseAmountString: '${QTY}', }, }, { divideTask: { big: '${QTY}', }, }, ], onFailure: onFailureTaskDesc, }, }, { conditionalTask: { attempt: [ { multiplyTask: { job: { tasks: [ { oracleTask: { pythAddress: pythUsdOracle, pythAllowedConfidenceInterval: 10, }, }, ], }, }, }, ], onFailure: [ { multiplyTask: { job: { tasks: [ { oracleTask: { switchboardAddress: switchboardUsdDaoOracle, }, }, ], }, }, }, ], }, }, ], }), ).finish(), }, ], }) const lockTx = aggregatorAccount.lockInstruction(payer, {}) const transferAuthIx = aggregatorAccount.setAuthorityInstruction(payer, { newAuthority: MANGO_DAO_WALLET, }) const instructions = [ ...[...txArray1, lockTx, transferAuthIx].flatMap((x) => x.ixns), ] const signers = [ ...[...txArray1, lockTx, transferAuthIx].flatMap((x) => x.signers), ] const transactionInstructions: TransactionInstructionWithType[] = [] chunk(instructions, 1).map((chunkInstructions) => transactionInstructions.push({ instructionsSet: [ new TransactionInstructionWithSigners(createComputeBudgetIx(80000)), ...chunkInstructions.map( (inst) => new TransactionInstructionWithSigners( inst, signers.filter((x) => chunkInstructions.find((instruction) => instruction.keys .filter((key) => key.isSigner) .find((key) => key.pubkey.equals(x.publicKey)), ), ), ), ), ], sequenceType: SequenceType.Sequential, }), ) await sendSignAndConfirmTransactions({ connection, wallet, transactionInstructions, backupConnections: client.opts.multipleConnections ? [...client.opts.multipleConnections] : [new Connection(LITE_RPC_URL, 'recent')], config: { maxTxesInBatch: 20, autoRetry: true, logFlowInfo: true, maxRetries: 5, }, }) notify({ type: 'success', title: 'Successfully created oracle', }) window.open( `https://app.switchboard.xyz/solana/mainnet/feed/${aggregatorAccount.publicKey.toBase58()}`, '_blank', ) setCreatingOracle(false) onClose(aggregatorAccount.publicKey) } catch (e) { console.log(e) setCreatingOracle(false) if (e === poolAddressError) { notify({ title: 'Transaction failed', description: 'No orca or raydium pool found for oracle', type: 'error', }) } else if (e === wrongTierPassedForCreation) { notify({ title: 'Transaction failed', description: 'Wrong tier passed for oracle creation', type: 'error', }) } else if (isMangoError(e)) { notify({ title: 'Transaction failed', description: e.message, txid: e?.txid, type: 'error', }) } else { notify({ title: 'Transaction failed', description: `${e}`, type: 'error', }) } } }, [ baseTokenName, baseTokenPk, client.opts.multipleConnections, connection, isPoolReversed, isSolPool, onClose, orcaPoolAddress, raydiumPoolAddress, stakePoolAddress, tierKey, tierSwitchboardSettings, tokenDecimals, tokenPrice, wallet, ]) return (

{t('create-switch-oracle')} {baseTokenName}/USD for tier{' '} {LISTING_PRESETS[tierKey].preset_name}

{t('estimated-oracle-cost')}{' '} {tierSwitchboardSettings[tierKey]?.fundAmount} SOL

This oracle can be used only with this tier or lower, cant be used with higher tiers

) } export default CreateSwitchboardOracleModal