message_buffer.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  1. import * as anchor from "@coral-xyz/anchor";
  2. import { IdlTypes, Program, BorshAccountsCoder } from "@coral-xyz/anchor";
  3. import { MessageBuffer } from "../target/types/message_buffer";
  4. import { MockCpiCaller } from "../target/types/mock_cpi_caller";
  5. import lumina from "@lumina-dev/test";
  6. import { assert } from "chai";
  7. import { AccountMeta, ComputeBudgetProgram } from "@solana/web3.js";
  8. import bs58 from "bs58";
  9. // Enables tool that runs in local browser for easier debugging of
  10. // transactions in this test - https://lumina.fyi/debug
  11. lumina();
  12. const messageBufferProgram = anchor.workspace
  13. .MessageBuffer as Program<MessageBuffer>;
  14. const mockCpiProg = anchor.workspace.MockCpiCaller as Program<MockCpiCaller>;
  15. let whitelistAuthority = anchor.web3.Keypair.generate();
  16. const [mockCpiCallerAuth] = anchor.web3.PublicKey.findProgramAddressSync(
  17. [messageBufferProgram.programId.toBuffer(), Buffer.from("cpi")],
  18. mockCpiProg.programId
  19. );
  20. const [fundPda] = anchor.web3.PublicKey.findProgramAddressSync(
  21. [Buffer.from("fund")],
  22. messageBufferProgram.programId
  23. );
  24. const pythPriceAccountId = new anchor.BN(1);
  25. const addPriceParams = {
  26. id: pythPriceAccountId,
  27. price: new anchor.BN(2),
  28. priceExpo: new anchor.BN(3),
  29. ema: new anchor.BN(4),
  30. emaExpo: new anchor.BN(5),
  31. };
  32. const [pythPriceAccountPk] = anchor.web3.PublicKey.findProgramAddressSync(
  33. [
  34. Buffer.from("pyth"),
  35. Buffer.from("price"),
  36. pythPriceAccountId.toArrayLike(Buffer, "le", 8),
  37. ],
  38. mockCpiProg.programId
  39. );
  40. const MESSAGE = Buffer.from("message");
  41. const [accumulatorPdaKey] = anchor.web3.PublicKey.findProgramAddressSync(
  42. [mockCpiCallerAuth.toBuffer(), MESSAGE, pythPriceAccountPk.toBuffer()],
  43. messageBufferProgram.programId
  44. );
  45. let fundBalance = 100 * anchor.web3.LAMPORTS_PER_SOL;
  46. describe("accumulator_updater", () => {
  47. // Configure the client to use the local cluster.
  48. let provider = anchor.AnchorProvider.env();
  49. anchor.setProvider(provider);
  50. const [whitelistPubkey, whitelistBump] =
  51. anchor.web3.PublicKey.findProgramAddressSync(
  52. [MESSAGE, Buffer.from("whitelist")],
  53. messageBufferProgram.programId
  54. );
  55. before("transfer lamports to the fund", async () => {
  56. await provider.connection.requestAirdrop(fundPda, fundBalance);
  57. });
  58. it("Is initialized!", async () => {
  59. // Add your test here.
  60. const tx = await messageBufferProgram.methods
  61. .initialize(whitelistAuthority.publicKey)
  62. .accounts({})
  63. .rpc();
  64. console.log("Your transaction signature", tx);
  65. const whitelist = await messageBufferProgram.account.whitelist.fetch(
  66. whitelistPubkey
  67. );
  68. assert.strictEqual(whitelist.bump, whitelistBump);
  69. assert.isTrue(whitelist.authority.equals(whitelistAuthority.publicKey));
  70. console.info(`whitelist: ${JSON.stringify(whitelist)}`);
  71. });
  72. it("Sets allowed programs to the whitelist", async () => {
  73. const allowedProgramAuthorities = [mockCpiCallerAuth];
  74. await messageBufferProgram.methods
  75. .setAllowedPrograms(allowedProgramAuthorities)
  76. .accounts({
  77. authority: whitelistAuthority.publicKey,
  78. })
  79. .signers([whitelistAuthority])
  80. .rpc();
  81. const whitelist = await messageBufferProgram.account.whitelist.fetch(
  82. whitelistPubkey
  83. );
  84. console.info(`whitelist after add: ${JSON.stringify(whitelist)}`);
  85. const whitelistAllowedPrograms = whitelist.allowedPrograms.map((pk) =>
  86. pk.toString()
  87. );
  88. assert.deepEqual(
  89. whitelistAllowedPrograms,
  90. allowedProgramAuthorities.map((p) => p.toString())
  91. );
  92. });
  93. it("Updates the whitelist authority", async () => {
  94. const newWhitelistAuthority = anchor.web3.Keypair.generate();
  95. await messageBufferProgram.methods
  96. .updateWhitelistAuthority(newWhitelistAuthority.publicKey)
  97. .accounts({
  98. authority: whitelistAuthority.publicKey,
  99. })
  100. .signers([whitelistAuthority])
  101. .rpc();
  102. const whitelist = await messageBufferProgram.account.whitelist.fetch(
  103. whitelistPubkey
  104. );
  105. assert.isTrue(whitelist.authority.equals(newWhitelistAuthority.publicKey));
  106. whitelistAuthority = newWhitelistAuthority;
  107. });
  108. it("Mock CPI program - AddPrice", async () => {
  109. const mockCpiCallerAddPriceTxPubkeys = await mockCpiProg.methods
  110. .addPrice(addPriceParams)
  111. .accounts({
  112. fund: fundPda,
  113. systemProgram: anchor.web3.SystemProgram.programId,
  114. auth: mockCpiCallerAuth,
  115. accumulatorWhitelist: whitelistPubkey,
  116. messageBufferProgram: messageBufferProgram.programId,
  117. })
  118. .pubkeys();
  119. const accumulatorPdaMetas = [
  120. {
  121. pubkey: accumulatorPdaKey,
  122. isSigner: false,
  123. isWritable: true,
  124. },
  125. ];
  126. const mockCpiCallerAddPriceTxPrep = await mockCpiProg.methods
  127. .addPrice(addPriceParams)
  128. .accounts({
  129. ...mockCpiCallerAddPriceTxPubkeys,
  130. })
  131. .remainingAccounts(accumulatorPdaMetas)
  132. .prepare();
  133. console.log(
  134. `ix: ${JSON.stringify(
  135. mockCpiCallerAddPriceTxPrep.instruction,
  136. (k, v) => {
  137. if (k === "data") {
  138. return v.toString();
  139. } else {
  140. return v;
  141. }
  142. },
  143. 2
  144. )}`
  145. );
  146. for (const prop in mockCpiCallerAddPriceTxPrep.pubkeys) {
  147. console.log(
  148. `${prop}: ${mockCpiCallerAddPriceTxPrep.pubkeys[prop].toString()}`
  149. );
  150. }
  151. const addPriceTx = await mockCpiProg.methods
  152. .addPrice(addPriceParams)
  153. .accounts({
  154. ...mockCpiCallerAddPriceTxPubkeys,
  155. })
  156. .remainingAccounts(accumulatorPdaMetas)
  157. .preInstructions([
  158. ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }),
  159. ])
  160. .rpc({
  161. skipPreflight: true,
  162. });
  163. console.log(`addPriceTx: ${addPriceTx}`);
  164. const pythPriceAccount = await provider.connection.getAccountInfo(
  165. mockCpiCallerAddPriceTxPubkeys.pythPriceAccount
  166. );
  167. const messageBuffer =
  168. await messageBufferProgram.account.messageBuffer.fetch(accumulatorPdaKey);
  169. const accumulatorPriceMessages = parseMessageBuffer(messageBuffer);
  170. console.log(
  171. `accumulatorPriceMessages: ${JSON.stringify(
  172. accumulatorPriceMessages,
  173. null,
  174. 2
  175. )}`
  176. );
  177. accumulatorPriceMessages.forEach((pm) => {
  178. assert.isTrue(pm.id.eq(addPriceParams.id));
  179. assert.isTrue(pm.price.eq(addPriceParams.price));
  180. assert.isTrue(pm.priceExpo.eq(addPriceParams.priceExpo));
  181. });
  182. const fundBalanceAfter = await provider.connection.getBalance(fundPda);
  183. assert.isTrue(fundBalance > fundBalanceAfter);
  184. });
  185. it("Fetches MessageBuffer using getProgramAccounts with discriminator", async () => {
  186. let discriminator =
  187. BorshAccountsCoder.accountDiscriminator("MessageBuffer");
  188. let messageBufferDiscriminator = bs58.encode(discriminator);
  189. // fetch using `getProgramAccounts` and memcmp filter
  190. const messageBufferAccounts = await provider.connection.getProgramAccounts(
  191. messageBufferProgram.programId,
  192. {
  193. filters: [
  194. {
  195. memcmp: {
  196. offset: 0,
  197. bytes: messageBufferDiscriminator,
  198. },
  199. },
  200. ],
  201. }
  202. );
  203. const msgBufferAcctKeys = messageBufferAccounts.map((ai) =>
  204. ai.pubkey.toString()
  205. );
  206. console.log(
  207. `messageBufferAccounts: ${JSON.stringify(msgBufferAcctKeys, null, 2)}`
  208. );
  209. assert.isTrue(messageBufferAccounts.length === 1);
  210. msgBufferAcctKeys.includes(accumulatorPdaKey.toString());
  211. });
  212. it("Mock CPI Program - UpdatePrice", async () => {
  213. const updatePriceParams = {
  214. price: new anchor.BN(5),
  215. priceExpo: new anchor.BN(6),
  216. ema: new anchor.BN(7),
  217. emaExpo: new anchor.BN(8),
  218. };
  219. let accumulatorPdaMeta = getAccumulatorPdaMeta(
  220. mockCpiCallerAuth,
  221. pythPriceAccountPk
  222. );
  223. await mockCpiProg.methods
  224. .updatePrice(updatePriceParams)
  225. .accounts({
  226. fund: fundPda,
  227. pythPriceAccount: pythPriceAccountPk,
  228. auth: mockCpiCallerAuth,
  229. accumulatorWhitelist: whitelistPubkey,
  230. messageBufferProgram: messageBufferProgram.programId,
  231. })
  232. .remainingAccounts([accumulatorPdaMeta])
  233. .preInstructions([
  234. ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }),
  235. ])
  236. .rpc({
  237. skipPreflight: true,
  238. });
  239. const pythPriceAccount = await mockCpiProg.account.priceAccount.fetch(
  240. pythPriceAccountPk
  241. );
  242. assert.isTrue(pythPriceAccount.price.eq(updatePriceParams.price));
  243. assert.isTrue(pythPriceAccount.priceExpo.eq(updatePriceParams.priceExpo));
  244. assert.isTrue(pythPriceAccount.ema.eq(updatePriceParams.ema));
  245. assert.isTrue(pythPriceAccount.emaExpo.eq(updatePriceParams.emaExpo));
  246. const messageBuffer =
  247. await messageBufferProgram.account.messageBuffer.fetch(
  248. accumulatorPdaMeta.pubkey
  249. );
  250. const updatedAccumulatorPriceMessages = parseMessageBuffer(messageBuffer);
  251. console.log(
  252. `updatedAccumulatorPriceMessages: ${JSON.stringify(
  253. updatedAccumulatorPriceMessages,
  254. null,
  255. 2
  256. )}`
  257. );
  258. updatedAccumulatorPriceMessages.forEach((pm) => {
  259. assert.isTrue(pm.id.eq(addPriceParams.id));
  260. assert.isTrue(pm.price.eq(updatePriceParams.price));
  261. assert.isTrue(pm.priceExpo.eq(updatePriceParams.priceExpo));
  262. });
  263. });
  264. it("Mock CPI Program - CPI Max Test", async () => {
  265. // with loosen CPI feature activated, max cpi instruction size len is 10KB
  266. let testCases = [[1024], [1024, 2048], [1024, 2048, 4096]];
  267. // for (let i = 1; i < 8; i++) {
  268. for (let i = 0; i < testCases.length; i++) {
  269. let testCase = testCases[i];
  270. console.info(`testCase: ${testCase}`);
  271. const updatePriceParams = {
  272. price: new anchor.BN(10 * i + 5),
  273. priceExpo: new anchor.BN(10 & (i + 6)),
  274. ema: new anchor.BN(10 * i + 7),
  275. emaExpo: new anchor.BN(10 * i + 8),
  276. };
  277. let accumulatorPdaMeta = getAccumulatorPdaMeta(
  278. mockCpiCallerAuth,
  279. pythPriceAccountPk
  280. );
  281. await mockCpiProg.methods
  282. .cpiMaxTest(updatePriceParams, testCase)
  283. .accounts({
  284. fund: fundPda,
  285. pythPriceAccount: pythPriceAccountPk,
  286. auth: mockCpiCallerAuth,
  287. accumulatorWhitelist: whitelistPubkey,
  288. messageBufferProgram: messageBufferProgram.programId,
  289. })
  290. .remainingAccounts([accumulatorPdaMeta])
  291. .preInstructions([
  292. ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }),
  293. ])
  294. .rpc({
  295. skipPreflight: true,
  296. });
  297. const pythPriceAccount = await mockCpiProg.account.priceAccount.fetch(
  298. pythPriceAccountPk
  299. );
  300. assert.isTrue(pythPriceAccount.price.eq(updatePriceParams.price));
  301. assert.isTrue(pythPriceAccount.priceExpo.eq(updatePriceParams.priceExpo));
  302. assert.isTrue(pythPriceAccount.ema.eq(updatePriceParams.ema));
  303. assert.isTrue(pythPriceAccount.emaExpo.eq(updatePriceParams.emaExpo));
  304. const messageBuffer =
  305. await messageBufferProgram.account.messageBuffer.fetch(
  306. accumulatorPdaMeta.pubkey
  307. );
  308. const updatedAccumulatorPriceMessages = parseMessageBuffer(messageBuffer);
  309. console.log(
  310. `updatedAccumulatorPriceMessages: ${JSON.stringify(
  311. updatedAccumulatorPriceMessages,
  312. null,
  313. 2
  314. )}`
  315. );
  316. updatedAccumulatorPriceMessages.forEach((pm) => {
  317. assert.isTrue(pm.id.eq(addPriceParams.id));
  318. assert.isTrue(pm.price.eq(updatePriceParams.price));
  319. assert.isTrue(pm.priceExpo.eq(updatePriceParams.priceExpo));
  320. });
  321. }
  322. });
  323. it("Mock CPI Program - CPI Max Test Fail", async () => {
  324. // with loosen CPI feature activated, max cpi instruction size len is 10KB
  325. let testCases = [[1024, 2048, 4096, 8192]];
  326. // for (let i = 1; i < 8; i++) {
  327. for (let i = 0; i < testCases.length; i++) {
  328. let testCase = testCases[i];
  329. console.info(`testCase: ${testCase}`);
  330. const updatePriceParams = {
  331. price: new anchor.BN(10 * i + 5),
  332. priceExpo: new anchor.BN(10 & (i + 6)),
  333. ema: new anchor.BN(10 * i + 7),
  334. emaExpo: new anchor.BN(10 * i + 8),
  335. };
  336. let accumulatorPdaMeta = getAccumulatorPdaMeta(
  337. mockCpiCallerAuth,
  338. pythPriceAccountPk
  339. );
  340. let errorThrown = false;
  341. try {
  342. await mockCpiProg.methods
  343. .cpiMaxTest(updatePriceParams, testCase)
  344. .accounts({
  345. fund: fundPda,
  346. pythPriceAccount: pythPriceAccountPk,
  347. auth: mockCpiCallerAuth,
  348. accumulatorWhitelist: whitelistPubkey,
  349. messageBufferProgram: messageBufferProgram.programId,
  350. })
  351. .remainingAccounts([accumulatorPdaMeta])
  352. .preInstructions([
  353. ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }),
  354. ])
  355. .rpc({
  356. skipPreflight: true,
  357. });
  358. } catch (_err) {
  359. errorThrown = true;
  360. }
  361. assert.ok(errorThrown);
  362. }
  363. });
  364. });
  365. export const getAccumulatorPdaMeta = (
  366. cpiCallerAuth: anchor.web3.PublicKey,
  367. pythAccount: anchor.web3.PublicKey
  368. ): AccountMeta => {
  369. const accumulatorPdaKey = anchor.web3.PublicKey.findProgramAddressSync(
  370. [cpiCallerAuth.toBuffer(), MESSAGE, pythAccount.toBuffer()],
  371. messageBufferProgram.programId
  372. )[0];
  373. return {
  374. pubkey: accumulatorPdaKey,
  375. isSigner: false,
  376. isWritable: true,
  377. };
  378. };
  379. type BufferHeader = IdlTypes<MessageBuffer>["BufferHeader"];
  380. // Parses MessageBuffer.data into a PriceAccount or PriceOnly object based on the
  381. // accountType and accountSchema.
  382. function parseMessageBuffer({
  383. header,
  384. messages,
  385. }: {
  386. header: BufferHeader;
  387. messages: number[];
  388. }): AccumulatorPriceMessage[] {
  389. const accumulatorMessages = [];
  390. let dataBuffer = Buffer.from(messages);
  391. let start = 0;
  392. for (let i = 0; i < header.endOffsets.length; i++) {
  393. const endOffset = header.endOffsets[i];
  394. if (endOffset == 0) {
  395. console.log(`endOffset = 0. breaking`);
  396. break;
  397. }
  398. const messageBytes = dataBuffer.subarray(start, endOffset);
  399. const { header: msgHeader, data: msgData } =
  400. parseMessageBytes(messageBytes);
  401. console.info(`header: ${JSON.stringify(msgHeader, null, 2)}`);
  402. if (msgHeader.schema == 0) {
  403. accumulatorMessages.push(parseFullPriceMessage(msgData));
  404. } else if (msgHeader.schema == 1) {
  405. accumulatorMessages.push(parseCompactPriceMessage(msgData));
  406. } else {
  407. console.warn("unknown msgHeader.schema: " + i);
  408. continue;
  409. }
  410. start = endOffset;
  411. }
  412. return accumulatorMessages;
  413. }
  414. type MessageHeader = {
  415. schema: number;
  416. version: number;
  417. size: number;
  418. };
  419. type MessageBuffer = {
  420. header: MessageHeader;
  421. data: Buffer;
  422. };
  423. function parseMessageBytes(data: Buffer): MessageBuffer {
  424. let offset = 0;
  425. const schema = data.readInt8(offset);
  426. offset += 1;
  427. const version = data.readInt16BE(offset);
  428. offset += 2;
  429. const size = data.readUInt32BE(offset);
  430. offset += 4;
  431. const messageHeader = {
  432. schema,
  433. version,
  434. size,
  435. };
  436. let messageData = data.subarray(offset, offset + size);
  437. return {
  438. header: messageHeader,
  439. data: messageData,
  440. };
  441. }
  442. //TODO: follow wormhole sdk parsing structure?
  443. // - https://github.com/wormhole-foundation/wormhole/blob/main/sdk/js/src/vaa/generic.ts
  444. type AccumulatorPriceMessage = FullPriceMessage | CompactPriceMessage;
  445. type FullPriceMessage = {
  446. id: anchor.BN;
  447. price: anchor.BN;
  448. priceExpo: anchor.BN;
  449. ema: anchor.BN;
  450. emaExpo: anchor.BN;
  451. };
  452. function parseFullPriceMessage(data: Uint8Array): FullPriceMessage {
  453. return {
  454. id: new anchor.BN(data.subarray(0, 8), "be"),
  455. price: new anchor.BN(data.subarray(8, 16), "be"),
  456. priceExpo: new anchor.BN(data.subarray(16, 24), "be"),
  457. ema: new anchor.BN(data.subarray(24, 32), "be"),
  458. emaExpo: new anchor.BN(data.subarray(32, 40), "be"),
  459. };
  460. }
  461. type CompactPriceMessage = {
  462. id: anchor.BN;
  463. price: anchor.BN;
  464. priceExpo: anchor.BN;
  465. };
  466. function parseCompactPriceMessage(data: Uint8Array): CompactPriceMessage {
  467. return {
  468. id: new anchor.BN(data.subarray(0, 8), "be"),
  469. price: new anchor.BN(data.subarray(8, 16), "be"),
  470. priceExpo: new anchor.BN(data.subarray(16, 24), "be"),
  471. };
  472. }