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 { PublicKey, Transaction } 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' const poolAddressError = 'no-pool-address-found' const SWITCHBOARD_PERMISSIONLESS_QUE = '5JYwqvKkqp35w8Nq3ba4z1WYUeJQ1rB36V8XvaGp6zn1' const SWITCHBOARD_PERMISSIONLESS_CRANK = 'BKtF8yyQsj3Ft6jb2nkfpEKzARZVdGgdEPs6mFmZNmbA' type BaseProps = ModalProps & { openbookMarketPk: string baseTokenPk: string baseTokenName: string tier: string } type RaydiumProps = BaseProps & { raydiumPoolAddress: string orcaPoolAddress?: string } type OrcaProps = BaseProps & { raydiumPoolAddress?: string orcaPoolAddress: string } const CreateSwitchboardOracleModal = ({ isOpen, onClose, baseTokenPk, baseTokenName, raydiumPoolAddress, orcaPoolAddress, tier, }: RaydiumProps | OrcaProps) => { const { t } = useTranslation(['governance']) const connection = mangoStore((s) => s.connection) const wallet = useWallet() const quoteTokenName = 'USD' const pythUsdOracle = 'Gnt27xtC473ZT2Mw5u8wZ68Z3gULkSTb5DuxJy7eJotD' const tierToSwapValue: { [key: string]: string } = { PREMIUM: '10000', MID: '2000', MEME: '500', SHIT: '100', UNTRUSTED: '100', } const tierSettings: { [key: string]: { varianceThreshold: number fundAmount: number } } = { PREMIUM: { varianceThreshold: 0.62, fundAmount: 5, }, MID: { varianceThreshold: 0.62, fundAmount: 5, }, MEME: { varianceThreshold: 1, fundAmount: 2, }, SHIT: { varianceThreshold: 1, fundAmount: 2, }, UNTRUSTED: { varianceThreshold: 1, fundAmount: 2, }, } const [creatingOracle, setCreatingOracle] = useState(false) const isPoolReversed = async (type: 'orca' | 'raydium', poolPk: 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() == USDC_MINT || false } if (type === 'raydium') { const info = await connection.getAccountInfo(new PublicKey(poolPk)) const poolState = LIQUIDITY_STATE_LAYOUT_V4.decode(info!.data) return poolState.quoteMint.toBase58() === USDC_MINT || false } return false } const create = useCallback(async () => { try { const swapValue = tierToSwapValue[tier] setCreatingOracle(true) const payer = wallet!.publicKey! if (!orcaPoolAddress && !raydiumPoolAddress) { throw poolAddressError } const poolPropertyName = orcaPoolAddress ? 'orcaPoolAddress' : 'raydiumPoolAddress' const poolAddress = orcaPoolAddress ? orcaPoolAddress : raydiumPoolAddress const isReversePool = await isPoolReversed( orcaPoolAddress ? 'orca' : 'raydium', poolAddress!, ) 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), ]) let onFailureTaskDesc if (isReversePool) { onFailureTaskDesc = [ { lpExchangeRateTask: { [poolPropertyName]: poolAddress, }, }, ] } else { onFailureTaskDesc = [ { valueTask: { big: 1, }, }, { divideTask: { job: { tasks: [ { lpExchangeRateTask: { [poolPropertyName]: poolAddress, }, }, ], }, }, }, ] } const [aggregatorAccount, txArray1] = await queueAccount.createFeedInstructions(payer, { name: `${baseTokenName}/${quoteTokenName}`, batchSize: 6, minRequiredOracleResults: 3, minRequiredJobResults: 2, minUpdateDelaySeconds: 6, forceReportPeriod: 60 * 60, withdrawAuthority: MANGO_DAO_WALLET, authority: payer, crankDataBuffer: crankAccount.dataBuffer?.publicKey, crankPubkey: crankAccount.publicKey, fundAmount: tierSettings[tier].fundAmount, slidingWindow: true, disableCrank: false, varianceThreshold: tierSettings[tier].varianceThreshold, maxPriorityFeeMultiplier: 5, priorityFeeBumpPeriod: 10, priorityFeeBump: 1000, basePriorityFee: 1000, jobs: [ { weight: 1, data: OracleJob.encodeDelimited( OracleJob.fromObject({ tasks: [ { conditionalTask: { attempt: [ { valueTask: { big: swapValue, }, }, { divideTask: { job: { tasks: [ { jupiterSwapTask: { inTokenAddress: USDC_MINT, outTokenAddress: baseTokenPk, baseAmountString: swapValue, }, }, ], }, }, }, ], onFailure: [ { lpExchangeRateTask: { [poolPropertyName]: poolAddress, }, }, ], }, }, { multiplyTask: { job: { tasks: [ { oracleTask: { pythAddress: pythUsdOracle, pythAllowedConfidenceInterval: 0.1, }, }, ], }, }, }, ], }), ).finish(), }, { weight: 1, data: OracleJob.encodeDelimited( 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, }, }, { multiplyTask: { job: { tasks: [ { oracleTask: { pythAddress: pythUsdOracle, pythAllowedConfidenceInterval: 0.1, }, }, ], }, }, }, ], }), ).finish(), }, ], }) const lockTx = aggregatorAccount.lockInstruction(payer, {}) const transferAuthIx = aggregatorAccount.setAuthorityInstruction(payer, { newAuthority: MANGO_DAO_WALLET, }) const txChunks = chunk([...txArray1, lockTx, transferAuthIx], 1) const transactions: Transaction[] = [] const latestBlockhash = await connection.getLatestBlockhash('confirmed') for (const chunk of txChunks) { const tx = new Transaction() const singers = [...chunk.flatMap((x) => x.signers)] tx.add(...chunk.flatMap((x) => x.ixns)) tx.lastValidBlockHeight = latestBlockhash.lastValidBlockHeight tx.recentBlockhash = latestBlockhash.blockhash tx.feePayer = payer if (singers.length) { tx.sign(...singers) } transactions.push(tx) } const signedTransactions = await wallet.signAllTransactions!(transactions) for (const tx of signedTransactions) { const rawTransaction = tx.serialize() const address = await connection.sendRawTransaction(rawTransaction, { skipPreflight: true, }) await connection.confirmTransaction({ blockhash: latestBlockhash.blockhash, lastValidBlockHeight: latestBlockhash.lastValidBlockHeight, signature: address, }) } setCreatingOracle(false) onClose() } catch (e) { setCreatingOracle(false) if (e === poolAddressError) { notify({ title: 'Transaction failed', description: 'No orca or raydium pool found for oracle', type: 'error', }) } else { if (!isMangoError(e)) return notify({ title: 'Transaction failed', description: e.message, txid: e?.txid, type: 'error', }) } } }, [ baseTokenName, baseTokenPk, connection, onClose, orcaPoolAddress, raydiumPoolAddress, tier, tierToSwapValue, wallet, ]) return (

{t('create-switch-oracle')} {baseTokenName}/USD

{t('estimated-oracle-cost')} {tierSettings[tier].fundAmount} SOL

) } export default CreateSwitchboardOracleModal