[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:
parent
fedb92e446
commit
8ef49e6128
|
@ -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,
|
SystemProgram.transfer({
|
||||||
instructions: [
|
fromPubkey: new PublicKey("AWQ18oKzd187aM2oMB4YirBcdgX1FgWfukmqEX91BRES"),
|
||||||
SystemProgram.transfer({
|
toPubkey: new PublicKey("J25GT2knN8V2Wvg9jNrYBuj9SZdsLnU6bK7WCGrL7daj"),
|
||||||
fromPubkey: new PublicKey(
|
lamports: 890880,
|
||||||
"AWQ18oKzd187aM2oMB4YirBcdgX1FgWfukmqEX91BRES"
|
}),
|
||||||
),
|
]);
|
||||||
toPubkey: new PublicKey("J25GT2knN8V2Wvg9jNrYBuj9SZdsLnU6bK7WCGrL7daj"),
|
|
||||||
lamports: 890880,
|
buffer = expectedExecutePostedVaa.encode();
|
||||||
}),
|
|
||||||
] as TransactionInstruction[],
|
|
||||||
};
|
|
||||||
buffer = encodeExecutePostedVaa(expectedExecuteVaaArgs);
|
|
||||||
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(
|
||||||
|
|
|
@ -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,
|
SystemProgram.transfer({
|
||||||
instructions: [
|
fromPubkey: PublicKey.unique(),
|
||||||
SystemProgram.transfer({
|
toPubkey: PublicKey.unique(),
|
||||||
fromPubkey: PublicKey.unique(),
|
lamports: 890880,
|
||||||
toPubkey: PublicKey.unique(),
|
}),
|
||||||
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(
|
) {
|
||||||
"pythnet"
|
expect(parsedInstruction.args.governanceAction.targetChainId).toBe(
|
||||||
);
|
"pythnet"
|
||||||
|
|
||||||
(
|
|
||||||
parsedInstruction.args.governanceArgs
|
|
||||||
.instructions as TransactionInstruction[]
|
|
||||||
).forEach((instruction, i) => {
|
|
||||||
expect(
|
|
||||||
instruction.programId.equals(
|
|
||||||
executePostedVaaArgs.instructions[i].programId
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
expect(
|
|
||||||
instruction.data.equals(executePostedVaaArgs.instructions[i].data)
|
(
|
||||||
);
|
parsedInstruction.args.governanceAction
|
||||||
instruction.keys.forEach((account, j) => {
|
.instructions as TransactionInstruction[]
|
||||||
|
).forEach((instruction, i) => {
|
||||||
expect(
|
expect(
|
||||||
account.pubkey.equals(
|
instruction.programId.equals(
|
||||||
executePostedVaaArgs.instructions[i].keys[j].pubkey
|
executePostedVaa.instructions[i].programId
|
||||||
)
|
)
|
||||||
).toBeTruthy();
|
|
||||||
expect(account.isSigner).toBe(
|
|
||||||
executePostedVaaArgs.instructions[i].keys[j].isSigner
|
|
||||||
);
|
);
|
||||||
expect(account.isWritable).toBe(
|
expect(
|
||||||
executePostedVaaArgs.instructions[i].keys[j].isWritable
|
instruction.data.equals(executePostedVaa.instructions[i].data)
|
||||||
);
|
);
|
||||||
|
instruction.keys.forEach((account, j) => {
|
||||||
|
expect(
|
||||||
|
account.pubkey.equals(
|
||||||
|
executePostedVaa.instructions[i].keys[j].pubkey
|
||||||
|
)
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(account.isSigner).toBe(
|
||||||
|
executePostedVaa.instructions[i].keys[j].isSigner
|
||||||
|
);
|
||||||
|
expect(account.isWritable).toBe(
|
||||||
|
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");
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,62 +77,69 @@ 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,
|
||||||
let deserialized = executePostedVaaLayout.decode(data);
|
instructions: TransactionInstruction[]
|
||||||
|
) {
|
||||||
|
this.targetChainId = targetChainId;
|
||||||
|
this.instructions = instructions;
|
||||||
|
}
|
||||||
|
|
||||||
let header = verifyHeader(deserialized.header);
|
/** Decode ExecutePostedVaaArgs */
|
||||||
|
static decode(data: Buffer): ExecutePostedVaa {
|
||||||
|
let deserialized = executePostedVaaLayout.decode(data);
|
||||||
|
|
||||||
let instructions: TransactionInstruction[] = deserialized.instructions.map(
|
let header = verifyHeader(deserialized.header);
|
||||||
(ix) => {
|
|
||||||
let programId: PublicKey = new PublicKey(ix.programId);
|
let instructions: TransactionInstruction[] = deserialized.instructions.map(
|
||||||
let keys: AccountMeta[] = ix.accounts.map((acc) => {
|
(ix) => {
|
||||||
|
let programId: PublicKey = new PublicKey(ix.programId);
|
||||||
|
let keys: AccountMeta[] = ix.accounts.map((acc) => {
|
||||||
|
return {
|
||||||
|
pubkey: new PublicKey(acc.pubkey),
|
||||||
|
isSigner: Boolean(acc.isSigner),
|
||||||
|
isWritable: Boolean(acc.isWritable),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
let data: Buffer = Buffer.from(ix.data);
|
||||||
|
return { programId, keys, data };
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return new ExecutePostedVaa(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
|
||||||
|
const buffer = Buffer.alloc(PACKET_DATA_SIZE);
|
||||||
|
const offset = encodeHeader(
|
||||||
|
{ action: "ExecutePostedVaa", targetChainId: this.targetChainId },
|
||||||
|
buffer
|
||||||
|
);
|
||||||
|
let instructions: InstructionData[] = this.instructions.map((ix) => {
|
||||||
|
let programId = ix.programId.toBytes();
|
||||||
|
let accounts: AccountMetadata[] = ix.keys.map((acc) => {
|
||||||
return {
|
return {
|
||||||
pubkey: new PublicKey(acc.pubkey),
|
pubkey: acc.pubkey.toBytes(),
|
||||||
isSigner: Boolean(acc.isSigner),
|
isSigner: acc.isSigner ? 1 : 0,
|
||||||
isWritable: Boolean(acc.isWritable),
|
isWritable: acc.isWritable ? 1 : 0,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
let data: Buffer = Buffer.from(ix.data);
|
let data = [...ix.data];
|
||||||
return { programId, keys, data };
|
return { programId, accounts, data };
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return { targetChainId: header.targetChainId, instructions };
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 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 offset = encodeHeader(
|
|
||||||
{ action: "ExecutePostedVaa", targetChainId: src.targetChainId },
|
|
||||||
buffer
|
|
||||||
);
|
|
||||||
let instructions: InstructionData[] = src.instructions.map((ix) => {
|
|
||||||
let programId = ix.programId.toBytes();
|
|
||||||
let accounts: AccountMetadata[] = ix.keys.map((acc) => {
|
|
||||||
return {
|
|
||||||
pubkey: acc.pubkey.toBytes(),
|
|
||||||
isSigner: acc.isSigner ? 1 : 0,
|
|
||||||
isWritable: acc.isWritable ? 1 : 0,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
let data = [...ix.data];
|
|
||||||
return { programId, accounts, data };
|
|
||||||
});
|
|
||||||
|
|
||||||
const span =
|
const span =
|
||||||
offset +
|
offset +
|
||||||
new Vector<InstructionData>(instructionDataLayout, "instructions").encode(
|
new Vector<InstructionData>(instructionDataLayout, "instructions").encode(
|
||||||
instructions,
|
instructions,
|
||||||
buffer,
|
buffer,
|
||||||
offset
|
offset
|
||||||
);
|
);
|
||||||
return buffer.subarray(0, span);
|
return buffer.subarray(0, span);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue