[xc-admin] Refactor wormhole messages as classes (#478)

* Refactor wormhole messages as classes

* Type error

* Export ExecutePostedVaa

* Rename to UnknownGovernanceAction

* Improve interface
This commit is contained in:
guibescos 2023-01-12 11:47:56 -06:00 committed by GitHub
parent fedb92e446
commit 8ef49e6128
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 136 additions and 151 deletions

View File

@ -1,17 +1,6 @@
import { ChainName } from "@certusone/wormhole-sdk"; import { ChainName } from "@certusone/wormhole-sdk";
import { import { PACKET_DATA_SIZE, PublicKey, SystemProgram } from "@solana/web3.js";
PACKET_DATA_SIZE, import { ActionName, decodeHeader, encodeHeader, ExecutePostedVaa } from "..";
PublicKey,
SystemProgram,
TransactionInstruction,
} from "@solana/web3.js";
import {
ActionName,
decodeExecutePostedVaa,
decodeHeader,
encodeHeader,
} from "..";
import { encodeExecutePostedVaa } from "../governance_payload/ExecutePostedVaa";
test("GovernancePayload ser/de", (done) => { test("GovernancePayload ser/de", (done) => {
jest.setTimeout(60000); jest.setTimeout(60000);
@ -75,32 +64,25 @@ test("GovernancePayload ser/de", (done) => {
).toThrow("Invalid header, action doesn't match module"); ).toThrow("Invalid header, action doesn't match module");
// Decode executePostVaa with empty instructions // Decode executePostVaa with empty instructions
let expectedExecuteVaaArgs = { let expectedExecutePostedVaa = new ExecutePostedVaa("pythnet", []);
targetChainId: "pythnet" as ChainName, buffer = expectedExecutePostedVaa.encode();
instructions: [] as TransactionInstruction[],
};
buffer = encodeExecutePostedVaa(expectedExecuteVaaArgs);
expect( expect(
buffer.equals(Buffer.from([80, 84, 71, 77, 0, 0, 0, 26, 0, 0, 0, 0])) buffer.equals(Buffer.from([80, 84, 71, 77, 0, 0, 0, 26, 0, 0, 0, 0]))
).toBeTruthy(); ).toBeTruthy();
let executePostedVaaArgs = decodeExecutePostedVaa(buffer); let executePostedVaaArgs = ExecutePostedVaa.decode(buffer);
expect(executePostedVaaArgs?.targetChainId).toBe("pythnet"); expect(executePostedVaaArgs?.targetChainId).toBe("pythnet");
expect(executePostedVaaArgs?.instructions.length).toBe(0); expect(executePostedVaaArgs?.instructions.length).toBe(0);
// Decode executePostVaa with one system instruction // Decode executePostVaa with one system instruction
expectedExecuteVaaArgs = { expectedExecutePostedVaa = new ExecutePostedVaa("pythnet", [
targetChainId: "pythnet" as ChainName,
instructions: [
SystemProgram.transfer({ SystemProgram.transfer({
fromPubkey: new PublicKey( fromPubkey: new PublicKey("AWQ18oKzd187aM2oMB4YirBcdgX1FgWfukmqEX91BRES"),
"AWQ18oKzd187aM2oMB4YirBcdgX1FgWfukmqEX91BRES"
),
toPubkey: new PublicKey("J25GT2knN8V2Wvg9jNrYBuj9SZdsLnU6bK7WCGrL7daj"), toPubkey: new PublicKey("J25GT2knN8V2Wvg9jNrYBuj9SZdsLnU6bK7WCGrL7daj"),
lamports: 890880, lamports: 890880,
}), }),
] as TransactionInstruction[], ]);
};
buffer = encodeExecutePostedVaa(expectedExecuteVaaArgs); buffer = expectedExecutePostedVaa.encode();
expect( expect(
buffer.equals( buffer.equals(
Buffer.from([ Buffer.from([
@ -115,7 +97,7 @@ test("GovernancePayload ser/de", (done) => {
]) ])
) )
).toBeTruthy(); ).toBeTruthy();
executePostedVaaArgs = decodeExecutePostedVaa(buffer); executePostedVaaArgs = ExecutePostedVaa.decode(buffer);
expect(executePostedVaaArgs?.targetChainId).toBe("pythnet"); expect(executePostedVaaArgs?.targetChainId).toBe("pythnet");
expect(executePostedVaaArgs?.instructions.length).toBe(1); expect(executePostedVaaArgs?.instructions.length).toBe(1);
expect( expect(

View File

@ -1,4 +1,3 @@
import { ChainName } from "@certusone/wormhole-sdk";
import { createWormholeProgramInterface } from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole"; import { createWormholeProgramInterface } from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole";
import { AnchorProvider, Wallet } from "@project-serum/anchor"; import { AnchorProvider, Wallet } from "@project-serum/anchor";
import { import {
@ -16,11 +15,8 @@ import {
MultisigInstructionProgram, MultisigInstructionProgram,
MultisigParser, MultisigParser,
WORMHOLE_ADDRESS, WORMHOLE_ADDRESS,
ExecutePostedVaa,
} from ".."; } from "..";
import {
encodeExecutePostedVaa,
ExecutePostedVaaArgs,
} from "../governance_payload/ExecutePostedVaa";
import { WormholeMultisigInstruction } from "../multisig_transaction/WormholeMultisigInstruction"; import { WormholeMultisigInstruction } from "../multisig_transaction/WormholeMultisigInstruction";
test("Wormhole multisig instruction parse: send message without governance payload", (done) => { test("Wormhole multisig instruction parse: send message without governance payload", (done) => {
@ -184,19 +180,16 @@ test("Wormhole multisig instruction parse: send message with governance payload"
); );
const parser = MultisigParser.fromCluster(cluster); const parser = MultisigParser.fromCluster(cluster);
const executePostedVaaArgs: ExecutePostedVaaArgs = { const executePostedVaa: ExecutePostedVaa = new ExecutePostedVaa("pythnet", [
targetChainId: "pythnet" as ChainName,
instructions: [
SystemProgram.transfer({ SystemProgram.transfer({
fromPubkey: PublicKey.unique(), fromPubkey: PublicKey.unique(),
toPubkey: PublicKey.unique(), toPubkey: PublicKey.unique(),
lamports: 890880, lamports: 890880,
}), }),
], ]);
};
wormholeProgram.methods wormholeProgram.methods
.postMessage(0, encodeExecutePostedVaa(executePostedVaaArgs), 0) .postMessage(0, executePostedVaa.encode(), 0)
.accounts({ .accounts({
bridge: PublicKey.unique(), bridge: PublicKey.unique(),
message: PublicKey.unique(), message: PublicKey.unique(),
@ -319,45 +312,47 @@ test("Wormhole multisig instruction parse: send message with governance payload"
expect(parsedInstruction.args.nonce).toBe(0); expect(parsedInstruction.args.nonce).toBe(0);
expect( expect(
parsedInstruction.args.payload.equals( parsedInstruction.args.payload.equals(executePostedVaa.encode())
encodeExecutePostedVaa(executePostedVaaArgs)
)
); );
expect(parsedInstruction.args.consistencyLevel).toBe(0); expect(parsedInstruction.args.consistencyLevel).toBe(0);
expect(parsedInstruction.args.governanceName).toBe("ExecutePostedVaa"); if (
parsedInstruction.args.governanceAction instanceof ExecutePostedVaa
expect(parsedInstruction.args.governanceArgs.targetChainId).toBe( ) {
expect(parsedInstruction.args.governanceAction.targetChainId).toBe(
"pythnet" "pythnet"
); );
( (
parsedInstruction.args.governanceArgs parsedInstruction.args.governanceAction
.instructions as TransactionInstruction[] .instructions as TransactionInstruction[]
).forEach((instruction, i) => { ).forEach((instruction, i) => {
expect( expect(
instruction.programId.equals( instruction.programId.equals(
executePostedVaaArgs.instructions[i].programId executePostedVaa.instructions[i].programId
) )
); );
expect( expect(
instruction.data.equals(executePostedVaaArgs.instructions[i].data) instruction.data.equals(executePostedVaa.instructions[i].data)
); );
instruction.keys.forEach((account, j) => { instruction.keys.forEach((account, j) => {
expect( expect(
account.pubkey.equals( account.pubkey.equals(
executePostedVaaArgs.instructions[i].keys[j].pubkey executePostedVaa.instructions[i].keys[j].pubkey
) )
).toBeTruthy(); ).toBeTruthy();
expect(account.isSigner).toBe( expect(account.isSigner).toBe(
executePostedVaaArgs.instructions[i].keys[j].isSigner executePostedVaa.instructions[i].keys[j].isSigner
); );
expect(account.isWritable).toBe( expect(account.isWritable).toBe(
executePostedVaaArgs.instructions[i].keys[j].isWritable executePostedVaa.instructions[i].keys[j].isWritable
); );
}); });
}); });
done(); done();
} else {
done("Not instance of ExecutePostedVaa");
}
} else { } else {
done("Not instance of WormholeInstruction"); done("Not instance of WormholeInstruction");
} }

View File

@ -3,7 +3,7 @@ import * as BufferLayout from "@solana/buffer-layout";
import { import {
encodeHeader, encodeHeader,
governanceHeaderLayout, governanceHeaderLayout,
PythGovernanceHeader, PythGovernanceAction,
verifyHeader, verifyHeader,
} from "."; } from ".";
import { Layout } from "@solana/buffer-layout"; import { Layout } from "@solana/buffer-layout";
@ -77,13 +77,20 @@ export const executePostedVaaLayout: BufferLayout.Structure<
new Vector<InstructionData>(instructionDataLayout, "instructions"), new Vector<InstructionData>(instructionDataLayout, "instructions"),
]); ]);
export type ExecutePostedVaaArgs = { export class ExecutePostedVaa implements PythGovernanceAction {
targetChainId: ChainName; readonly targetChainId: ChainName;
instructions: TransactionInstruction[]; readonly instructions: TransactionInstruction[];
};
/** Decode ExecutePostedVaaArgs and return undefined if it failed */ constructor(
export function decodeExecutePostedVaa(data: Buffer): ExecutePostedVaaArgs { targetChainId: ChainName,
instructions: TransactionInstruction[]
) {
this.targetChainId = targetChainId;
this.instructions = instructions;
}
/** Decode ExecutePostedVaaArgs */
static decode(data: Buffer): ExecutePostedVaa {
let deserialized = executePostedVaaLayout.decode(data); let deserialized = executePostedVaaLayout.decode(data);
let header = verifyHeader(deserialized.header); let header = verifyHeader(deserialized.header);
@ -102,19 +109,18 @@ export function decodeExecutePostedVaa(data: Buffer): ExecutePostedVaaArgs {
return { programId, keys, data }; return { programId, keys, data };
} }
); );
return new ExecutePostedVaa(header.targetChainId, instructions);
}
return { targetChainId: header.targetChainId, instructions }; /** Encode ExecutePostedVaaArgs */
} encode(): Buffer {
// PACKET_DATA_SIZE is the maximum transaction size of Solana, so our serialized payload will never be bigger than that
/** Encode ExecutePostedVaaArgs */
export function encodeExecutePostedVaa(src: ExecutePostedVaaArgs): Buffer {
// PACKET_DATA_SIZE is the maximum transactin size of Solana, so our serialized payload will never be bigger than that
const buffer = Buffer.alloc(PACKET_DATA_SIZE); const buffer = Buffer.alloc(PACKET_DATA_SIZE);
const offset = encodeHeader( const offset = encodeHeader(
{ action: "ExecutePostedVaa", targetChainId: src.targetChainId }, { action: "ExecutePostedVaa", targetChainId: this.targetChainId },
buffer buffer
); );
let instructions: InstructionData[] = src.instructions.map((ix) => { let instructions: InstructionData[] = this.instructions.map((ix) => {
let programId = ix.programId.toBytes(); let programId = ix.programId.toBytes();
let accounts: AccountMetadata[] = ix.keys.map((acc) => { let accounts: AccountMetadata[] = ix.keys.map((acc) => {
return { return {
@ -135,4 +141,5 @@ export function encodeExecutePostedVaa(src: ExecutePostedVaaArgs): Buffer {
offset offset
); );
return buffer.subarray(0, span); return buffer.subarray(0, span);
}
} }

View File

@ -5,10 +5,12 @@ import {
toChainName, toChainName,
} from "@certusone/wormhole-sdk"; } from "@certusone/wormhole-sdk";
import * as BufferLayout from "@solana/buffer-layout"; import * as BufferLayout from "@solana/buffer-layout";
import { import { ExecutePostedVaa } from "./ExecutePostedVaa";
decodeExecutePostedVaa,
ExecutePostedVaaArgs, export interface PythGovernanceAction {
} from "./ExecutePostedVaa"; readonly targetChainId: ChainName;
encode(): Buffer;
}
export const ExecutorAction = { export const ExecutorAction = {
ExecutePostedVaa: 0, ExecutePostedVaa: 0,
@ -134,17 +136,14 @@ export function verifyHeader(
return governanceHeader; return governanceHeader;
} }
export function decodeGovernancePayload(data: Buffer): { export function decodeGovernancePayload(data: Buffer): PythGovernanceAction {
name: string;
args: ExecutePostedVaaArgs;
} {
const header = decodeHeader(data); const header = decodeHeader(data);
switch (header.action) { switch (header.action) {
case "ExecutePostedVaa": case "ExecutePostedVaa":
return { name: "ExecutePostedVaa", args: decodeExecutePostedVaa(data) }; return ExecutePostedVaa.decode(data);
default: default:
throw "Not supported"; throw "Not supported";
} }
} }
export { decodeExecutePostedVaa } from "./ExecutePostedVaa"; export { ExecutePostedVaa } from "./ExecutePostedVaa";

View File

@ -3,7 +3,10 @@ import { WormholeInstructionCoder } from "@certusone/wormhole-sdk/lib/cjs/solana
import { getPythClusterApiUrl } from "@pythnetwork/client/lib/cluster"; import { getPythClusterApiUrl } from "@pythnetwork/client/lib/cluster";
import { Connection, TransactionInstruction } from "@solana/web3.js"; import { Connection, TransactionInstruction } from "@solana/web3.js";
import { MultisigInstruction, MultisigInstructionProgram } from "."; import { MultisigInstruction, MultisigInstructionProgram } from ".";
import { decodeGovernancePayload } from "../governance_payload"; import {
decodeGovernancePayload,
PythGovernanceAction,
} from "../governance_payload";
import { AnchorAccounts, resolveAccountNames } from "./anchor"; import { AnchorAccounts, resolveAccountNames } from "./anchor";
export class WormholeMultisigInstruction implements MultisigInstruction { export class WormholeMultisigInstruction implements MultisigInstruction {
@ -47,12 +50,12 @@ export class WormholeMultisigInstruction implements MultisigInstruction {
if (result.name === "postMessage") { if (result.name === "postMessage") {
try { try {
const decoded = decodeGovernancePayload(result.args.payload); const decoded: PythGovernanceAction = decodeGovernancePayload(
result.args.governanceName = decoded.name; result.args.payload
result.args.governanceArgs = decoded.args; );
result.args.governanceAction = decoded;
} catch { } catch {
result.args.governanceName = "Unrecognized governance message"; result.args.governanceAction = {};
result.args.governanceArgs = {};
} }
} }
return result; return result;

View File

@ -10,7 +10,7 @@ import {
createWormholeProgramInterface, createWormholeProgramInterface,
getPostMessageAccounts, getPostMessageAccounts,
} from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole"; } from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole";
import { encodeExecutePostedVaa } from "./governance_payload/ExecutePostedVaa"; import { ExecutePostedVaa } from "./governance_payload/ExecutePostedVaa";
type SquadInstruction = { type SquadInstruction = {
instruction: TransactionInstruction; instruction: TransactionInstruction;
@ -152,10 +152,9 @@ export async function wrapAsRemoteInstruction(
provider provider
); );
const buffer = encodeExecutePostedVaa({ const buffer: Buffer = new ExecutePostedVaa("pythnet", [
targetChainId: "pythnet", instruction,
instructions: [instruction], ]).encode();
});
const accounts = getPostMessageAccounts( const accounts = getPostMessageAccounts(
wormholeAddress, wormholeAddress,