소스 검색

[xc-admin] Add message buffer instructions (#869)

* [xc-admin] Add message buffer instructions

* Use coral-xyz/anchor

* Address feedbacks

* Address feedbacks
Ali Behjati 2 년 전
부모
커밋
95ca9d1d92

+ 1 - 0
governance/xc_admin/Dockerfile

@@ -7,6 +7,7 @@ WORKDIR /home/node/
 USER 1000
 
 COPY --chown=1000:1000 governance/xc_admin governance/xc_admin
+COPY --chown=1000:1000 pythnet/message_buffer pythnet/message_buffer
 
 RUN npx lerna run build --scope="{crank_executor,crank_pythnet_relayer,proposer_server}" --include-dependencies
 

+ 1 - 0
governance/xc_admin/packages/xc_admin_common/package.json

@@ -21,6 +21,7 @@
   },
   "dependencies": {
     "@certusone/wormhole-sdk": "^0.9.8",
+    "@coral-xyz/anchor": "^0.26.0",
     "@pythnetwork/client": "^2.17.0",
     "@solana/buffer-layout": "^4.0.1",
     "@solana/web3.js": "^1.73.0",

+ 235 - 0
governance/xc_admin/packages/xc_admin_common/src/__tests__/MessageBufferMultisigInstruction.test.ts

@@ -0,0 +1,235 @@
+import { AnchorProvider, Wallet, Program, Idl } from "@coral-xyz/anchor";
+import {
+  getPythClusterApiUrl,
+  PythCluster,
+} from "@pythnetwork/client/lib/cluster";
+import { Connection, Keypair, PublicKey } from "@solana/web3.js";
+import {
+  MessageBufferMultisigInstruction,
+  MESSAGE_BUFFER_PROGRAM_ID,
+  MultisigInstructionProgram,
+  MultisigParser,
+} from "..";
+import messageBuffer from "message_buffer/idl/message_buffer.json";
+import { MessageBuffer } from "message_buffer/idl/message_buffer";
+
+test("Message buffer multisig instruction parse: create buffer", (done) => {
+  jest.setTimeout(60000);
+
+  const cluster: PythCluster = "pythtest-crosschain";
+
+  const messageBufferProgram = new Program(
+    messageBuffer as Idl,
+    new PublicKey(MESSAGE_BUFFER_PROGRAM_ID),
+    new AnchorProvider(
+      new Connection(getPythClusterApiUrl(cluster)),
+      new Wallet(new Keypair()),
+      AnchorProvider.defaultOptions()
+    )
+  ) as unknown as Program<MessageBuffer>;
+
+  const parser = MultisigParser.fromCluster(cluster);
+
+  const allowedProgramAuth = PublicKey.unique();
+  const baseAccountKey = PublicKey.unique();
+
+  messageBufferProgram.methods
+    .createBuffer(allowedProgramAuth, baseAccountKey, 100)
+    .accounts({
+      admin: PublicKey.unique(),
+      payer: PublicKey.unique(),
+    })
+    .remainingAccounts([
+      {
+        pubkey: PublicKey.unique(),
+        isSigner: false,
+        isWritable: true,
+      },
+    ])
+    .instruction()
+    .then((instruction) => {
+      const parsedInstruction = parser.parseInstruction(instruction);
+
+      if (parsedInstruction instanceof MessageBufferMultisigInstruction) {
+        expect(parsedInstruction.program).toBe(
+          MultisigInstructionProgram.MessageBuffer
+        );
+        expect(parsedInstruction.name).toBe("createBuffer");
+
+        expect(
+          parsedInstruction.accounts.named["whitelist"].pubkey.equals(
+            instruction.keys[0].pubkey
+          )
+        ).toBeTruthy();
+        expect(parsedInstruction.accounts.named["whitelist"].isSigner).toBe(
+          instruction.keys[0].isSigner
+        );
+        expect(parsedInstruction.accounts.named["whitelist"].isWritable).toBe(
+          instruction.keys[0].isWritable
+        );
+
+        expect(
+          parsedInstruction.accounts.named["admin"].pubkey.equals(
+            instruction.keys[1].pubkey
+          )
+        ).toBeTruthy();
+        expect(parsedInstruction.accounts.named["admin"].isSigner).toBe(
+          instruction.keys[1].isSigner
+        );
+        expect(parsedInstruction.accounts.named["admin"].isWritable).toBe(
+          instruction.keys[1].isWritable
+        );
+
+        expect(
+          parsedInstruction.accounts.named["payer"].pubkey.equals(
+            instruction.keys[2].pubkey
+          )
+        ).toBeTruthy();
+        expect(parsedInstruction.accounts.named["payer"].isSigner).toBe(
+          instruction.keys[2].isSigner
+        );
+        expect(parsedInstruction.accounts.named["payer"].isWritable).toBe(
+          instruction.keys[2].isWritable
+        );
+
+        expect(
+          parsedInstruction.accounts.named["systemProgram"].pubkey.equals(
+            instruction.keys[3].pubkey
+          )
+        ).toBeTruthy();
+        expect(parsedInstruction.accounts.named["systemProgram"].isSigner).toBe(
+          instruction.keys[3].isSigner
+        );
+        expect(
+          parsedInstruction.accounts.named["systemProgram"].isWritable
+        ).toBe(instruction.keys[3].isWritable);
+
+        expect(parsedInstruction.accounts.remaining.length).toBe(1);
+
+        expect(
+          parsedInstruction.accounts.remaining[0].pubkey.equals(
+            instruction.keys[4].pubkey
+          )
+        ).toBeTruthy();
+        expect(parsedInstruction.accounts.remaining[0].isSigner).toBe(
+          instruction.keys[4].isSigner
+        );
+        expect(parsedInstruction.accounts.remaining[0].isWritable).toBe(
+          instruction.keys[4].isWritable
+        );
+
+        expect(
+          parsedInstruction.args.allowedProgramAuth.equals(allowedProgramAuth)
+        ).toBeTruthy();
+        expect(
+          parsedInstruction.args.baseAccountKey.equals(baseAccountKey)
+        ).toBeTruthy();
+        expect(parsedInstruction.args.targetSize).toBe(100);
+
+        done();
+      } else {
+        done("Not instance of MessageBufferMultisigInstruction");
+      }
+    });
+});
+
+test("Message buffer multisig instruction parse: delete buffer", (done) => {
+  jest.setTimeout(60000);
+
+  const cluster: PythCluster = "pythtest-crosschain";
+
+  const messageBufferProgram = new Program(
+    messageBuffer as Idl,
+    new PublicKey(MESSAGE_BUFFER_PROGRAM_ID),
+    new AnchorProvider(
+      new Connection(getPythClusterApiUrl(cluster)),
+      new Wallet(new Keypair()),
+      AnchorProvider.defaultOptions()
+    )
+  ) as unknown as Program<MessageBuffer>;
+
+  const parser = MultisigParser.fromCluster(cluster);
+
+  const allowedProgramAuth = PublicKey.unique();
+  const baseAccountKey = PublicKey.unique();
+
+  messageBufferProgram.methods
+    .deleteBuffer(allowedProgramAuth, baseAccountKey)
+    .accounts({
+      admin: PublicKey.unique(),
+      payer: PublicKey.unique(),
+      messageBuffer: PublicKey.unique(),
+    })
+    .instruction()
+    .then((instruction) => {
+      const parsedInstruction = parser.parseInstruction(instruction);
+
+      if (parsedInstruction instanceof MessageBufferMultisigInstruction) {
+        expect(parsedInstruction.program).toBe(
+          MultisigInstructionProgram.MessageBuffer
+        );
+        expect(parsedInstruction.name).toBe("deleteBuffer");
+
+        expect(
+          parsedInstruction.accounts.named["whitelist"].pubkey.equals(
+            instruction.keys[0].pubkey
+          )
+        ).toBeTruthy();
+        expect(parsedInstruction.accounts.named["whitelist"].isSigner).toBe(
+          instruction.keys[0].isSigner
+        );
+        expect(parsedInstruction.accounts.named["whitelist"].isWritable).toBe(
+          instruction.keys[0].isWritable
+        );
+
+        expect(
+          parsedInstruction.accounts.named["admin"].pubkey.equals(
+            instruction.keys[1].pubkey
+          )
+        ).toBeTruthy();
+        expect(parsedInstruction.accounts.named["admin"].isSigner).toBe(
+          instruction.keys[1].isSigner
+        );
+        expect(parsedInstruction.accounts.named["admin"].isWritable).toBe(
+          instruction.keys[1].isWritable
+        );
+
+        expect(
+          parsedInstruction.accounts.named["payer"].pubkey.equals(
+            instruction.keys[2].pubkey
+          )
+        ).toBeTruthy();
+        expect(parsedInstruction.accounts.named["payer"].isSigner).toBe(
+          instruction.keys[2].isSigner
+        );
+        expect(parsedInstruction.accounts.named["payer"].isWritable).toBe(
+          instruction.keys[2].isWritable
+        );
+
+        expect(
+          parsedInstruction.accounts.named["messageBuffer"].pubkey.equals(
+            instruction.keys[3].pubkey
+          )
+        ).toBeTruthy();
+        expect(parsedInstruction.accounts.named["messageBuffer"].isSigner).toBe(
+          instruction.keys[3].isSigner
+        );
+        expect(
+          parsedInstruction.accounts.named["messageBuffer"].isWritable
+        ).toBe(instruction.keys[3].isWritable);
+
+        expect(parsedInstruction.accounts.remaining.length).toBe(0);
+
+        expect(
+          parsedInstruction.args.allowedProgramAuth.equals(allowedProgramAuth)
+        ).toBeTruthy();
+        expect(
+          parsedInstruction.args.baseAccountKey.equals(baseAccountKey)
+        ).toBeTruthy();
+
+        done();
+      } else {
+        done("Not instance of MessageBufferMultisigInstruction");
+      }
+    });
+});

+ 1 - 1
governance/xc_admin/packages/xc_admin_common/src/__tests__/PythMultisigInstruction.test.ts

@@ -1,4 +1,4 @@
-import { AnchorProvider, Wallet } from "@project-serum/anchor";
+import { AnchorProvider, Wallet } from "@coral-xyz/anchor";
 import { pythOracleProgram } from "@pythnetwork/client";
 import {
   getPythClusterApiUrl,

+ 1 - 1
governance/xc_admin/packages/xc_admin_common/src/__tests__/TransactionSize.test.ts

@@ -1,4 +1,4 @@
-import { AnchorProvider, Wallet } from "@project-serum/anchor";
+import { AnchorProvider, Wallet } from "@coral-xyz/anchor";
 import { pythOracleProgram } from "@pythnetwork/client";
 import {
   getPythClusterApiUrl,

+ 1 - 1
governance/xc_admin/packages/xc_admin_common/src/__tests__/WormholeMultisigInstruction.test.ts

@@ -1,5 +1,5 @@
 import { createWormholeProgramInterface } from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole";
-import { AnchorProvider, Wallet } from "@project-serum/anchor";
+import { AnchorProvider, Wallet } from "@coral-xyz/anchor";
 import {
   getPythClusterApiUrl,
   PythCluster,

+ 1 - 0
governance/xc_admin/packages/xc_admin_common/src/index.ts

@@ -8,3 +8,4 @@ export * from "./remote_executor";
 export * from "./bpf_upgradable_loader";
 export * from "./deterministic_oracle_accounts";
 export * from "./cranks";
+export * from "./message_buffer";

+ 41 - 0
governance/xc_admin/packages/xc_admin_common/src/message_buffer.ts

@@ -0,0 +1,41 @@
+import { getPythProgramKeyForCluster, PythCluster } from "@pythnetwork/client";
+import { PublicKey } from "@solana/web3.js";
+
+/**
+ * Address of the message buffer program.
+ */
+export const MESSAGE_BUFFER_PROGRAM_ID: PublicKey = new PublicKey(
+  "7Vbmv1jt4vyuqBZcpYPpnVhrqVe5e6ZPb6JxDcffRHUM"
+);
+
+export const MESSAGE_BUFFER_BUFFER_SIZE = 2048;
+
+export function isMessageBufferAvailable(cluster: PythCluster): boolean {
+  return cluster === "pythtest-crosschain";
+}
+
+export function getPythOracleMessageBufferCpiAuth(
+  cluster: PythCluster
+): PublicKey {
+  const pythOracleProgramId = getPythProgramKeyForCluster(cluster);
+  return PublicKey.findProgramAddressSync(
+    [Buffer.from("upd_price_write"), MESSAGE_BUFFER_PROGRAM_ID.toBuffer()],
+    pythOracleProgramId
+  )[0];
+}
+
+// TODO: We can remove this when createBuffer takes message buffer account
+// as a named account because Anchor can automatically find the address.
+export function getMessageBufferAddressForPrice(
+  cluster: PythCluster,
+  priceAccount: PublicKey
+): PublicKey {
+  return PublicKey.findProgramAddressSync(
+    [
+      getPythOracleMessageBufferCpiAuth(cluster).toBuffer(),
+      Buffer.from("message"),
+      priceAccount.toBuffer(),
+    ],
+    MESSAGE_BUFFER_PROGRAM_ID
+  )[0];
+}

+ 55 - 0
governance/xc_admin/packages/xc_admin_common/src/multisig_transaction/MessageBufferMultisigInstruction.ts

@@ -0,0 +1,55 @@
+import {
+  MultisigInstruction,
+  MultisigInstructionProgram,
+  UNRECOGNIZED_INSTRUCTION,
+} from ".";
+import { AnchorAccounts, resolveAccountNames } from "./anchor";
+import messageBuffer from "message_buffer/idl/message_buffer.json";
+import { TransactionInstruction } from "@solana/web3.js";
+import { Idl, BorshCoder } from "@coral-xyz/anchor";
+
+export class MessageBufferMultisigInstruction implements MultisigInstruction {
+  readonly program = MultisigInstructionProgram.MessageBuffer;
+  readonly name: string;
+  readonly args: { [key: string]: any };
+  readonly accounts: AnchorAccounts;
+
+  constructor(
+    name: string,
+    args: { [key: string]: any },
+    accounts: AnchorAccounts
+  ) {
+    this.name = name;
+    this.args = args;
+    this.accounts = accounts;
+  }
+
+  static fromTransactionInstruction(
+    instruction: TransactionInstruction
+  ): MessageBufferMultisigInstruction {
+    const messageBufferInstructionCoder = new BorshCoder(messageBuffer as Idl)
+      .instruction;
+
+    const deserializedData = messageBufferInstructionCoder.decode(
+      instruction.data
+    );
+
+    if (deserializedData) {
+      return new MessageBufferMultisigInstruction(
+        deserializedData.name,
+        deserializedData.data,
+        resolveAccountNames(
+          messageBuffer as Idl,
+          deserializedData.name,
+          instruction
+        )
+      );
+    } else {
+      return new MessageBufferMultisigInstruction(
+        UNRECOGNIZED_INSTRUCTION,
+        { data: instruction.data },
+        { named: {}, remaining: instruction.keys }
+      );
+    }
+  }
+}

+ 8 - 0
governance/xc_admin/packages/xc_admin_common/src/multisig_transaction/index.ts

@@ -3,7 +3,9 @@ import {
   PythCluster,
 } from "@pythnetwork/client/lib/cluster";
 import { PublicKey, TransactionInstruction } from "@solana/web3.js";
+import { MESSAGE_BUFFER_PROGRAM_ID } from "../message_buffer";
 import { WORMHOLE_ADDRESS } from "../wormhole";
+import { MessageBufferMultisigInstruction } from "./MessageBufferMultisigInstruction";
 import { PythMultisigInstruction } from "./PythMultisigInstruction";
 import { WormholeMultisigInstruction } from "./WormholeMultisigInstruction";
 
@@ -11,6 +13,7 @@ export const UNRECOGNIZED_INSTRUCTION = "unrecognizedInstruction";
 export enum MultisigInstructionProgram {
   PythOracle,
   WormholeBridge,
+  MessageBuffer,
   UnrecognizedProgram,
 }
 
@@ -60,6 +63,10 @@ export class MultisigParser {
       );
     } else if (instruction.programId.equals(this.pythOracleAddress)) {
       return PythMultisigInstruction.fromTransactionInstruction(instruction);
+    } else if (instruction.programId.equals(MESSAGE_BUFFER_PROGRAM_ID)) {
+      return MessageBufferMultisigInstruction.fromTransactionInstruction(
+        instruction
+      );
     } else {
       return UnrecognizedProgram.fromTransactionInstruction(instruction);
     }
@@ -68,3 +75,4 @@ export class MultisigParser {
 
 export { WormholeMultisigInstruction } from "./WormholeMultisigInstruction";
 export { PythMultisigInstruction } from "./PythMultisigInstruction";
+export { MessageBufferMultisigInstruction } from "./MessageBufferMultisigInstruction";

+ 1 - 1
governance/xc_admin/packages/xc_admin_common/src/propose.ts

@@ -9,7 +9,7 @@ import {
   PACKET_DATA_SIZE,
 } from "@solana/web3.js";
 import { BN } from "bn.js";
-import { AnchorProvider } from "@project-serum/anchor";
+import { AnchorProvider } from "@coral-xyz/anchor";
 import {
   createWormholeProgramInterface,
   deriveWormholeBridgeDataKey,

+ 73 - 2
governance/xc_admin/packages/xc_admin_frontend/components/tabs/General.tsx

@@ -1,19 +1,27 @@
-import { AnchorProvider, Program } from '@coral-xyz/anchor'
+import { AnchorProvider, Idl, Program } from '@coral-xyz/anchor'
 import { AccountType, getPythProgramKeyForCluster } from '@pythnetwork/client'
 import { PythOracle, pythOracleProgram } from '@pythnetwork/client/lib/anchor'
 import { useWallet } from '@solana/wallet-adapter-react'
 import { Cluster, PublicKey, TransactionInstruction } from '@solana/web3.js'
+import messageBuffer from 'message_buffer/idl/message_buffer.json'
+import { MessageBuffer } from 'message_buffer/idl/message_buffer'
 import axios from 'axios'
 import { useCallback, useContext, useEffect, useState } from 'react'
 import toast from 'react-hot-toast'
 import {
   findDetermisticAccountAddress,
   getMultisigCluster,
+  getPythOracleMessageBufferCpiAuth,
+  isMessageBufferAvailable,
   isRemoteCluster,
   mapKey,
+  MESSAGE_BUFFER_PROGRAM_ID,
+  MESSAGE_BUFFER_BUFFER_SIZE,
   PRICE_FEED_MULTISIG,
   proposeInstructions,
   WORMHOLE_ADDRESS,
+  PRICE_FEED_OPS_KEY,
+  getMessageBufferAddressForPrice,
 } from 'xc_admin_common'
 import { ClusterContext } from '../../contexts/ClusterContext'
 import { useMultisigContext } from '../../contexts/MultisigContext'
@@ -42,6 +50,9 @@ const General = ({ proposerServerUrl }: { proposerServerUrl: string }) => {
   const [pythProgramClient, setPythProgramClient] =
     useState<Program<PythOracle>>()
 
+  const [messageBufferClient, setMessageBufferClient] =
+    useState<Program<MessageBuffer>>()
+
   const openModal = () => {
     setIsModalOpen(true)
   }
@@ -323,6 +334,33 @@ const General = ({ proposerServerUrl }: { proposerServerUrl: string }) => {
               .instruction()
           )
 
+          if (isMessageBufferAvailable(cluster) && messageBufferClient) {
+            // create create buffer instruction for the price account
+            instructions.push(
+              await messageBufferClient.methods
+                .createBuffer(
+                  getPythOracleMessageBufferCpiAuth(cluster),
+                  priceAccountKey,
+                  MESSAGE_BUFFER_BUFFER_SIZE
+                )
+                .accounts({
+                  admin: fundingAccount,
+                  payer: PRICE_FEED_OPS_KEY,
+                })
+                .remainingAccounts([
+                  {
+                    pubkey: getMessageBufferAddressForPrice(
+                      cluster,
+                      priceAccountKey
+                    ),
+                    isSigner: false,
+                    isWritable: true,
+                  },
+                ])
+                .instruction()
+            )
+          }
+
           // create add publisher instruction if there are any publishers
 
           for (let publisherKey of newChanges.priceAccounts[0].publishers) {
@@ -350,6 +388,8 @@ const General = ({ proposerServerUrl }: { proposerServerUrl: string }) => {
             )
           }
         } else if (!newChanges) {
+          const priceAccount = new PublicKey(prev.priceAccounts[0].address)
+
           // if new is undefined, it means that the symbol is deleted
           // create delete price account instruction
           instructions.push(
@@ -358,10 +398,11 @@ const General = ({ proposerServerUrl }: { proposerServerUrl: string }) => {
               .accounts({
                 fundingAccount,
                 productAccount: new PublicKey(prev.address),
-                priceAccount: new PublicKey(prev.priceAccounts[0].address),
+                priceAccount,
               })
               .instruction()
           )
+
           // create delete product account instruction
           instructions.push(
             await pythProgramClient.methods
@@ -373,6 +414,26 @@ const General = ({ proposerServerUrl }: { proposerServerUrl: string }) => {
               })
               .instruction()
           )
+
+          if (isMessageBufferAvailable(cluster) && messageBufferClient) {
+            // create delete buffer instruction for the price buffer
+            instructions.push(
+              await messageBufferClient.methods
+                .deleteBuffer(
+                  getPythOracleMessageBufferCpiAuth(cluster),
+                  priceAccount
+                )
+                .accounts({
+                  admin: fundingAccount,
+                  payer: PRICE_FEED_OPS_KEY,
+                  messageBuffer: getMessageBufferAddressForPrice(
+                    cluster,
+                    priceAccount
+                  ),
+                })
+                .instruction()
+            )
+          }
         } else {
           // check if metadata has changed
           if (
@@ -741,6 +802,16 @@ const General = ({ proposerServerUrl }: { proposerServerUrl: string }) => {
       setPythProgramClient(
         pythOracleProgram(getPythProgramKeyForCluster(cluster), provider)
       )
+
+      if (isMessageBufferAvailable(cluster)) {
+        setMessageBufferClient(
+          new Program(
+            messageBuffer as Idl,
+            new PublicKey(MESSAGE_BUFFER_PROGRAM_ID),
+            provider
+          ) as unknown as Program<MessageBuffer>
+        )
+      }
     }
   }, [connection, connected, cluster, proposeSquads])
 

+ 47 - 6
governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals.tsx

@@ -17,9 +17,11 @@ import {
   getMultisigCluster,
   getProposals,
   MultisigInstruction,
+  MultisigInstructionProgram,
   MultisigParser,
   PRICE_FEED_MULTISIG,
   PythMultisigInstruction,
+  MessageBufferMultisigInstruction,
   UnrecognizedProgram,
   WormholeMultisigInstruction,
 } from 'xc_admin_common'
@@ -788,9 +790,12 @@ const Proposal = ({
                                 {parsedInstruction instanceof
                                 PythMultisigInstruction
                                   ? 'Pyth Oracle'
-                                  : innerInstruction instanceof
+                                  : parsedInstruction instanceof
                                     WormholeMultisigInstruction
                                   ? 'Wormhole'
+                                  : parsedInstruction instanceof
+                                    MessageBufferMultisigInstruction
+                                  ? 'Message Buffer'
                                   : 'Unknown'}
                               </div>
                             </div>
@@ -803,7 +808,9 @@ const Proposal = ({
                                 {parsedInstruction instanceof
                                   PythMultisigInstruction ||
                                 parsedInstruction instanceof
-                                  WormholeMultisigInstruction
+                                  WormholeMultisigInstruction ||
+                                parsedInstruction instanceof
+                                  MessageBufferMultisigInstruction
                                   ? parsedInstruction.name
                                   : 'Unknown'}
                               </div>
@@ -816,7 +823,9 @@ const Proposal = ({
                               {parsedInstruction instanceof
                                 PythMultisigInstruction ||
                               parsedInstruction instanceof
-                                WormholeMultisigInstruction ? (
+                                WormholeMultisigInstruction ||
+                              parsedInstruction instanceof
+                                MessageBufferMultisigInstruction ? (
                                 Object.keys(parsedInstruction.args).length >
                                 0 ? (
                                   <div className="col-span-4 mt-2 bg-[#444157] p-4 lg:col-span-3 lg:mt-0">
@@ -906,7 +915,9 @@ const Proposal = ({
                             {parsedInstruction instanceof
                               PythMultisigInstruction ||
                             parsedInstruction instanceof
-                              WormholeMultisigInstruction ? (
+                              WormholeMultisigInstruction ||
+                            parsedInstruction instanceof
+                              MessageBufferMultisigInstruction ? (
                               <div
                                 key={`${index}_accounts`}
                                 className="grid grid-cols-4 justify-between"
@@ -983,9 +994,36 @@ const Proposal = ({
                                         ) : null}
                                       </>
                                     ))}
+                                    {parsedInstruction.accounts.remaining.map(
+                                      (accountMeta, index) => (
+                                        <>
+                                          <div
+                                            key="rem-{index}"
+                                            className="flex justify-between border-t border-beige-300 py-3"
+                                          >
+                                            <div className="max-w-[80px] break-words sm:max-w-none sm:break-normal">
+                                              Remaining {index + 1}
+                                            </div>
+                                            <div className="space-y-2 sm:flex sm:space-y-0 sm:space-x-2">
+                                              <div className="flex items-center space-x-2 sm:ml-2">
+                                                {accountMeta.isSigner ? (
+                                                  <SignerTag />
+                                                ) : null}
+                                                {accountMeta.isWritable ? (
+                                                  <WritableTag />
+                                                ) : null}
+                                              </div>
+                                              <CopyPubkey
+                                                pubkey={accountMeta.pubkey.toBase58()}
+                                              />
+                                            </div>
+                                          </div>
+                                        </>
+                                      )
+                                    )}
                                   </div>
                                 ) : (
-                                  <div>No arguments</div>
+                                  <div>No accounts</div>
                                 )}
                               </div>
                             ) : parsedInstruction instanceof
@@ -1125,7 +1163,10 @@ const Proposals = ({
                       keys: remoteIx.keys as AccountMeta[],
                     })
                   return (
-                    parsedRemoteInstruction instanceof PythMultisigInstruction
+                    parsedRemoteInstruction instanceof
+                      PythMultisigInstruction ||
+                    parsedRemoteInstruction instanceof
+                      MessageBufferMultisigInstruction
                   )
                 }) &&
                 ix.governanceAction.targetChainId === 'pythnet')

+ 2 - 1
governance/xc_admin/packages/xc_admin_frontend/package.json

@@ -35,7 +35,8 @@
     "react-hot-toast": "^2.4.0",
     "typescript": "4.9.4",
     "use-debounce": "^9.0.2",
-    "xc_admin_common": "*"
+    "xc_admin_common": "*",
+    "message_buffer": "*"
   },
   "devDependencies": {
     "@svgr/webpack": "^6.3.1",

+ 4 - 0
package-lock.json

@@ -1333,6 +1333,7 @@
       "license": "ISC",
       "dependencies": {
         "@certusone/wormhole-sdk": "^0.9.8",
+        "@coral-xyz/anchor": "^0.26.0",
         "@pythnetwork/client": "^2.17.0",
         "@solana/buffer-layout": "^4.0.1",
         "@solana/web3.js": "^1.73.0",
@@ -1421,6 +1422,7 @@
         "axios": "^1.4.0",
         "copy-to-clipboard": "^3.3.3",
         "gsap": "^3.11.4",
+        "message_buffer": "*",
         "next": "12.2.5",
         "next-seo": "^5.15.0",
         "react": "18.2.0",
@@ -106543,6 +106545,7 @@
       "version": "file:governance/xc_admin/packages/xc_admin_common",
       "requires": {
         "@certusone/wormhole-sdk": "^0.9.8",
+        "@coral-xyz/anchor": "^0.26.0",
         "@pythnetwork/client": "^2.17.0",
         "@solana/buffer-layout": "^4.0.1",
         "@solana/web3.js": "^1.73.0",
@@ -106626,6 +106629,7 @@
         "eslint": "8.22.0",
         "eslint-config-next": "12.2.5",
         "gsap": "^3.11.4",
+        "message_buffer": "*",
         "next": "12.2.5",
         "next-seo": "^5.15.0",
         "postcss": "^8.4.16",