From fa04b6b360456531f4c6c55cbb84789a47fd3b8b Mon Sep 17 00:00:00 2001 From: Pavel Strakhov Date: Mon, 16 Sep 2024 12:16:29 +0100 Subject: [PATCH] feat(governance/xc_admin): initialize publisher buffers in cli and frontend --- .../packages/xc_admin_cli/src/index.ts | 70 +++++++++++++++++++ .../packages/xc_admin_common/src/index.ts | 1 + .../xc_admin_common/src/price_store.ts | 59 ++++++++++++---- .../components/PermissionDepermissionKey.tsx | 25 ++++++- .../components/tabs/General.tsx | 30 ++++++++ 5 files changed, 170 insertions(+), 15 deletions(-) diff --git a/governance/xc_admin/packages/xc_admin_cli/src/index.ts b/governance/xc_admin/packages/xc_admin_cli/src/index.ts index 542272efe..68c0eacb1 100644 --- a/governance/xc_admin/packages/xc_admin_cli/src/index.ts +++ b/governance/xc_admin/packages/xc_admin_cli/src/index.ts @@ -39,9 +39,12 @@ import { MultisigParser, MultisigVault, PROGRAM_AUTHORITY_ESCROW, + createDetermisticPriceStoreInitializePublisherInstruction, + createPriceStoreInstruction, findDetermisticStakeAccountAddress, getMultisigCluster, getProposalInstructions, + isPriceStorePublisherInitialized, } from "@pythnetwork/xc-admin-common"; import { @@ -559,6 +562,73 @@ multisigCommand( ); }); +multisigCommand("init-price-store", "Init price store program").action( + async (options: any) => { + const vault = await loadVaultFromOptions(options); + const cluster: PythCluster = options.cluster; + const authorityKey = await vault.getVaultAuthorityPDA(cluster); + const instruction = createPriceStoreInstruction({ + type: "Initialize", + data: { + authorityKey, + payerKey: authorityKey, + }, + }); + await vault.proposeInstructions( + [instruction], + cluster, + DEFAULT_PRIORITY_FEE_CONFIG + ); + } +); + +multisigCommand("init-price-store-buffers", "Init price store buffers").action( + async (options: any) => { + const vault = await loadVaultFromOptions(options); + const cluster: PythCluster = options.cluster; + const oracleProgramId = getPythProgramKeyForCluster(cluster); + const connection = new Connection(getPythClusterApiUrl(cluster)); + const authorityKey = await vault.getVaultAuthorityPDA(cluster); + + const allPythAccounts = await connection.getProgramAccounts( + oracleProgramId + ); + const allPublishers: Set = new Set(); + for (const account of allPythAccounts) { + const data = account.account.data; + const base = parseBaseData(data); + if (base?.type === AccountType.Price) { + const parsed = parsePriceData(data); + for (const component of parsed.priceComponents.slice( + 0, + parsed.numComponentPrices + )) { + allPublishers.add(component.publisher); + } + } + } + + let instructions = []; + for (const publisherKey of allPublishers) { + if (await isPriceStorePublisherInitialized(connection, publisherKey)) { + // Already configured. + continue; + } + instructions.push( + await createDetermisticPriceStoreInitializePublisherInstruction( + authorityKey, + publisherKey + ) + ); + } + await vault.proposeInstructions( + instructions, + cluster, + DEFAULT_PRIORITY_FEE_CONFIG + ); + } +); + program .command("parse-transaction") .description("Parse a transaction sitting in the multisig") diff --git a/governance/xc_admin/packages/xc_admin_common/src/index.ts b/governance/xc_admin/packages/xc_admin_common/src/index.ts index b1252bd58..3ffc5923f 100644 --- a/governance/xc_admin/packages/xc_admin_common/src/index.ts +++ b/governance/xc_admin/packages/xc_admin_common/src/index.ts @@ -12,3 +12,4 @@ export * from "./message_buffer"; export * from "./executor"; export * from "./chains"; export * from "./deterministic_stake_accounts"; +export * from "./price_store"; diff --git a/governance/xc_admin/packages/xc_admin_common/src/price_store.ts b/governance/xc_admin/packages/xc_admin_common/src/price_store.ts index 67f797422..7fa53e0ad 100644 --- a/governance/xc_admin/packages/xc_admin_common/src/price_store.ts +++ b/governance/xc_admin/packages/xc_admin_common/src/price_store.ts @@ -1,5 +1,6 @@ import { AccountMeta, + Connection, MAX_SEED_LENGTH, PublicKey, SystemProgram, @@ -45,15 +46,28 @@ enum InstructionId { InitializePublisher = 2, } +export function findPriceStoreConfigAddress(): [PublicKey, number] { + return PublicKey.findProgramAddressSync( + [Buffer.from("CONFIG")], + PRICE_STORE_PROGRAM_ID + ); +} + +export function findPriceStorePublisherConfigAddress( + publisherKey: PublicKey +): [PublicKey, number] { + return PublicKey.findProgramAddressSync( + [Buffer.from("PUBLISHER_CONFIG"), publisherKey.toBuffer()], + PRICE_STORE_PROGRAM_ID + ); +} + export function createPriceStoreInstruction( data: PriceStoreInstruction ): TransactionInstruction { switch (data.type) { case "Initialize": { - const [configKey, configBump] = PublicKey.findProgramAddressSync( - [Buffer.from("CONFIG")], - PRICE_STORE_PROGRAM_ID - ); + const [configKey, configBump] = findPriceStoreConfigAddress(); const instructionData = Buffer.concat([ Buffer.from([InstructionId.Initialize, configBump]), data.data.authorityKey.toBuffer(), @@ -82,15 +96,9 @@ export function createPriceStoreInstruction( }); } case "InitializePublisher": { - const [configKey, configBump] = PublicKey.findProgramAddressSync( - [Buffer.from("CONFIG")], - PRICE_STORE_PROGRAM_ID - ); + const [configKey, configBump] = findPriceStoreConfigAddress(); const [publisherConfigKey, publisherConfigBump] = - PublicKey.findProgramAddressSync( - [Buffer.from("PUBLISHER_CONFIG"), data.data.publisherKey.toBuffer()], - PRICE_STORE_PROGRAM_ID - ); + findPriceStorePublisherConfigAddress(data.data.publisherKey); const instructionData = Buffer.concat([ Buffer.from([ InstructionId.InitializePublisher, @@ -273,5 +281,32 @@ export async function findDetermisticPublisherBufferAddress( return [address, seed]; } +export async function createDetermisticPriceStoreInitializePublisherInstruction( + authorityKey: PublicKey, + publisherKey: PublicKey +): Promise { + const bufferKey = ( + await findDetermisticPublisherBufferAddress(publisherKey) + )[0]; + return createPriceStoreInstruction({ + type: "InitializePublisher", + data: { + authorityKey, + bufferKey, + publisherKey, + }, + }); +} + +export async function isPriceStorePublisherInitialized( + connection: Connection, + publisherKey: PublicKey +): Promise { + const publisherConfigKey = + findPriceStorePublisherConfigAddress(publisherKey)[0]; + const response = await connection.getAccountInfo(publisherConfigKey); + return response !== null; +} + // Recommended buffer size, enough to hold 5000 prices. export const PRICE_STORE_BUFFER_SPACE = 100048; diff --git a/governance/xc_admin/packages/xc_admin_frontend/components/PermissionDepermissionKey.tsx b/governance/xc_admin/packages/xc_admin_frontend/components/PermissionDepermissionKey.tsx index 37e165cf9..ac1f7c9ac 100644 --- a/governance/xc_admin/packages/xc_admin_frontend/components/PermissionDepermissionKey.tsx +++ b/governance/xc_admin/packages/xc_admin_frontend/components/PermissionDepermissionKey.tsx @@ -10,8 +10,10 @@ import axios from 'axios' import { Fragment, useContext, useEffect, useState } from 'react' import toast from 'react-hot-toast' import { + createDetermisticPriceStoreInitializePublisherInstruction, getMaximumNumberOfPublishers, getMultisigCluster, + isPriceStorePublisherInitialized, isRemoteCluster, mapKey, PRICE_FEED_MULTISIG, @@ -53,7 +55,7 @@ const PermissionDepermissionKey = ({ const [isSubmitButtonLoading, setIsSubmitButtonLoading] = useState(false) const [priceAccounts, setPriceAccounts] = useState([]) const { cluster } = useContext(ClusterContext) - const { rawConfig, dataIsLoading } = usePythContext() + const { rawConfig, dataIsLoading, connection } = usePythContext() const { connected } = useWallet() // get current input value @@ -86,11 +88,12 @@ const PermissionDepermissionKey = ({ ? mapKey(multisigAuthority) : multisigAuthority + const publisherPublicKey = new PublicKey(publisherKey) for (const priceAccount of priceAccounts) { if (isPermission) { instructions.push( await pythProgramClient.methods - .addPublisher(new PublicKey(publisherKey)) + .addPublisher(publisherPublicKey) .accounts({ fundingAccount, priceAccount: priceAccount, @@ -100,7 +103,7 @@ const PermissionDepermissionKey = ({ } else { instructions.push( await pythProgramClient.methods - .delPublisher(new PublicKey(publisherKey)) + .delPublisher(publisherPublicKey) .accounts({ fundingAccount, priceAccount: priceAccount, @@ -109,6 +112,22 @@ const PermissionDepermissionKey = ({ ) } } + if (isPermission) { + if ( + !connection || + !(await isPriceStorePublisherInitialized( + connection, + publisherPublicKey + )) + ) { + instructions.push( + await createDetermisticPriceStoreInitializePublisherInstruction( + fundingAccount, + publisherPublicKey + ) + ) + } + } setIsSubmitButtonLoading(true) try { const response = await axios.post(proposerServerUrl + '/api/propose', { diff --git a/governance/xc_admin/packages/xc_admin_frontend/components/tabs/General.tsx b/governance/xc_admin/packages/xc_admin_frontend/components/tabs/General.tsx index 9a8f2854d..ecef00db3 100644 --- a/governance/xc_admin/packages/xc_admin_frontend/components/tabs/General.tsx +++ b/governance/xc_admin/packages/xc_admin_frontend/components/tabs/General.tsx @@ -21,6 +21,8 @@ import { PRICE_FEED_OPS_KEY, getMessageBufferAddressForPrice, getMaximumNumberOfPublishers, + isPriceStorePublisherInitialized, + createDetermisticPriceStoreInitializePublisherInstruction, } from '@pythnetwork/xc-admin-common' import { ClusterContext } from '../../contexts/ClusterContext' import { useMultisigContext } from '../../contexts/MultisigContext' @@ -288,6 +290,8 @@ const General = ({ proposerServerUrl }: { proposerServerUrl: string }) => { const handleSendProposalButtonClick = async () => { if (pythProgramClient && dataChanges && !isMultisigLoading && squads) { const instructions: TransactionInstruction[] = [] + const publisherInitializationsVerified: PublicKey[] = [] + for (const symbol of Object.keys(dataChanges)) { const multisigAuthority = squads.getAuthorityPDA( PRICE_FEED_MULTISIG[getMultisigCluster(cluster)], @@ -296,6 +300,30 @@ const General = ({ proposerServerUrl }: { proposerServerUrl: string }) => { const fundingAccount = isRemote ? mapKey(multisigAuthority) : multisigAuthority + + const initPublisher = async (publisherKey: PublicKey) => { + if ( + publisherInitializationsVerified.every( + (el) => !el.equals(publisherKey) + ) + ) { + if ( + !connection || + !(await isPriceStorePublisherInitialized( + connection, + publisherKey + )) + ) { + instructions.push( + await createDetermisticPriceStoreInitializePublisherInstruction( + fundingAccount, + publisherKey + ) + ) + } + publisherInitializationsVerified.push(publisherKey) + } + } const { prev, new: newChanges } = dataChanges[symbol] // if prev is undefined, it means that the symbol is new if (!prev) { @@ -377,6 +405,7 @@ const General = ({ proposerServerUrl }: { proposerServerUrl: string }) => { }) .instruction() ) + await initPublisher(publisherKey) } // create set min publisher instruction if there are any publishers @@ -545,6 +574,7 @@ const General = ({ proposerServerUrl }: { proposerServerUrl: string }) => { }) .instruction() ) + await initPublisher(publisherKey) } } }