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

* [xc-admin] Add message buffer instructions

* Use coral-xyz/anchor

* Address feedbacks

* Address feedbacks
This commit is contained in:
Ali Behjati 2023-06-09 19:56:40 +02:00 committed by GitHub
parent 183081cc20
commit 95ca9d1d92
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 472 additions and 13 deletions

View File

@ -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

View File

@ -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",

View File

@ -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");
}
});
});

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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";

View File

@ -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];
}

View File

@ -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 }
);
}
}
}

View File

@ -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";

View File

@ -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,

View File

@ -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])

View File

@ -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')

View File

@ -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
package-lock.json generated
View File

@ -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",