From aba0390495e029b365e31fcd8cfe3e9f8d59122a Mon Sep 17 00:00:00 2001 From: guibescos <59208140+guibescos@users.noreply.github.com> Date: Wed, 15 Feb 2023 12:13:51 +0000 Subject: [PATCH] Refactor (#599) --- .../packages/crank_executor/src/index.ts | 80 ++++--------- .../crank_pythnet_relayer/src/index.ts | 113 ++++++------------ .../packages/xc_admin_common/src/cranks.ts | 7 ++ .../src/deterministic_oracle_accounts.ts | 97 +++++++++++++++ .../packages/xc_admin_common/src/index.ts | 2 + .../xc_admin_common/src/remote_executor.ts | 14 ++- .../components/tabs/General.tsx | 27 +++-- 7 files changed, 192 insertions(+), 148 deletions(-) create mode 100644 governance/xc_admin/packages/xc_admin_common/src/cranks.ts create mode 100644 governance/xc_admin/packages/xc_admin_common/src/deterministic_oracle_accounts.ts diff --git a/governance/xc_admin/packages/crank_executor/src/index.ts b/governance/xc_admin/packages/crank_executor/src/index.ts index b9edb300..5a43776b 100644 --- a/governance/xc_admin/packages/crank_executor/src/index.ts +++ b/governance/xc_admin/packages/crank_executor/src/index.ts @@ -11,6 +11,8 @@ import SquadsMesh, { DEFAULT_MULTISIG_PROGRAM_ID, getIxPDA } from "@sqds/mesh"; import * as fs from "fs"; import NodeWallet from "@project-serum/anchor/dist/cjs/nodewallet"; import { + envOrErr, + getCreateAccountWithSeedInstruction, getProposals, MultisigParser, PythMultisigInstruction, @@ -20,44 +22,29 @@ import BN from "bn.js"; import { AnchorProvider } from "@project-serum/anchor"; import { getPythClusterApiUrl, - getPythProgramKeyForCluster, PythCluster, } from "@pythnetwork/client/lib/cluster"; import { deriveFeeCollectorKey, getWormholeBridgeData, } from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole"; -import { parseProductData } from "@pythnetwork/client"; +import { AccountType, parseProductData } from "@pythnetwork/client"; -export function envOrErr(env: string): string { - const val = process.env[env]; - if (!val) { - throw new Error(`environment variable "${env}" must be set`); - } - return String(process.env[env]); -} - -const PRODUCT_ACCOUNT_SIZE = 512; -const PRICE_ACCOUNT_SIZE = 3312; - -const CLUSTER: string = envOrErr("CLUSTER"); -const COMMITMENT: Commitment = - (process.env.COMMITMENT as Commitment) ?? "confirmed"; +const CLUSTER: PythCluster = envOrErr("CLUSTER") as PythCluster; const VAULT: PublicKey = new PublicKey(envOrErr("VAULT")); const KEYPAIR: Keypair = Keypair.fromSecretKey( Uint8Array.from(JSON.parse(fs.readFileSync(envOrErr("WALLET"), "ascii"))) ); +const COMMITMENT: Commitment = + (process.env.COMMITMENT as Commitment) ?? "confirmed"; async function run() { const squad = new SquadsMesh({ - connection: new Connection( - getPythClusterApiUrl(CLUSTER as PythCluster), - COMMITMENT - ), + connection: new Connection(getPythClusterApiUrl(CLUSTER), COMMITMENT), wallet: new NodeWallet(KEYPAIR), multisigProgramId: DEFAULT_MULTISIG_PROGRAM_ID, }); - const multisigParser = MultisigParser.fromCluster(CLUSTER as PythCluster); + const multisigParser = MultisigParser.fromCluster(CLUSTER); const wormholeFee = multisigParser.wormholeBridgeAddress ? ( @@ -111,25 +98,14 @@ async function run() { parsedInstruction.name == "addProduct" ) { /// Add product, fetch the symbol from the instruction - const productSeed = "product:" + parsedInstruction.args.symbol; - const productAddress = await PublicKey.createWithSeed( - squad.wallet.publicKey, - productSeed, - getPythProgramKeyForCluster(CLUSTER as PythCluster) - ); transaction.add( - SystemProgram.createAccountWithSeed({ - fromPubkey: squad.wallet.publicKey, - basePubkey: squad.wallet.publicKey, - newAccountPubkey: productAddress, - seed: productSeed, - space: PRODUCT_ACCOUNT_SIZE, - lamports: - await squad.connection.getMinimumBalanceForRentExemption( - PRODUCT_ACCOUNT_SIZE - ), - programId: getPythProgramKeyForCluster(CLUSTER as PythCluster), - }) + await getCreateAccountWithSeedInstruction( + squad.connection, + CLUSTER, + squad.wallet.publicKey, + parsedInstruction.args.symbol, + AccountType.Product + ) ); } else if ( parsedInstruction instanceof PythMultisigInstruction && @@ -140,26 +116,14 @@ async function run() { parsedInstruction.accounts.named.productAccount.pubkey ); if (productAccount) { - const priceSeed = - "price:" + parseProductData(productAccount.data).product.symbol; - const priceAddress = await PublicKey.createWithSeed( - squad.wallet.publicKey, - priceSeed, - getPythProgramKeyForCluster(CLUSTER as PythCluster) - ); transaction.add( - SystemProgram.createAccountWithSeed({ - fromPubkey: squad.wallet.publicKey, - basePubkey: squad.wallet.publicKey, - newAccountPubkey: priceAddress, - seed: priceSeed, - space: PRICE_ACCOUNT_SIZE, - lamports: - await squad.connection.getMinimumBalanceForRentExemption( - PRICE_ACCOUNT_SIZE - ), - programId: getPythProgramKeyForCluster(CLUSTER as PythCluster), - }) + await getCreateAccountWithSeedInstruction( + squad.connection, + CLUSTER, + squad.wallet.publicKey, + parseProductData(productAccount.data).product.symbol, + AccountType.Price + ) ); } else { throw Error("Product account not found"); diff --git a/governance/xc_admin/packages/crank_pythnet_relayer/src/index.ts b/governance/xc_admin/packages/crank_pythnet_relayer/src/index.ts index fdeea277..8b3c3c45 100644 --- a/governance/xc_admin/packages/crank_pythnet_relayer/src/index.ts +++ b/governance/xc_admin/packages/crank_pythnet_relayer/src/index.ts @@ -1,15 +1,11 @@ -import { ParsedVaa, parseVaa, postVaaSolana } from "@certusone/wormhole-sdk"; +import { parseVaa, postVaaSolana } from "@certusone/wormhole-sdk"; import { signTransactionFactory } from "@certusone/wormhole-sdk/lib/cjs/solana"; -import { - derivePostedVaaKey, - getPostedVaa, -} from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole"; +import { derivePostedVaaKey } from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole"; import { AnchorProvider, BN, Program } from "@coral-xyz/anchor"; import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet"; -import { parseProductData } from "@pythnetwork/client"; +import { AccountType, parseProductData } from "@pythnetwork/client"; import { getPythClusterApiUrl, - getPythProgramKeyForCluster, PythCluster, } from "@pythnetwork/client/lib/cluster"; import { @@ -18,43 +14,31 @@ import { Connection, Keypair, PublicKey, - SystemProgram, TransactionInstruction, } from "@solana/web3.js"; import * as fs from "fs"; import { decodeGovernancePayload, ExecutePostedVaa, + getCreateAccountWithSeedInstruction, MultisigParser, PythMultisigInstruction, WORMHOLE_ADDRESS, WORMHOLE_API_ENDPOINT, + CLAIM_RECORD_SEED, + mapKey, + REMOTE_EXECUTOR_ADDRESS, + envOrErr, } from "xc_admin_common"; -export function envOrErr(env: string): string { - const val = process.env[env]; - if (!val) { - throw new Error(`environment variable "${env}" must be set`); - } - return String(process.env[env]); -} - -const REMOTE_EXECUTOR_ADDRESS = new PublicKey( - "exe6S3AxPVNmy46L4Nj6HrnnAVQUhwyYzMSNcnRn3qq" -); - -const PRODUCT_ACCOUNT_SIZE = 512; -const PRICE_ACCOUNT_SIZE = 3312; -const CLAIM_RECORD_SEED = "CLAIM_RECORD"; -const EXECUTOR_KEY_SEED = "EXECUTOR_KEY"; const CLUSTER: PythCluster = envOrErr("CLUSTER") as PythCluster; -const COMMITMENT: Commitment = - (process.env.COMMITMENT as Commitment) ?? "confirmed"; -const OFFSET: number = Number(process.env.OFFSET ?? "-1"); const EMITTER: PublicKey = new PublicKey(envOrErr("EMITTER")); const KEYPAIR: Keypair = Keypair.fromSecretKey( Uint8Array.from(JSON.parse(fs.readFileSync(envOrErr("WALLET"), "ascii"))) ); +const OFFSET: number = Number(process.env.OFFSET ?? "-1"); +const COMMITMENT: Commitment = + (process.env.COMMITMENT as Commitment) ?? "confirmed"; async function run() { const provider = new AnchorProvider( @@ -65,6 +49,7 @@ async function run() { preflightCommitment: COMMITMENT, } ); + const multisigParser = MultisigParser.fromCluster(CLUSTER); const remoteExecutor = await Program.at(REMOTE_EXECUTOR_ADDRESS, provider); @@ -72,10 +57,7 @@ async function run() { [Buffer.from(CLAIM_RECORD_SEED), EMITTER.toBuffer()], remoteExecutor.programId )[0]; - const executorKey: PublicKey = PublicKey.findProgramAddressSync( - [Buffer.from(EXECUTOR_KEY_SEED), EMITTER.toBuffer()], - remoteExecutor.programId - )[0]; + const executorKey: PublicKey = mapKey(EMITTER); const claimRecord = await remoteExecutor.account.claimRecord.fetchNullable( claimRecordAddress ); @@ -105,7 +87,6 @@ async function run() { governancePayload instanceof ExecutePostedVaa && governancePayload.targetChainId == "pythnet" ) { - const multisigParser = MultisigParser.fromCluster(CLUSTER); const preInstructions: TransactionInstruction[] = []; console.log(`Found VAA ${lastSequenceNumber}, relaying ...`); @@ -139,25 +120,14 @@ async function run() { parsedInstruction instanceof PythMultisigInstruction && parsedInstruction.name == "addProduct" ) { - const productSeed = "product:" + parsedInstruction.args.symbol; - const productAddress = await PublicKey.createWithSeed( - provider.wallet.publicKey, - productSeed, - getPythProgramKeyForCluster(CLUSTER as PythCluster) - ); preInstructions.push( - SystemProgram.createAccountWithSeed({ - fromPubkey: provider.wallet.publicKey, - basePubkey: provider.wallet.publicKey, - newAccountPubkey: productAddress, - seed: productSeed, - space: PRODUCT_ACCOUNT_SIZE, - lamports: - await provider.connection.getMinimumBalanceForRentExemption( - PRODUCT_ACCOUNT_SIZE - ), - programId: getPythProgramKeyForCluster(CLUSTER as PythCluster), - }) + await getCreateAccountWithSeedInstruction( + provider.connection, + CLUSTER, + provider.wallet.publicKey, + parsedInstruction.args.symbol, + AccountType.Product + ) ); } else if ( parsedInstruction instanceof PythMultisigInstruction && @@ -167,28 +137,14 @@ async function run() { parsedInstruction.accounts.named.productAccount.pubkey ); if (productAccount) { - const priceSeed = - "price:" + parseProductData(productAccount.data).product.symbol; - const priceAddress = await PublicKey.createWithSeed( - provider.wallet.publicKey, - priceSeed, - getPythProgramKeyForCluster(CLUSTER as PythCluster) - ); preInstructions.push( - SystemProgram.createAccountWithSeed({ - fromPubkey: provider.wallet.publicKey, - basePubkey: provider.wallet.publicKey, - newAccountPubkey: priceAddress, - seed: priceSeed, - space: PRICE_ACCOUNT_SIZE, - lamports: - await provider.connection.getMinimumBalanceForRentExemption( - PRICE_ACCOUNT_SIZE - ), - programId: getPythProgramKeyForCluster( - CLUSTER as PythCluster - ), - }) + await getCreateAccountWithSeedInstruction( + provider.connection, + CLUSTER, + provider.wallet.publicKey, + parseProductData(productAccount.data).product.symbol, + AccountType.Price + ) ); } else { throw Error("Product account not found"); @@ -204,16 +160,14 @@ async function run() { }) .remainingAccounts(extraAccountMetas) .preInstructions(preInstructions) - .rpc(); + .rpc({ skipPreflight: true }); } } else if (response.code == 5) { - console.log(`Wormhole API failure`); - console.log( - `${wormholeApi}/v1/signed_vaa/1/${EMITTER.toBuffer().toString( + throw new Error( + `Wormhole API failure :${wormholeApi}/v1/signed_vaa/1/${EMITTER.toBuffer().toString( "hex" )}/${lastSequenceNumber}` ); - break; } else { throw new Error("Could not connect to wormhole api"); } @@ -221,5 +175,10 @@ async function run() { } (async () => { - await run(); + try { + await run(); + } catch (err) { + console.error(err); + throw new Error(); + } })(); diff --git a/governance/xc_admin/packages/xc_admin_common/src/cranks.ts b/governance/xc_admin/packages/xc_admin_common/src/cranks.ts new file mode 100644 index 00000000..bb4efddb --- /dev/null +++ b/governance/xc_admin/packages/xc_admin_common/src/cranks.ts @@ -0,0 +1,7 @@ +export function envOrErr(env: string): string { + const val = process.env[env]; + if (!val) { + throw new Error(`environment variable "${env}" must be set`); + } + return String(process.env[env]); +} diff --git a/governance/xc_admin/packages/xc_admin_common/src/deterministic_oracle_accounts.ts b/governance/xc_admin/packages/xc_admin_common/src/deterministic_oracle_accounts.ts new file mode 100644 index 00000000..c3e8cdb7 --- /dev/null +++ b/governance/xc_admin/packages/xc_admin_common/src/deterministic_oracle_accounts.ts @@ -0,0 +1,97 @@ +import { + AccountType, + getPythProgramKeyForCluster, + PythCluster, +} from "@pythnetwork/client"; +import { + Connection, + PublicKey, + SystemProgram, + TransactionInstruction, +} from "@solana/web3.js"; +import { OPS_KEY } from "./multisig"; + +/** + * Get seed for deterministic creation of a price/product account + * @param type Type of the account + * @param symbol Symbol of the price feed + * @returns + */ +function getSeed(accountType: AccountType, symbol: string): string { + switch (accountType) { + case AccountType.Price: + return "price:" + symbol; + case AccountType.Product: + return "product:" + symbol; + default: + throw new Error("Unimplemented"); + } +} + +/** + * Get required account size for a given oracle account type + * @param accountType Type of the account + * @returns + */ +function getAccountTypeSize(accountType: AccountType): number { + switch (accountType) { + case AccountType.Price: + return 3312; + case AccountType.Product: + return 512; + default: + throw new Error("Unimplemented"); + } +} + +/** + * Get the address and the seed of a deterministic price/product account + * @param type Type of the account + * @param symbol Symbol of the price feed + * @param cluster Cluster in which to create the deterministic account + * @returns + */ +export async function findDetermisticAccountAddress( + type: AccountType, + symbol: string, + cluster: PythCluster +): Promise<[PublicKey, string]> { + const seed: string = getSeed(type, symbol); + const address: PublicKey = await PublicKey.createWithSeed( + OPS_KEY, + seed, + getPythProgramKeyForCluster(cluster) + ); + return [address, seed]; +} + +/** + * Get instruction to create a determistic price/product account + * @param connection Connection used to compute rent, should be connected to `cluster` + * @param cluster Cluster in which to create the determistic account + * @param base Base for the determistic derivation + * @param symbol Symbol of the price feed + * @param accountType Type of the account + * @returns + */ +export async function getCreateAccountWithSeedInstruction( + connection: Connection, + cluster: PythCluster, + base: PublicKey, + symbol: string, + accountType: AccountType +): Promise { + const [address, seed]: [PublicKey, string] = + await findDetermisticAccountAddress(accountType, symbol, cluster); + return SystemProgram.createAccountWithSeed({ + fromPubkey: base, + basePubkey: base, + newAccountPubkey: address, + seed: seed, + space: getAccountTypeSize(accountType), + lamports: await connection.getMinimumBalanceForRentExemption( + getAccountTypeSize(accountType) + ), + programId: getPythProgramKeyForCluster(cluster), + }); +} 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 8c660ad1..d928593b 100644 --- a/governance/xc_admin/packages/xc_admin_common/src/index.ts +++ b/governance/xc_admin/packages/xc_admin_common/src/index.ts @@ -6,3 +6,5 @@ export * from "./multisig_transaction"; export * from "./cluster"; export * from "./remote_executor"; export * from "./bpf_upgradable_loader"; +export * from "./deterministic_oracle_accounts"; +export * from "./cranks"; diff --git a/governance/xc_admin/packages/xc_admin_common/src/remote_executor.ts b/governance/xc_admin/packages/xc_admin_common/src/remote_executor.ts index e5b4c00e..c9e87cef 100644 --- a/governance/xc_admin/packages/xc_admin_common/src/remote_executor.ts +++ b/governance/xc_admin/packages/xc_admin_common/src/remote_executor.ts @@ -1,9 +1,19 @@ import { PublicKey } from "@solana/web3.js"; +/** + * Seed for the claim PDA of the remote executor + */ +export const CLAIM_RECORD_SEED: string = "CLAIM_RECORD"; + +/** + * Seed for the executor PDA of the remote executor + */ +const EXECUTOR_KEY_SEED: string = "EXECUTOR_KEY"; + /** * Address of the remote executor (same on all networks) */ -export const REMOTE_EXECUTOR_ADDRESS = new PublicKey( +export const REMOTE_EXECUTOR_ADDRESS: PublicKey = new PublicKey( "exe6S3AxPVNmy46L4Nj6HrnnAVQUhwyYzMSNcnRn3qq" ); @@ -14,7 +24,7 @@ export const REMOTE_EXECUTOR_ADDRESS = new PublicKey( */ export function mapKey(key: PublicKey): PublicKey { return PublicKey.findProgramAddressSync( - [Buffer.from("EXECUTOR_KEY"), key.toBytes()], + [Buffer.from(EXECUTOR_KEY_SEED), key.toBytes()], REMOTE_EXECUTOR_ADDRESS )[0]; } 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 ccd7e1e6..927e1714 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 @@ -1,5 +1,5 @@ import { AnchorProvider, Program, Wallet } from '@coral-xyz/anchor' -import { getPythProgramKeyForCluster } from '@pythnetwork/client' +import { AccountType, getPythProgramKeyForCluster } from '@pythnetwork/client' import { PythOracle, pythOracleProgram } from '@pythnetwork/client/lib/anchor' import { useAnchorWallet, useWallet } from '@solana/wallet-adapter-react' import { WalletModalButton } from '@solana/wallet-adapter-react-ui' @@ -7,6 +7,7 @@ import { Cluster, PublicKey, TransactionInstruction } from '@solana/web3.js' import { useCallback, useContext, useEffect, useState } from 'react' import toast from 'react-hot-toast' import { + findDetermisticAccountAddress, getMultisigCluster, isRemoteCluster, mapKey, @@ -258,11 +259,13 @@ const General = () => { // if prev is undefined, it means that the symbol is new if (!prev) { // deterministically generate product account key - const productAccountKey = await PublicKey.createWithSeed( - OPS_KEY, - 'product:' + symbol, - pythProgramClient.programId - ) + const productAccountKey: PublicKey = ( + await findDetermisticAccountAddress( + AccountType.Product, + symbol, + cluster + ) + )[0] // create add product account instruction instructions.push( await pythProgramClient.methods @@ -276,11 +279,13 @@ const General = () => { ) // deterministically generate price account key - const priceAccountKey = await PublicKey.createWithSeed( - OPS_KEY, - 'price:' + symbol, - pythProgramClient.programId - ) + const priceAccountKey: PublicKey = ( + await findDetermisticAccountAddress( + AccountType.Price, + symbol, + cluster + ) + )[0] // create add price account instruction instructions.push( await pythProgramClient.methods