setup_message_buffer.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. import * as anchor from "@coral-xyz/anchor";
  2. import { Program, Idl } from "@coral-xyz/anchor";
  3. import { MessageBuffer } from "../target/types/message_buffer";
  4. import messageBuffer from "../target/idl/message_buffer.json";
  5. import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet";
  6. import { assert } from "chai";
  7. import { Connection, PublicKey } from "@solana/web3.js";
  8. import {
  9. getPythClusterApiUrl,
  10. getPythProgramKeyForCluster,
  11. PythCluster,
  12. parseBaseData,
  13. AccountType,
  14. parseProductData,
  15. } from "@pythnetwork/client";
  16. import path from "path";
  17. import dotenv from "dotenv";
  18. import fs from "fs";
  19. type PythClusterOrIntegration = PythCluster | "integration";
  20. /**
  21. * Script to initialize the message buffer program and whitelist admin
  22. * using the integration repo setup
  23. *
  24. * run using the following command:
  25. * `NODE_ENV=<env> yarn ts-node scripts/setup_message_buffer.ts`
  26. */
  27. const MESSAGE = Buffer.from("message");
  28. function getPythClusterEndpoint(cluster: PythClusterOrIntegration): string {
  29. if (cluster === "integration") {
  30. return "http://pythnet:8899";
  31. }
  32. return getPythClusterApiUrl(cluster);
  33. }
  34. function getPythPidForCluster(
  35. cluster: PythClusterOrIntegration,
  36. ): anchor.web3.PublicKey {
  37. if (cluster === "integration") {
  38. return new anchor.web3.PublicKey(
  39. "7th6GdMuo4u1zNLzFAyMY6psunHNsGjPjo8hXvcTgKei",
  40. );
  41. } else {
  42. return getPythProgramKeyForCluster(cluster);
  43. }
  44. }
  45. const getKeypairFromFile = (keypairPath: string): anchor.web3.Keypair => {
  46. const keypairBuffer = fs.readFileSync(keypairPath);
  47. return anchor.web3.Keypair.fromSecretKey(
  48. Uint8Array.from(JSON.parse(keypairBuffer.toString())),
  49. );
  50. };
  51. function getPythOracleCpiAuth(
  52. messageBufferProgramId: anchor.web3.PublicKey,
  53. pythOracleProgramId: anchor.web3.PublicKey,
  54. ): anchor.web3.PublicKey {
  55. return anchor.web3.PublicKey.findProgramAddressSync(
  56. [Buffer.from("upd_price_write"), messageBufferProgramId.toBuffer()],
  57. pythOracleProgramId,
  58. )[0];
  59. }
  60. function getMessageBufferPubkey(
  61. pythOracleCpiAuth: anchor.web3.PublicKey,
  62. pythPriceAccountPk: anchor.web3.PublicKey,
  63. messageBufferProgramId: anchor.web3.PublicKey,
  64. ): anchor.web3.PublicKey {
  65. return anchor.web3.PublicKey.findProgramAddressSync(
  66. [pythOracleCpiAuth.toBuffer(), MESSAGE, pythPriceAccountPk.toBuffer()],
  67. messageBufferProgramId,
  68. )[0];
  69. }
  70. export async function getPriceAccountPubkeys(
  71. connection: anchor.web3.Connection,
  72. pythPublicKey: anchor.web3.PublicKey,
  73. ): Promise<PublicKey[]> {
  74. const accountList = await connection.getProgramAccounts(
  75. pythPublicKey,
  76. connection.commitment,
  77. );
  78. console.info(
  79. `fetched ${
  80. accountList.length
  81. } programAccounts for pythProgram: ${pythPublicKey.toString()}`,
  82. );
  83. const priceAccountIds: PublicKey[] = [];
  84. accountList.forEach((singleAccount) => {
  85. const base = parseBaseData(singleAccount.account.data);
  86. if (base) {
  87. switch (base.type) {
  88. case AccountType.Product:
  89. const productData = parseProductData(singleAccount.account.data);
  90. priceAccountIds.push(productData.priceAccountKey);
  91. break;
  92. default:
  93. break;
  94. }
  95. }
  96. });
  97. return priceAccountIds;
  98. }
  99. async function main() {
  100. let canAirdrop = false;
  101. switch (process.env.NODE_ENV) {
  102. case "local":
  103. dotenv.config({ path: path.join(__dirname, ".env.local") });
  104. canAirdrop = true;
  105. break;
  106. case "integration":
  107. dotenv.config({ path: path.join(__dirname, ".env.integration") });
  108. canAirdrop = true;
  109. break;
  110. case "pythtest":
  111. dotenv.config({ path: path.join(__dirname, ".env.pythtest") });
  112. break;
  113. case "pythnet":
  114. dotenv.config({ path: path.join(__dirname, ".env.pythnet") });
  115. break;
  116. default:
  117. console.error(`Invalid NODE_ENV: ${process.env.NODE_ENV}`);
  118. process.exit(1);
  119. }
  120. const cluster = process.env.CLUSTER as PythClusterOrIntegration;
  121. const messageBufferPid = new anchor.web3.PublicKey(
  122. process.env.MESSAGE_BUFFER_PROGRAM_ID,
  123. );
  124. const pythOraclePid = getPythPidForCluster(cluster);
  125. const payer = getKeypairFromFile(
  126. path.resolve(process.env.PAYER_KEYPAIR_PATH),
  127. );
  128. const endpoint = getPythClusterEndpoint(cluster);
  129. const initialSize = parseInt(process.env.INITIAL_SIZE || "", 10);
  130. let whitelistAdmin = payer;
  131. console.info(`
  132. messageBufferPid: ${messageBufferPid.toString()}
  133. pythOraclePid: ${pythOraclePid.toString()}
  134. payer: ${payer.publicKey.toString()}
  135. endpoint: ${endpoint}
  136. whitelistAdmin: ${whitelistAdmin.publicKey.toString()}
  137. initialSize: ${initialSize}
  138. `);
  139. console.log(`connecting to ${endpoint}`);
  140. const connection = new anchor.web3.Connection(endpoint);
  141. const commitment = "finalized";
  142. const provider = new anchor.AnchorProvider(
  143. connection,
  144. new NodeWallet(payer),
  145. {
  146. commitment,
  147. preflightCommitment: commitment,
  148. skipPreflight: true,
  149. },
  150. );
  151. anchor.setProvider(provider);
  152. const messageBufferProgram = new Program(
  153. messageBuffer as Idl,
  154. messageBufferPid,
  155. provider,
  156. ) as unknown as Program<MessageBuffer>;
  157. const [whitelistPubkey, whitelistBump] =
  158. anchor.web3.PublicKey.findProgramAddressSync(
  159. [MESSAGE, Buffer.from("whitelist")],
  160. messageBufferProgram.programId,
  161. );
  162. const pythOracleCpiAuth = getPythOracleCpiAuth(
  163. messageBufferProgram.programId,
  164. pythOraclePid,
  165. );
  166. if (canAirdrop) {
  167. console.group("Requesting airdrop");
  168. let airdropSig = await provider.connection.requestAirdrop(
  169. payer.publicKey,
  170. 1 * anchor.web3.LAMPORTS_PER_SOL,
  171. );
  172. await provider.connection.confirmTransaction({
  173. signature: airdropSig,
  174. ...(await provider.connection.getLatestBlockhash()),
  175. });
  176. const payerBalance = await provider.connection.getBalance(payer.publicKey);
  177. console.log(`payerBalance: ${payerBalance}`);
  178. console.log("Airdrop complete");
  179. console.groupEnd();
  180. } else {
  181. console.log("Skipping airdrop for non-local/integration environments");
  182. }
  183. console.log("Initializing message buffer whitelist admin...");
  184. let whitelist =
  185. await messageBufferProgram.account.whitelist.fetchNullable(whitelistPubkey);
  186. if (whitelist === null) {
  187. console.group(
  188. "No whitelist detected. Initializing message buffer whitelist & admin",
  189. );
  190. const initializeTxnSig = await messageBufferProgram.methods
  191. .initialize()
  192. .accounts({
  193. admin: whitelistAdmin.publicKey,
  194. payer: payer.publicKey,
  195. })
  196. .signers([whitelistAdmin, payer])
  197. .rpc();
  198. console.log(`initializeTxnSig: ${initializeTxnSig}`);
  199. console.log("fetching & checking whitelist");
  200. whitelist =
  201. await messageBufferProgram.account.whitelist.fetch(whitelistPubkey);
  202. assert.strictEqual(whitelist.bump, whitelistBump);
  203. assert.isTrue(whitelist.admin.equals(whitelistAdmin.publicKey));
  204. console.groupEnd();
  205. } else {
  206. console.log("Whitelist already initialized");
  207. }
  208. if (whitelist.allowedPrograms.length === 0) {
  209. console.group("Setting Allowed Programs");
  210. const allowedProgramAuthorities = [pythOracleCpiAuth];
  211. let setAllowedProgramSig = await messageBufferProgram.methods
  212. .setAllowedPrograms(allowedProgramAuthorities)
  213. .accounts({
  214. admin: whitelistAdmin.publicKey,
  215. })
  216. .signers([whitelistAdmin])
  217. .rpc();
  218. console.log(`setAllowedProgramSig: ${setAllowedProgramSig}`);
  219. console.log("fetching & checking whitelist after add");
  220. whitelist =
  221. await messageBufferProgram.account.whitelist.fetch(whitelistPubkey);
  222. console.info(`whitelist after add: ${JSON.stringify(whitelist)}`);
  223. const whitelistAllowedPrograms = whitelist.allowedPrograms.map((pk) =>
  224. pk.toString(),
  225. );
  226. assert.deepEqual(
  227. whitelistAllowedPrograms,
  228. allowedProgramAuthorities.map((p) => p.toString()),
  229. );
  230. console.groupEnd();
  231. } else {
  232. console.log("Allowed Programs already set");
  233. }
  234. let priceIds = await getPriceAccountPubkeys(connection, pythOraclePid);
  235. console.info(`fetched ${priceIds.length} priceAccountIds`);
  236. let errorAccounts = [];
  237. let alreadyInitializedAccounts = [];
  238. let newlyInitializedAccounts = [];
  239. const messageBufferKeys = priceIds.map((priceId) => {
  240. return {
  241. messageBuffer: getMessageBufferPubkey(
  242. pythOracleCpiAuth,
  243. priceId,
  244. messageBufferPid,
  245. ),
  246. priceAccount: priceId,
  247. };
  248. });
  249. let accounts = await messageBufferProgram.account.messageBuffer.fetchMultiple(
  250. messageBufferKeys.map((k) => k.messageBuffer),
  251. );
  252. const msgBufferKeysAndData = messageBufferKeys.map((k, i) => {
  253. return {
  254. ...k,
  255. messageBufferData: accounts[i],
  256. };
  257. });
  258. alreadyInitializedAccounts = msgBufferKeysAndData
  259. .filter((idAndAccount) => {
  260. return idAndAccount.messageBufferData !== null;
  261. })
  262. .map((v) => {
  263. return {
  264. priceAccount: v.priceAccount.toString(),
  265. messageBuffer: v.messageBuffer.toString(),
  266. };
  267. });
  268. console.log(`
  269. ${alreadyInitializedAccounts.length} message buffer accounts already initialized`);
  270. console.table(alreadyInitializedAccounts);
  271. const priceAccountPubkeysForNewlyInitializedMessageBuffers =
  272. msgBufferKeysAndData.filter((idAndAccount) => {
  273. return idAndAccount.messageBufferData === null;
  274. });
  275. if (priceAccountPubkeysForNewlyInitializedMessageBuffers.length === 0) {
  276. console.info(`no new message buffers to initialize`);
  277. }
  278. // TODO: optimize with batching
  279. await Promise.all(
  280. priceAccountPubkeysForNewlyInitializedMessageBuffers.map(
  281. async (idAndAccount) => {
  282. const priceId = idAndAccount.priceAccount;
  283. const messageBufferPda = idAndAccount.messageBuffer;
  284. const msgBufferPdaMetas = [
  285. {
  286. pubkey: messageBufferPda,
  287. isSigner: false,
  288. isWritable: true,
  289. },
  290. ];
  291. try {
  292. await messageBufferProgram.methods
  293. .createBuffer(pythOracleCpiAuth, priceId, initialSize)
  294. .accounts({
  295. whitelist: whitelistPubkey,
  296. admin: whitelistAdmin.publicKey,
  297. systemProgram: anchor.web3.SystemProgram.programId,
  298. })
  299. .signers([whitelistAdmin])
  300. .remainingAccounts(msgBufferPdaMetas)
  301. .rpc({ skipPreflight: true });
  302. newlyInitializedAccounts.push({
  303. priceId: priceId.toString(),
  304. messageBuffer: messageBufferPda.toString(),
  305. });
  306. } catch (e) {
  307. console.error(
  308. "Error creating message buffer for price account: ",
  309. priceId.toString(),
  310. );
  311. console.error(e);
  312. errorAccounts.push({
  313. priceId: priceId.toString(),
  314. messageBuffer: messageBufferPda.toString(),
  315. });
  316. }
  317. },
  318. ),
  319. );
  320. if (errorAccounts.length !== 0) {
  321. console.error(
  322. `Ran into errors when initializing ${errorAccounts.length} accounts`,
  323. );
  324. console.info(`Accounts with errors: ${JSON.stringify(errorAccounts)}`);
  325. }
  326. console.log(`Initialized ${newlyInitializedAccounts.length} accounts`);
  327. console.table(newlyInitializedAccounts);
  328. // Update whitelist admin at the end otherwise all the message buffer PDAs
  329. // will have to be initialized by the whitelist admin (which could be the multisig)
  330. if (process.env.WHITELIST_ADMIN) {
  331. whitelist =
  332. await messageBufferProgram.account.whitelist.fetchNullable(
  333. whitelistPubkey,
  334. );
  335. let newWhitelistAdmin = new anchor.web3.PublicKey(
  336. process.env.WHITELIST_ADMIN,
  337. );
  338. if (!whitelist.admin.equals(newWhitelistAdmin)) {
  339. console.info(
  340. `updating whitelist admin from ${whitelist.admin.toString()} to ${newWhitelistAdmin.toString()}`,
  341. );
  342. try {
  343. await messageBufferProgram.methods
  344. .updateWhitelistAdmin(newWhitelistAdmin)
  345. .accounts({
  346. admin: whitelistAdmin.publicKey,
  347. })
  348. .signers([whitelistAdmin])
  349. .rpc();
  350. } catch (e) {
  351. console.error(`Error when attempting to update the admin: ${e}`);
  352. }
  353. } else {
  354. console.info(
  355. `whitelist admin is already ${newWhitelistAdmin.toString()}`,
  356. );
  357. }
  358. }
  359. }
  360. void main();