import { AnchorProvider, Idl, Program } from '@coral-xyz/anchor' import { AccountType, getPythProgramKeyForCluster } from '@pythnetwork/client' import { PythOracle, pythOracleProgram } from '@pythnetwork/client/lib/anchor' import { PublicKey, TransactionInstruction } from '@solana/web3.js' import messageBuffer from 'message_buffer/idl/message_buffer.json' import { MessageBuffer } from 'message_buffer/idl/message_buffer' import axios from 'axios' import { useCallback, useContext, useEffect, useState } from 'react' import toast from 'react-hot-toast' import { findDetermisticAccountAddress, getMultisigCluster, getPythOracleMessageBufferCpiAuth, isMessageBufferAvailable, isRemoteCluster, mapKey, MESSAGE_BUFFER_PROGRAM_ID, MESSAGE_BUFFER_BUFFER_SIZE, PRICE_FEED_MULTISIG, PRICE_FEED_OPS_KEY, getMessageBufferAddressForPrice, getMaximumNumberOfPublishers, isPriceStoreInitialized, isPriceStorePublisherInitialized, createDetermisticPriceStoreInitializePublisherInstruction, } from '@pythnetwork/xc-admin-common' import { ClusterContext } from '../../contexts/ClusterContext' import { useMultisigContext } from '../../contexts/MultisigContext' import { usePythContext } from '../../contexts/PythContext' import { capitalizeFirstLetter } from '../../utils/capitalizeFirstLetter' import ClusterSwitch from '../ClusterSwitch' import Modal from '../common/Modal' import Spinner from '../common/Spinner' import Loadbar from '../loaders/Loadbar' import PermissionDepermissionKey from '../PermissionDepermissionKey' import { PriceRawConfig } from '../../hooks/usePyth' import { Wallet } from '@coral-xyz/anchor/dist/cjs/provider' const General = ({ proposerServerUrl }: { proposerServerUrl: string }) => { const [data, setData] = useState({}) const [dataChanges, setDataChanges] = useState>() const [existingSymbols, setExistingSymbols] = useState>(new Set()) const [isModalOpen, setIsModalOpen] = useState(false) const [isSendProposalButtonLoading, setIsSendProposalButtonLoading] = useState(false) const { cluster } = useContext(ClusterContext) const isRemote: boolean = isRemoteCluster(cluster) // Move to multisig context const { isLoading: isMultisigLoading, readOnlySquads } = useMultisigContext() const { rawConfig, dataIsLoading, connection } = usePythContext() const [pythProgramClient, setPythProgramClient] = useState>() const [messageBufferClient, setMessageBufferClient] = useState>() const openModal = () => { setIsModalOpen(true) } const closeModal = () => { setIsModalOpen(false) } const sortData = (data: any) => { const sortedData: any = {} Object.keys(data) .sort() .forEach((key) => { const sortedInnerData: any = {} Object.keys(data[key]) .sort() .forEach((innerKey) => { if (innerKey === 'metadata') { sortedInnerData[innerKey] = sortObjectByKeys(data[key][innerKey]) } else if (innerKey === 'priceAccounts') { // sort price accounts by address sortedInnerData[innerKey] = data[key][innerKey].sort( (priceAccount1: any, priceAccount2: any) => priceAccount1.address.localeCompare(priceAccount2.address) ) // sort price accounts keys sortedInnerData[innerKey] = sortedInnerData[innerKey].map( (priceAccount: any) => { const sortedPriceAccount: any = {} Object.keys(priceAccount) .sort() .forEach((priceAccountKey) => { if (priceAccountKey === 'publishers') { sortedPriceAccount[priceAccountKey] = priceAccount[ priceAccountKey ].sort((pub1: string, pub2: string) => pub1.localeCompare(pub2) ) } else { sortedPriceAccount[priceAccountKey] = priceAccount[priceAccountKey] } }) return sortedPriceAccount } ) } else { sortedInnerData[innerKey] = data[key][innerKey] } }) sortedData[key] = sortedInnerData }) return sortedData } const sortDataMemo = useCallback(sortData, []) useEffect(() => { if (!dataIsLoading && rawConfig && rawConfig.mappingAccounts.length > 0) { const symbolToData: any = {} rawConfig.mappingAccounts .sort( (mapping1, mapping2) => mapping2.products.length - mapping1.products.length )[0] .products.sort((product1, product2) => product1.metadata.symbol.localeCompare(product2.metadata.symbol) ) .map((product) => { symbolToData[product.metadata.symbol] = { address: product.address.toBase58(), metadata: { ...product.metadata, }, priceAccounts: product.priceAccounts.map((p: PriceRawConfig) => { return { address: p.address.toBase58(), publishers: p.publishers .map((p) => p.toBase58()) .slice(0, getMaximumNumberOfPublishers(cluster)), expo: p.expo, minPub: p.minPub, maxLatency: p.maxLatency, } }), } // these fields are immutable and should not be updated delete symbolToData[product.metadata.symbol].metadata.symbol delete symbolToData[product.metadata.symbol].metadata.price_account }) setExistingSymbols(new Set(Object.keys(symbolToData))) setData(sortDataMemo(symbolToData)) } }, [rawConfig, dataIsLoading, sortDataMemo, cluster]) const sortObjectByKeys = (obj: any) => { const sortedObj: any = {} Object.keys(obj) .sort() .forEach((key) => { sortedObj[key] = obj[key] }) return sortedObj } // function to download json file const handleDownloadJsonButtonClick = () => { const dataStr = 'data:text/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(data, null, 2)) const downloadAnchor = document.createElement('a') downloadAnchor.setAttribute('href', dataStr) downloadAnchor.setAttribute('download', `data-${cluster}.json`) document.body.appendChild(downloadAnchor) // required for firefox downloadAnchor.click() downloadAnchor.remove() } // function to upload json file and update changes state const handleUploadJsonButtonClick = () => { const uploadAnchor = document.createElement('input') uploadAnchor.setAttribute('type', 'file') uploadAnchor.setAttribute('accept', '.json') uploadAnchor.addEventListener('change', (e) => { const file = (e.target as HTMLInputElement).files![0] const reader = new FileReader() reader.onload = (e) => { if (e.target) { const fileData = e.target.result if (!isValidJson(fileData as string)) return const fileDataParsed = sortData(JSON.parse(fileData as string)) const changes: Record = {} Object.keys(fileDataParsed).forEach((symbol) => { // remove duplicate publishers fileDataParsed[symbol].priceAccounts[0].publishers = [ ...new Set(fileDataParsed[symbol].priceAccounts[0].publishers), ] if (!existingSymbols.has(symbol)) { // if symbol is not in existing symbols, create new entry changes[symbol] = { new: {} } changes[symbol].new = { ...fileDataParsed[symbol] } changes[symbol].new.metadata = { ...changes[symbol].new.metadata, symbol, } // these fields are generated deterministically and should not be updated delete changes[symbol].new.address delete changes[symbol].new.priceAccounts[0].address } else if ( // if symbol is in existing symbols, check if data is different JSON.stringify(data[symbol]) !== JSON.stringify(fileDataParsed[symbol]) ) { changes[symbol] = { prev: {}, new: {} } changes[symbol].prev = { ...data[symbol] } changes[symbol].new = { ...fileDataParsed[symbol] } } }) // check if any existing symbols are not in uploaded json Object.keys(data).forEach((symbol) => { if (!fileDataParsed[symbol]) { changes[symbol] = { prev: {} } changes[symbol].prev = { ...data[symbol] } } }) setDataChanges(changes) openModal() } } reader.readAsText(file) }) document.body.appendChild(uploadAnchor) // required for firefox uploadAnchor.click() uploadAnchor.remove() } // check if uploaded json is valid json const isValidJson = (json: string) => { try { JSON.parse(json) } catch (e: any) { toast.error(capitalizeFirstLetter(e.message)) return false } let isValid = true // check if json keys "address" key is changed const jsonParsed = JSON.parse(json) Object.keys(jsonParsed).forEach((symbol) => { if ( existingSymbols.has(symbol) && jsonParsed[symbol].address && jsonParsed[symbol].address !== data[symbol].address ) { toast.error( `Address field for product cannot be changed for symbol ${symbol}. Please revert any changes to the address field and try again.` ) isValid = false } }) // check if json keys "priceAccounts" key "address" key is changed Object.keys(jsonParsed).forEach((symbol) => { if ( existingSymbols.has(symbol) && jsonParsed[symbol].priceAccounts[0] && data[symbol].priceAccounts[0] && jsonParsed[symbol].priceAccounts[0].address && jsonParsed[symbol].priceAccounts[0].address !== data[symbol].priceAccounts[0].address ) { toast.error( `Address field for priceAccounts cannot be changed for symbol ${symbol}. Please revert any changes to the address field and try again.` ) isValid = false } }) // check that no price account has more than the maximum number of publishers Object.keys(jsonParsed).forEach((symbol) => { const maximumNumberOfPublishers = getMaximumNumberOfPublishers(cluster) if ( jsonParsed[symbol].priceAccounts[0].publishers.length > maximumNumberOfPublishers ) { toast.error( `${symbol} has more than ${maximumNumberOfPublishers} publishers.` ) isValid = false } }) return isValid } const handleSendProposalButtonClick = async () => { if (pythProgramClient && dataChanges && !isMultisigLoading) { const instructions: TransactionInstruction[] = [] const publisherInPriceStoreInitializationsVerified: PublicKey[] = [] for (const symbol of Object.keys(dataChanges)) { const multisigAuthority = readOnlySquads.getAuthorityPDA( PRICE_FEED_MULTISIG[getMultisigCluster(cluster)], 1 ) const fundingAccount = isRemote ? mapKey(multisigAuthority) : multisigAuthority const initPublisherInPriceStore = async (publisherKey: PublicKey) => { // Ignore this step if Price Store is not initialized (or not deployed) if (!connection || !(await isPriceStoreInitialized(connection))) { return } if ( publisherInPriceStoreInitializationsVerified.every( (el) => !el.equals(publisherKey) ) ) { if ( !connection || !(await isPriceStorePublisherInitialized( connection, publisherKey )) ) { instructions.push( await createDetermisticPriceStoreInitializePublisherInstruction( fundingAccount, publisherKey ) ) } publisherInPriceStoreInitializationsVerified.push(publisherKey) } } const { prev, new: newChanges } = dataChanges[symbol] // if prev is undefined, it means that the symbol is new if (!prev) { // deterministically generate product account key const productAccountKey: PublicKey = ( await findDetermisticAccountAddress( AccountType.Product, symbol, cluster ) )[0] // create add product account instruction instructions.push( await pythProgramClient.methods .addProduct({ ...newChanges.metadata }) .accounts({ fundingAccount, tailMappingAccount: rawConfig.mappingAccounts[0].address, productAccount: productAccountKey, }) .instruction() ) // deterministically generate price account key const priceAccountKey: PublicKey = ( await findDetermisticAccountAddress( AccountType.Price, symbol, cluster ) )[0] // create add price account instruction instructions.push( await pythProgramClient.methods .addPrice(newChanges.priceAccounts[0].expo, 1) .accounts({ fundingAccount, productAccount: productAccountKey, priceAccount: priceAccountKey, }) .instruction() ) if (isMessageBufferAvailable(cluster) && messageBufferClient) { // create create buffer instruction for the price account instructions.push( await messageBufferClient.methods .createBuffer( getPythOracleMessageBufferCpiAuth(cluster), priceAccountKey, MESSAGE_BUFFER_BUFFER_SIZE ) .accounts({ admin: fundingAccount, payer: PRICE_FEED_OPS_KEY, }) .remainingAccounts([ { pubkey: getMessageBufferAddressForPrice( cluster, priceAccountKey ), isSigner: false, isWritable: true, }, ]) .instruction() ) } // create add publisher instruction if there are any publishers for (const publisherKey of newChanges.priceAccounts[0].publishers) { const publisherPubKey = new PublicKey(publisherKey) instructions.push( await pythProgramClient.methods .addPublisher(publisherPubKey) .accounts({ fundingAccount, priceAccount: priceAccountKey, }) .instruction() ) await initPublisherInPriceStore(publisherPubKey) } // create set min publisher instruction if there are any publishers if (newChanges.priceAccounts[0].minPub !== undefined) { instructions.push( await pythProgramClient.methods .setMinPub(newChanges.priceAccounts[0].minPub, [0, 0, 0]) .accounts({ priceAccount: priceAccountKey, fundingAccount, }) .instruction() ) } } else if (!newChanges) { const priceAccount = new PublicKey(prev.priceAccounts[0].address) // if new is undefined, it means that the symbol is deleted // create delete price account instruction instructions.push( await pythProgramClient.methods .delPrice() .accounts({ fundingAccount, productAccount: new PublicKey(prev.address), priceAccount, }) .instruction() ) // create delete product account instruction instructions.push( await pythProgramClient.methods .delProduct() .accounts({ fundingAccount, mappingAccount: rawConfig.mappingAccounts[0].address, productAccount: new PublicKey(prev.address), }) .instruction() ) if (isMessageBufferAvailable(cluster) && messageBufferClient) { // create delete buffer instruction for the price buffer instructions.push( await messageBufferClient.methods .deleteBuffer( getPythOracleMessageBufferCpiAuth(cluster), priceAccount ) .accounts({ admin: fundingAccount, payer: PRICE_FEED_OPS_KEY, messageBuffer: getMessageBufferAddressForPrice( cluster, priceAccount ), }) .instruction() ) } } else { // check if metadata has changed if ( JSON.stringify(prev.metadata) !== JSON.stringify(newChanges.metadata) ) { // create update product account instruction instructions.push( await pythProgramClient.methods .updProduct({ symbol, ...newChanges.metadata }) // If there's a symbol in newChanges.metadata, it will overwrite the current symbol .accounts({ fundingAccount, productAccount: new PublicKey(prev.address), }) .instruction() ) } if ( JSON.stringify(prev.priceAccounts[0].expo) !== JSON.stringify(newChanges.priceAccounts[0].expo) ) { // create update exponent instruction instructions.push( await pythProgramClient.methods .setExponent(newChanges.priceAccounts[0].expo, 1) .accounts({ fundingAccount, priceAccount: new PublicKey(prev.priceAccounts[0].address), }) .instruction() ) } // check if maxLatency has changed if ( prev.priceAccounts[0].maxLatency !== newChanges.priceAccounts[0].maxLatency ) { // create update product account instruction instructions.push( await pythProgramClient.methods .setMaxLatency( newChanges.priceAccounts[0].maxLatency, [0, 0, 0] ) .accounts({ priceAccount: new PublicKey(prev.priceAccounts[0].address), fundingAccount, }) .instruction() ) } // check if publishers have changed const publisherKeysToAdd = newChanges.priceAccounts[0].publishers.filter( (newPublisher: string) => !prev.priceAccounts[0].publishers.includes(newPublisher) ) // check if there are any publishers to remove by comparing prev and new const publisherKeysToRemove = prev.priceAccounts[0].publishers.filter( (prevPublisher: string) => !newChanges.priceAccounts[0].publishers.includes(prevPublisher) ) // add instructions to remove publishers for (const publisherKey of publisherKeysToRemove) { instructions.push( await pythProgramClient.methods .delPublisher(new PublicKey(publisherKey)) .accounts({ fundingAccount, priceAccount: new PublicKey(prev.priceAccounts[0].address), }) .instruction() ) } // add instructions to add new publishers for (const publisherKey of publisherKeysToAdd) { const publisherPubKey = new PublicKey(publisherKey) instructions.push( await pythProgramClient.methods .addPublisher(publisherPubKey) .accounts({ fundingAccount, priceAccount: new PublicKey(prev.priceAccounts[0].address), }) .instruction() ) await initPublisherInPriceStore(publisherPubKey) } // check if minPub has changed if ( prev.priceAccounts[0].minPub !== newChanges.priceAccounts[0].minPub ) { // create update product account instruction instructions.push( await pythProgramClient.methods .setMinPub(newChanges.priceAccounts[0].minPub, [0, 0, 0]) .accounts({ priceAccount: new PublicKey(prev.priceAccounts[0].address), fundingAccount, }) .instruction() ) } } } setIsSendProposalButtonLoading(true) try { const response = await axios.post(proposerServerUrl + '/api/propose', { instructions, cluster, }) const { proposalPubkey } = response.data toast.success(`Proposal sent! 🚀 Proposal Pubkey: ${proposalPubkey}`) setIsSendProposalButtonLoading(false) closeModal() } catch (error: any) { if (error.response) { toast.error(capitalizeFirstLetter(error.response.data)) } else { toast.error(capitalizeFirstLetter(error.message)) } setIsSendProposalButtonLoading(false) } } } const MetadataChangesRows = ({ changes }: { changes: any }) => { const addPriceFeed = changes.prev === undefined && changes.new !== undefined return ( <> {Object.keys(changes.new).map( (metadataKey) => (addPriceFeed || changes.prev[metadataKey] !== changes.new[metadataKey]) && ( {metadataKey .split('_') .map((word) => capitalizeFirstLetter(word)) .join(' ')} {!addPriceFeed ? ( <> {changes.prev[metadataKey]}
{' '} ) : null} {changes.new[metadataKey]} ) )} ) } const PriceAccountsChangesRows = ({ changes }: { changes: any }) => { const addPriceFeed = changes.prev === undefined && changes.new !== undefined return ( <> {changes.new.map((priceAccount: any, index: number) => Object.keys(priceAccount).map((priceAccountKey) => priceAccountKey === 'publishers' ? ( addPriceFeed ? ( ) : ( JSON.stringify(changes.prev[index][priceAccountKey]) !== JSON.stringify(priceAccount[priceAccountKey]) && ( ) ) ) : ( (addPriceFeed || changes.prev[index][priceAccountKey] !== priceAccount[priceAccountKey]) && ( {priceAccountKey .split('_') .map((word) => capitalizeFirstLetter(word)) .join(' ')} {!addPriceFeed ? ( <> {changes.prev[index][priceAccountKey]}
) : null} {priceAccount[priceAccountKey]} ) ) ) )} ) } const PublisherKeysChangesRows = ({ changes }: { changes: any }) => { const addPriceFeed = changes.prev === undefined && changes.new !== undefined const publisherKeysToAdd = addPriceFeed ? changes.new : changes.new.filter( (newPublisher: string) => !changes.prev.includes(newPublisher) ) const publisherKeysToRemove = addPriceFeed ? [] : changes.prev.filter( (prevPublisher: string) => !changes.new.includes(prevPublisher) ) return ( <> {publisherKeysToRemove.length > 0 && ( Remove Publisher(s) {publisherKeysToRemove.map((publisherKey: string) => ( {publisherKey} ))} )} {publisherKeysToAdd.length > 0 && ( Add Publisher(s) {publisherKeysToAdd.map((publisherKey: string) => ( {publisherKey} ))} )} ) } const NewPriceFeedsRows = ({ priceFeedData }: { priceFeedData: any }) => { return ( <> ) } const OldPriceFeedsRows = ({ priceFeedSymbol, }: { priceFeedSymbol: string }) => { return ( <> Symbol {priceFeedSymbol} ) } const ModalContent = ({ changes }: { changes: any }) => { return ( <> {Object.keys(changes).length > 0 ? ( {/* compare changes.prev and changes.new and display the fields that are different */} {Object.keys(changes).map((key) => { const { prev, new: newChanges } = changes[key] const addPriceFeed = prev === undefined && newChanges !== undefined const deletePriceFeed = prev !== undefined && newChanges === undefined const diff = addPriceFeed || deletePriceFeed ? [] : Object.keys(prev).filter( (k) => JSON.stringify(prev[k]) !== JSON.stringify(newChanges[k]) ) return ( {addPriceFeed ? ( ) : deletePriceFeed ? ( ) : ( diff.map((k) => k === 'metadata' ? ( ) : k === 'priceAccounts' ? ( ) : null ) )} {/* add a divider only if its not the last item */} {Object.keys(changes).indexOf(key) !== Object.keys(changes).length - 1 ? ( ) : null} ) })}
{addPriceFeed ? 'Add New Price Feed' : deletePriceFeed ? 'Delete Old Price Feed' : key}

) : (

No proposed changes.

)} {Object.keys(changes).length > 0 && ( <> )} ) } useEffect(() => { if (connection) { const provider = new AnchorProvider( connection, readOnlySquads.wallet as Wallet, AnchorProvider.defaultOptions() ) setPythProgramClient( pythOracleProgram(getPythProgramKeyForCluster(cluster), provider) ) if (isMessageBufferAvailable(cluster)) { setMessageBufferClient( new Program( messageBuffer as Idl, new PublicKey(MESSAGE_BUFFER_PROGRAM_ID), provider ) as unknown as Program ) } } }, [connection, cluster, readOnlySquads]) return (
} />

General

{dataIsLoading ? (
) : (
)}
) } export default General