Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ test("GovernancePayload ser/de", (done) => {
buffer.equals(Buffer.from([80, 84, 71, 77, 0, 0, 0, 26]))
).toBeTruthy();
let governanceHeader = PythGovernanceHeader.decode(buffer);
expect(governanceHeader.targetChainId).toBe("pythnet");
expect(governanceHeader.action).toBe("ExecutePostedVaa");
expect(governanceHeader?.targetChainId).toBe("pythnet");
expect(governanceHeader?.action).toBe("ExecutePostedVaa");

// Valid header 2
expectedGovernanceHeader = new PythGovernanceHeader(
Expand All @@ -37,25 +37,25 @@ test("GovernancePayload ser/de", (done) => {
expect(governanceHeader?.action).toBe("SetFee");

// Wrong magic number
expect(() =>
expect(
PythGovernanceHeader.decode(
Buffer.from([0, 0, 0, 0, 0, 0, 0, 26, 0, 0, 0, 0])
)
).toThrow("Wrong magic number");
).toBeUndefined();

// Wrong chain
expect(() =>
expect(
PythGovernanceHeader.decode(
Buffer.from([80, 84, 71, 77, 0, 0, 255, 255, 0, 0, 0, 0])
)
).toThrow("Chain Id not found");
).toBeUndefined();

// Wrong module/action combination
expect(() =>
expect(
PythGovernanceHeader.decode(
Buffer.from([80, 84, 71, 77, 0, 1, 0, 26, 0, 0, 0, 0])
)
).toThrow("Invalid header, action doesn't match module");
).toBeUndefined();

// Decode executePostVaa with empty instructions
let expectedExecutePostedVaa = new ExecutePostedVaa("pythnet", []);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,9 +199,6 @@ test("Wormhole multisig instruction parse: send message with governance payload"
.then((instruction) => {
const parsedInstruction = parser.parseInstruction(instruction);
if (parsedInstruction instanceof WormholeMultisigInstruction) {
expect(
parsedInstruction instanceof WormholeMultisigInstruction
).toBeTruthy();
expect(parsedInstruction.program).toBe(
MultisigInstructionProgram.WormholeBridge
);
Expand Down Expand Up @@ -313,15 +310,13 @@ test("Wormhole multisig instruction parse: send message with governance payload"
);
expect(parsedInstruction.args.consistencyLevel).toBe(0);

if (
parsedInstruction.args.governanceAction instanceof ExecutePostedVaa
) {
expect(parsedInstruction.args.governanceAction.targetChainId).toBe(
if (parsedInstruction.governanceAction instanceof ExecutePostedVaa) {
expect(parsedInstruction.governanceAction.targetChainId).toBe(
"pythnet"
);

(
parsedInstruction.args.governanceAction
parsedInstruction.governanceAction
.instructions as TransactionInstruction[]
).forEach((instruction, i) => {
expect(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { ChainId, ChainName } from "@certusone/wormhole-sdk";
import * as BufferLayout from "@solana/buffer-layout";
import { PythGovernanceAction, PythGovernanceHeader } from ".";
import {
PythGovernanceAction,
PythGovernanceHeader,
safeLayoutDecode,
} from ".";
import { Layout } from "@solana/buffer-layout";
import {
AccountMeta,
Expand Down Expand Up @@ -75,11 +79,16 @@ export class ExecutePostedVaa implements PythGovernanceAction {
}

/** Decode ExecutePostedVaa */
static decode(data: Buffer): ExecutePostedVaa {
let header = PythGovernanceHeader.decode(data);
let deserialized = this.layout.decode(
static decode(data: Buffer): ExecutePostedVaa | undefined {
const header = PythGovernanceHeader.decode(data);
if (!header) return undefined;

const deserialized = safeLayoutDecode(
this.layout,
data.subarray(PythGovernanceHeader.span)
);
if (!deserialized) return undefined;

Copy link
Contributor Author

@guibescos guibescos Jan 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buffer-layout can throw errors, so we need to handle them here so the user of our structs won't have to deal with them

let instructions: TransactionInstruction[] = deserialized.map((ix) => {
let programId: PublicKey = new PublicKey(ix.programId);
let keys: AccountMeta[] = ix.accounts.map((acc) => {
Expand Down
58 changes: 39 additions & 19 deletions xc-admin/packages/xc-admin-common/src/governance_payload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const TargetAction = {
/** Helper to get the ActionName from a (moduleId, actionId) tuple*/
export function toActionName(
deserialized: Readonly<{ moduleId: number; actionId: number }>
): ActionName {
): ActionName | undefined {
if (deserialized.moduleId == MODULE_EXECUTOR && deserialized.actionId == 0) {
return "ExecutePostedVaa";
} else if (deserialized.moduleId == MODULE_TARGET) {
Expand All @@ -49,7 +49,7 @@ export function toActionName(
return "RequestGovernanceDataSourceTransfer";
}
}
throw new Error("Invalid header, action doesn't match module");
return undefined;
}

export declare type ActionName =
Expand Down Expand Up @@ -84,23 +84,28 @@ export class PythGovernanceHeader {
this.action = action;
}
/** Decode Pyth Governance Header */
static decode(data: Buffer): PythGovernanceHeader {
let deserialized = this.layout.decode(data);
if (deserialized.magicNumber !== MAGIC_NUMBER) {
throw new Error("Wrong magic number");
}
static decode(data: Buffer): PythGovernanceHeader | undefined {
const deserialized = safeLayoutDecode(this.layout, data);

if (!toChainName(deserialized.chain)) {
throw new Error("Chain Id not found");
}
if (!deserialized) return undefined;

return new PythGovernanceHeader(
toChainName(deserialized.chain),
toActionName({
actionId: deserialized.action,
moduleId: deserialized.module,
})
);
if (deserialized.magicNumber !== MAGIC_NUMBER) return undefined;

if (!toChainName(deserialized.chain)) return undefined;

const actionName = toActionName({
actionId: deserialized.action,
moduleId: deserialized.module,
});

if (actionName) {
return new PythGovernanceHeader(
toChainName(deserialized.chain),
actionName
);
} else {
return undefined;
}
}

/** Encode Pyth Governance Header */
Expand Down Expand Up @@ -134,13 +139,28 @@ export const MODULE_EXECUTOR = 0;
export const MODULE_TARGET = 1;

/** Decode a governance payload */
export function decodeGovernancePayload(data: Buffer): PythGovernanceAction {
export function decodeGovernancePayload(
data: Buffer
): PythGovernanceAction | undefined {
const header = PythGovernanceHeader.decode(data);
if (!header) return undefined;

switch (header.action) {
case "ExecutePostedVaa":
return ExecutePostedVaa.decode(data);
default:
throw "Not supported";
return undefined;
}
}

export function safeLayoutDecode<T>(
layout: BufferLayout.Layout<T>,
data: Buffer
): T | undefined {
try {
return layout.decode(data);
} catch {
return undefined;
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { MultisigInstruction, MultisigInstructionProgram } from ".";
import {
MultisigInstruction,
MultisigInstructionProgram,
UNRECOGNIZED_INSTRUCTION,
} from ".";
import { AnchorAccounts, resolveAccountNames } from "./anchor";
import { pythIdl, pythOracleCoder } from "@pythnetwork/client";
import { TransactionInstruction } from "@solana/web3.js";
Expand Down Expand Up @@ -35,8 +39,8 @@ export class PythMultisigInstruction implements MultisigInstruction {
);
} else {
return new PythMultisigInstruction(
"Unrecognized instruction",
{},
UNRECOGNIZED_INSTRUCTION,
{ data: instruction.data },
{ named: {}, remaining: instruction.keys }
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { createReadOnlyWormholeProgramInterface } from "@certusone/wormhole-sdk/
import { WormholeInstructionCoder } from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole/coder/instruction";
import { getPythClusterApiUrl } from "@pythnetwork/client/lib/cluster";
import { Connection, TransactionInstruction } from "@solana/web3.js";
import { MultisigInstruction, MultisigInstructionProgram } from ".";
import {
MultisigInstruction,
MultisigInstructionProgram,
UNRECOGNIZED_INSTRUCTION,
} from ".";
import {
decodeGovernancePayload,
PythGovernanceAction,
Expand All @@ -14,15 +18,18 @@ export class WormholeMultisigInstruction implements MultisigInstruction {
readonly name: string;
readonly args: { [key: string]: any };
readonly accounts: AnchorAccounts;
readonly governanceAction: PythGovernanceAction | undefined;

constructor(
name: string,
args: { [key: string]: any },
accounts: AnchorAccounts
accounts: AnchorAccounts,
governanceAction: PythGovernanceAction | undefined
) {
this.name = name;
this.args = args;
this.accounts = accounts;
this.governanceAction = governanceAction;
}

static fromTransactionInstruction(
Expand All @@ -38,32 +45,38 @@ export class WormholeMultisigInstruction implements MultisigInstruction {
).decode(instruction.data);

if (deserializedData) {
let result = new WormholeMultisigInstruction(
deserializedData.name,
deserializedData.data,
resolveAccountNames(
wormholeProgram.idl,
deserializedData.name,
instruction
)
);
if (deserializedData.name === "postMessage") {
const decodedGovernanceAction: PythGovernanceAction | undefined =
decodeGovernancePayload((deserializedData.data as any).payload);

if (result.name === "postMessage") {
try {
const decoded: PythGovernanceAction = decodeGovernancePayload(
result.args.payload
);
result.args.governanceAction = decoded;
} catch {
result.args.governanceAction = {};
}
return new WormholeMultisigInstruction(
deserializedData.name,
deserializedData.data,
resolveAccountNames(
wormholeProgram.idl,
deserializedData.name,
instruction
),
decodedGovernanceAction
);
} else {
return new WormholeMultisigInstruction(
deserializedData.name,
deserializedData.data,
resolveAccountNames(
wormholeProgram.idl,
deserializedData.name,
instruction
),
undefined
);
}
return result;
} else {
return new WormholeMultisigInstruction(
"Unrecognized instruction",
{},
{ named: {}, remaining: instruction.keys }
UNRECOGNIZED_INSTRUCTION,
{ data: instruction.data },
{ named: {}, remaining: instruction.keys },
undefined
);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function resolveAccountNames(
): { named: NamedAccounts; remaining: RemainingAccounts } {
const ix = idl.instructions.find((ix) => ix.name == name);
if (!ix) {
throw Error("Instruction name not found");
return { named: {}, remaining: instruction.keys };
}
const named: NamedAccounts = {};
const remaining: RemainingAccounts = [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { WORMHOLE_ADDRESS } from "../wormhole";
import { PythMultisigInstruction } from "./PythMultisigInstruction";
import { WormholeMultisigInstruction } from "./WormholeMultisigInstruction";

export const UNRECOGNIZED_INSTRUCTION = "unrecognizedInstruction";
export enum MultisigInstructionProgram {
PythOracle,
WormholeBridge,
Expand Down