import * as anchor from "@coral-xyz/anchor"; import { Program, Idl } from "@coral-xyz/anchor"; import { MessageBuffer } from "../target/types/message_buffer"; import messageBuffer from "../target/idl/message_buffer.json"; import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet"; import { assert } from "chai"; import { Connection, PublicKey } from "@solana/web3.js"; import { getPythClusterApiUrl, getPythProgramKeyForCluster, PythCluster, parseBaseData, AccountType, parseProductData, } from "@pythnetwork/client"; import path from "path"; import dotenv from "dotenv"; import fs from "fs"; type PythClusterOrIntegration = PythCluster | "integration"; /** * Script to initialize the message buffer program and whitelist admin * using the integration repo setup * * run using the following command: * `NODE_ENV= yarn ts-node scripts/setup_message_buffer.ts` */ const MESSAGE = Buffer.from("message"); function getPythClusterEndpoint(cluster: PythClusterOrIntegration): string { if (cluster === "integration") { return "http://pythnet:8899"; } return getPythClusterApiUrl(cluster); } function getPythPidForCluster( cluster: PythClusterOrIntegration ): anchor.web3.PublicKey { if (cluster === "integration") { return new anchor.web3.PublicKey( "7th6GdMuo4u1zNLzFAyMY6psunHNsGjPjo8hXvcTgKei" ); } else { return getPythProgramKeyForCluster(cluster); } } const getKeypairFromFile = (keypairPath: string): anchor.web3.Keypair => { const keypairBuffer = fs.readFileSync(keypairPath); return anchor.web3.Keypair.fromSecretKey( Uint8Array.from(JSON.parse(keypairBuffer.toString())) ); }; function getPythOracleCpiAuth( messageBufferProgramId: anchor.web3.PublicKey, pythOracleProgramId: anchor.web3.PublicKey ): anchor.web3.PublicKey { return anchor.web3.PublicKey.findProgramAddressSync( [Buffer.from("upd_price_write"), messageBufferProgramId.toBuffer()], pythOracleProgramId )[0]; } function getMessageBufferPubkey( pythOracleCpiAuth: anchor.web3.PublicKey, pythPriceAccountPk: anchor.web3.PublicKey, messageBufferProgramId: anchor.web3.PublicKey ): anchor.web3.PublicKey { return anchor.web3.PublicKey.findProgramAddressSync( [pythOracleCpiAuth.toBuffer(), MESSAGE, pythPriceAccountPk.toBuffer()], messageBufferProgramId )[0]; } export async function getPriceAccountPubkeys( connection: anchor.web3.Connection, pythPublicKey: anchor.web3.PublicKey ): Promise { const accountList = await connection.getProgramAccounts( pythPublicKey, connection.commitment ); console.info( `fetched ${ accountList.length } programAccounts for pythProgram: ${pythPublicKey.toString()}` ); const priceAccountIds: PublicKey[] = []; accountList.forEach((singleAccount) => { const base = parseBaseData(singleAccount.account.data); if (base) { switch (base.type) { case AccountType.Product: const productData = parseProductData(singleAccount.account.data); priceAccountIds.push(productData.priceAccountKey); break; default: break; } } }); return priceAccountIds; } async function main() { let canAirdrop = false; switch (process.env.NODE_ENV) { case "local": dotenv.config({ path: path.join(__dirname, ".env.local") }); canAirdrop = true; break; case "integration": dotenv.config({ path: path.join(__dirname, ".env.integration") }); canAirdrop = true; break; case "pythtest": dotenv.config({ path: path.join(__dirname, ".env.pythtest") }); break; case "pythnet": dotenv.config({ path: path.join(__dirname, ".env.pythnet") }); break; default: console.error(`Invalid NODE_ENV: ${process.env.NODE_ENV}`); process.exit(1); } const cluster = process.env.CLUSTER as PythClusterOrIntegration; const messageBufferPid = new anchor.web3.PublicKey( process.env.MESSAGE_BUFFER_PROGRAM_ID ); const pythOraclePid = getPythPidForCluster(cluster); const payer = getKeypairFromFile( path.resolve(process.env.PAYER_KEYPAIR_PATH) ); const endpoint = getPythClusterEndpoint(cluster); const initialSize = parseInt(process.env.INITIAL_SIZE || "", 10); let whitelistAdmin = payer; console.info(` messageBufferPid: ${messageBufferPid.toString()} pythOraclePid: ${pythOraclePid.toString()} payer: ${payer.publicKey.toString()} endpoint: ${endpoint} whitelistAdmin: ${whitelistAdmin.publicKey.toString()} initialSize: ${initialSize} `); console.log(`connecting to ${endpoint}`); const connection = new anchor.web3.Connection(endpoint); const commitment = "finalized"; const provider = new anchor.AnchorProvider( connection, new NodeWallet(payer), { commitment, preflightCommitment: commitment, skipPreflight: true, } ); anchor.setProvider(provider); const messageBufferProgram = new Program( messageBuffer as Idl, messageBufferPid, provider ) as unknown as Program; const [whitelistPubkey, whitelistBump] = anchor.web3.PublicKey.findProgramAddressSync( [MESSAGE, Buffer.from("whitelist")], messageBufferProgram.programId ); const pythOracleCpiAuth = getPythOracleCpiAuth( messageBufferProgram.programId, pythOraclePid ); if (canAirdrop) { console.group("Requesting airdrop"); let airdropSig = await provider.connection.requestAirdrop( payer.publicKey, 1 * anchor.web3.LAMPORTS_PER_SOL ); await provider.connection.confirmTransaction({ signature: airdropSig, ...(await provider.connection.getLatestBlockhash()), }); const payerBalance = await provider.connection.getBalance(payer.publicKey); console.log(`payerBalance: ${payerBalance}`); console.log("Airdrop complete"); console.groupEnd(); } else { console.log("Skipping airdrop for non-local/integration environments"); } console.log("Initializing message buffer whitelist admin..."); let whitelist = await messageBufferProgram.account.whitelist.fetchNullable( whitelistPubkey ); if (whitelist === null) { console.group( "No whitelist detected. Initializing message buffer whitelist & admin" ); const initializeTxnSig = await messageBufferProgram.methods .initialize() .accounts({ admin: whitelistAdmin.publicKey, payer: payer.publicKey, }) .signers([whitelistAdmin, payer]) .rpc(); console.log(`initializeTxnSig: ${initializeTxnSig}`); console.log("fetching & checking whitelist"); whitelist = await messageBufferProgram.account.whitelist.fetch( whitelistPubkey ); assert.strictEqual(whitelist.bump, whitelistBump); assert.isTrue(whitelist.admin.equals(whitelistAdmin.publicKey)); console.groupEnd(); } else { console.log("Whitelist already initialized"); } if (whitelist.allowedPrograms.length === 0) { console.group("Setting Allowed Programs"); const allowedProgramAuthorities = [pythOracleCpiAuth]; let setAllowedProgramSig = await messageBufferProgram.methods .setAllowedPrograms(allowedProgramAuthorities) .accounts({ admin: whitelistAdmin.publicKey, }) .signers([whitelistAdmin]) .rpc(); console.log(`setAllowedProgramSig: ${setAllowedProgramSig}`); console.log("fetching & checking whitelist after add"); whitelist = await messageBufferProgram.account.whitelist.fetch( whitelistPubkey ); console.info(`whitelist after add: ${JSON.stringify(whitelist)}`); const whitelistAllowedPrograms = whitelist.allowedPrograms.map((pk) => pk.toString() ); assert.deepEqual( whitelistAllowedPrograms, allowedProgramAuthorities.map((p) => p.toString()) ); console.groupEnd(); } else { console.log("Allowed Programs already set"); } let priceIds = await getPriceAccountPubkeys(connection, pythOraclePid); console.info(`fetched ${priceIds.length} priceAccountIds`); let errorAccounts = []; let alreadyInitializedAccounts = []; let newlyInitializedAccounts = []; const messageBufferKeys = priceIds.map((priceId) => { return { messageBuffer: getMessageBufferPubkey( pythOracleCpiAuth, priceId, messageBufferPid ), priceAccount: priceId, }; }); let accounts = await messageBufferProgram.account.messageBuffer.fetchMultiple( messageBufferKeys.map((k) => k.messageBuffer) ); const msgBufferKeysAndData = messageBufferKeys.map((k, i) => { return { ...k, messageBufferData: accounts[i], }; }); alreadyInitializedAccounts = msgBufferKeysAndData .filter((idAndAccount) => { return idAndAccount.messageBufferData !== null; }) .map((v) => { return { priceAccount: v.priceAccount.toString(), messageBuffer: v.messageBuffer.toString(), }; }); console.log(` ${alreadyInitializedAccounts.length} message buffer accounts already initialized`); console.table(alreadyInitializedAccounts); const priceAccountPubkeysForNewlyInitializedMessageBuffers = msgBufferKeysAndData.filter((idAndAccount) => { return idAndAccount.messageBufferData === null; }); if (priceAccountPubkeysForNewlyInitializedMessageBuffers.length === 0) { console.info(`no new message buffers to initialize`); } // TODO: optimize with batching await Promise.all( priceAccountPubkeysForNewlyInitializedMessageBuffers.map( async (idAndAccount) => { const priceId = idAndAccount.priceAccount; const messageBufferPda = idAndAccount.messageBuffer; const msgBufferPdaMetas = [ { pubkey: messageBufferPda, isSigner: false, isWritable: true, }, ]; try { await messageBufferProgram.methods .createBuffer(pythOracleCpiAuth, priceId, initialSize) .accounts({ whitelist: whitelistPubkey, admin: whitelistAdmin.publicKey, systemProgram: anchor.web3.SystemProgram.programId, }) .signers([whitelistAdmin]) .remainingAccounts(msgBufferPdaMetas) .rpc({ skipPreflight: true }); newlyInitializedAccounts.push({ priceId: priceId.toString(), messageBuffer: messageBufferPda.toString(), }); } catch (e) { console.error( "Error creating message buffer for price account: ", priceId.toString() ); console.error(e); errorAccounts.push({ priceId: priceId.toString(), messageBuffer: messageBufferPda.toString(), }); } } ) ); if (errorAccounts.length !== 0) { console.error( `Ran into errors when initializing ${errorAccounts.length} accounts` ); console.info(`Accounts with errors: ${JSON.stringify(errorAccounts)}`); } console.log(`Initialized ${newlyInitializedAccounts.length} accounts`); console.table(newlyInitializedAccounts); // Update whitelist admin at the end otherwise all the message buffer PDAs // will have to be initialized by the whitelist admin (which could be the multisig) if (process.env.WHITELIST_ADMIN) { whitelist = await messageBufferProgram.account.whitelist.fetchNullable( whitelistPubkey ); let newWhitelistAdmin = new anchor.web3.PublicKey( process.env.WHITELIST_ADMIN ); if (!whitelist.admin.equals(newWhitelistAdmin)) { console.info( `updating whitelist admin from ${whitelist.admin.toString()} to ${newWhitelistAdmin.toString()}` ); try { await messageBufferProgram.methods .updateWhitelistAdmin(newWhitelistAdmin) .accounts({ admin: whitelistAdmin.publicKey, }) .signers([whitelistAdmin]) .rpc(); } catch (e) { console.error(`Error when attempting to update the admin: ${e}`); } } else { console.info( `whitelist admin is already ${newWhitelistAdmin.toString()}` ); } } } void main();