[xc-admin] Decoders can't throw (#486)

* Push code

* Format
This commit is contained in:
guibescos 2023-01-12 20:35:19 -06:00 committed by GitHub
parent 411ed32859
commit bab75170f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 109 additions and 67 deletions

View File

@ -14,8 +14,8 @@ test("GovernancePayload ser/de", (done) => {
buffer.equals(Buffer.from([80, 84, 71, 77, 0, 0, 0, 26])) buffer.equals(Buffer.from([80, 84, 71, 77, 0, 0, 0, 26]))
).toBeTruthy(); ).toBeTruthy();
let governanceHeader = PythGovernanceHeader.decode(buffer); let governanceHeader = PythGovernanceHeader.decode(buffer);
expect(governanceHeader.targetChainId).toBe("pythnet"); expect(governanceHeader?.targetChainId).toBe("pythnet");
expect(governanceHeader.action).toBe("ExecutePostedVaa"); expect(governanceHeader?.action).toBe("ExecutePostedVaa");
// Valid header 2 // Valid header 2
expectedGovernanceHeader = new PythGovernanceHeader( expectedGovernanceHeader = new PythGovernanceHeader(
@ -37,25 +37,25 @@ test("GovernancePayload ser/de", (done) => {
expect(governanceHeader?.action).toBe("SetFee"); expect(governanceHeader?.action).toBe("SetFee");
// Wrong magic number // Wrong magic number
expect(() => expect(
PythGovernanceHeader.decode( PythGovernanceHeader.decode(
Buffer.from([0, 0, 0, 0, 0, 0, 0, 26, 0, 0, 0, 0]) Buffer.from([0, 0, 0, 0, 0, 0, 0, 26, 0, 0, 0, 0])
) )
).toThrow("Wrong magic number"); ).toBeUndefined();
// Wrong chain // Wrong chain
expect(() => expect(
PythGovernanceHeader.decode( PythGovernanceHeader.decode(
Buffer.from([80, 84, 71, 77, 0, 0, 255, 255, 0, 0, 0, 0]) Buffer.from([80, 84, 71, 77, 0, 0, 255, 255, 0, 0, 0, 0])
) )
).toThrow("Chain Id not found"); ).toBeUndefined();
// Wrong module/action combination // Wrong module/action combination
expect(() => expect(
PythGovernanceHeader.decode( PythGovernanceHeader.decode(
Buffer.from([80, 84, 71, 77, 0, 1, 0, 26, 0, 0, 0, 0]) 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 // Decode executePostVaa with empty instructions
let expectedExecutePostedVaa = new ExecutePostedVaa("pythnet", []); let expectedExecutePostedVaa = new ExecutePostedVaa("pythnet", []);

View File

@ -199,9 +199,6 @@ test("Wormhole multisig instruction parse: send message with governance payload"
.then((instruction) => { .then((instruction) => {
const parsedInstruction = parser.parseInstruction(instruction); const parsedInstruction = parser.parseInstruction(instruction);
if (parsedInstruction instanceof WormholeMultisigInstruction) { if (parsedInstruction instanceof WormholeMultisigInstruction) {
expect(
parsedInstruction instanceof WormholeMultisigInstruction
).toBeTruthy();
expect(parsedInstruction.program).toBe( expect(parsedInstruction.program).toBe(
MultisigInstructionProgram.WormholeBridge MultisigInstructionProgram.WormholeBridge
); );
@ -313,15 +310,13 @@ test("Wormhole multisig instruction parse: send message with governance payload"
); );
expect(parsedInstruction.args.consistencyLevel).toBe(0); expect(parsedInstruction.args.consistencyLevel).toBe(0);
if ( if (parsedInstruction.governanceAction instanceof ExecutePostedVaa) {
parsedInstruction.args.governanceAction instanceof ExecutePostedVaa expect(parsedInstruction.governanceAction.targetChainId).toBe(
) {
expect(parsedInstruction.args.governanceAction.targetChainId).toBe(
"pythnet" "pythnet"
); );
( (
parsedInstruction.args.governanceAction parsedInstruction.governanceAction
.instructions as TransactionInstruction[] .instructions as TransactionInstruction[]
).forEach((instruction, i) => { ).forEach((instruction, i) => {
expect( expect(

View File

@ -1,6 +1,10 @@
import { ChainId, ChainName } from "@certusone/wormhole-sdk"; import { ChainId, ChainName } from "@certusone/wormhole-sdk";
import * as BufferLayout from "@solana/buffer-layout"; import * as BufferLayout from "@solana/buffer-layout";
import { PythGovernanceAction, PythGovernanceHeader } from "."; import {
PythGovernanceAction,
PythGovernanceHeader,
safeLayoutDecode,
} from ".";
import { Layout } from "@solana/buffer-layout"; import { Layout } from "@solana/buffer-layout";
import { import {
AccountMeta, AccountMeta,
@ -75,11 +79,16 @@ export class ExecutePostedVaa implements PythGovernanceAction {
} }
/** Decode ExecutePostedVaa */ /** Decode ExecutePostedVaa */
static decode(data: Buffer): ExecutePostedVaa { static decode(data: Buffer): ExecutePostedVaa | undefined {
let header = PythGovernanceHeader.decode(data); const header = PythGovernanceHeader.decode(data);
let deserialized = this.layout.decode( if (!header) return undefined;
const deserialized = safeLayoutDecode(
this.layout,
data.subarray(PythGovernanceHeader.span) data.subarray(PythGovernanceHeader.span)
); );
if (!deserialized) return undefined;
let instructions: TransactionInstruction[] = deserialized.map((ix) => { let instructions: TransactionInstruction[] = deserialized.map((ix) => {
let programId: PublicKey = new PublicKey(ix.programId); let programId: PublicKey = new PublicKey(ix.programId);
let keys: AccountMeta[] = ix.accounts.map((acc) => { let keys: AccountMeta[] = ix.accounts.map((acc) => {

View File

@ -30,7 +30,7 @@ export const TargetAction = {
/** Helper to get the ActionName from a (moduleId, actionId) tuple*/ /** Helper to get the ActionName from a (moduleId, actionId) tuple*/
export function toActionName( export function toActionName(
deserialized: Readonly<{ moduleId: number; actionId: number }> deserialized: Readonly<{ moduleId: number; actionId: number }>
): ActionName { ): ActionName | undefined {
if (deserialized.moduleId == MODULE_EXECUTOR && deserialized.actionId == 0) { if (deserialized.moduleId == MODULE_EXECUTOR && deserialized.actionId == 0) {
return "ExecutePostedVaa"; return "ExecutePostedVaa";
} else if (deserialized.moduleId == MODULE_TARGET) { } else if (deserialized.moduleId == MODULE_TARGET) {
@ -49,7 +49,7 @@ export function toActionName(
return "RequestGovernanceDataSourceTransfer"; return "RequestGovernanceDataSourceTransfer";
} }
} }
throw new Error("Invalid header, action doesn't match module"); return undefined;
} }
export declare type ActionName = export declare type ActionName =
@ -84,23 +84,28 @@ export class PythGovernanceHeader {
this.action = action; this.action = action;
} }
/** Decode Pyth Governance Header */ /** Decode Pyth Governance Header */
static decode(data: Buffer): PythGovernanceHeader { static decode(data: Buffer): PythGovernanceHeader | undefined {
let deserialized = this.layout.decode(data); const deserialized = safeLayoutDecode(this.layout, data);
if (deserialized.magicNumber !== MAGIC_NUMBER) {
throw new Error("Wrong magic number");
}
if (!toChainName(deserialized.chain)) { if (!deserialized) return undefined;
throw new Error("Chain Id not found");
}
return new PythGovernanceHeader( if (deserialized.magicNumber !== MAGIC_NUMBER) return undefined;
toChainName(deserialized.chain),
toActionName({ if (!toChainName(deserialized.chain)) return undefined;
actionId: deserialized.action,
moduleId: deserialized.module, 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 */ /** Encode Pyth Governance Header */
@ -134,13 +139,28 @@ export const MODULE_EXECUTOR = 0;
export const MODULE_TARGET = 1; export const MODULE_TARGET = 1;
/** Decode a governance payload */ /** Decode a governance payload */
export function decodeGovernancePayload(data: Buffer): PythGovernanceAction { export function decodeGovernancePayload(
data: Buffer
): PythGovernanceAction | undefined {
const header = PythGovernanceHeader.decode(data); const header = PythGovernanceHeader.decode(data);
if (!header) return undefined;
switch (header.action) { switch (header.action) {
case "ExecutePostedVaa": case "ExecutePostedVaa":
return ExecutePostedVaa.decode(data); return ExecutePostedVaa.decode(data);
default: 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;
} }
} }

View File

@ -1,4 +1,8 @@
import { MultisigInstruction, MultisigInstructionProgram } from "."; import {
MultisigInstruction,
MultisigInstructionProgram,
UNRECOGNIZED_INSTRUCTION,
} from ".";
import { AnchorAccounts, resolveAccountNames } from "./anchor"; import { AnchorAccounts, resolveAccountNames } from "./anchor";
import { pythIdl, pythOracleCoder } from "@pythnetwork/client"; import { pythIdl, pythOracleCoder } from "@pythnetwork/client";
import { TransactionInstruction } from "@solana/web3.js"; import { TransactionInstruction } from "@solana/web3.js";
@ -35,8 +39,8 @@ export class PythMultisigInstruction implements MultisigInstruction {
); );
} else { } else {
return new PythMultisigInstruction( return new PythMultisigInstruction(
"Unrecognized instruction", UNRECOGNIZED_INSTRUCTION,
{}, { data: instruction.data },
{ named: {}, remaining: instruction.keys } { named: {}, remaining: instruction.keys }
); );
} }

View File

@ -2,7 +2,11 @@ import { createReadOnlyWormholeProgramInterface } from "@certusone/wormhole-sdk/
import { WormholeInstructionCoder } from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole/coder/instruction"; import { WormholeInstructionCoder } from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole/coder/instruction";
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,
UNRECOGNIZED_INSTRUCTION,
} from ".";
import { import {
decodeGovernancePayload, decodeGovernancePayload,
PythGovernanceAction, PythGovernanceAction,
@ -14,15 +18,18 @@ export class WormholeMultisigInstruction implements MultisigInstruction {
readonly name: string; readonly name: string;
readonly args: { [key: string]: any }; readonly args: { [key: string]: any };
readonly accounts: AnchorAccounts; readonly accounts: AnchorAccounts;
readonly governanceAction: PythGovernanceAction | undefined;
constructor( constructor(
name: string, name: string,
args: { [key: string]: any }, args: { [key: string]: any },
accounts: AnchorAccounts accounts: AnchorAccounts,
governanceAction: PythGovernanceAction | undefined
) { ) {
this.name = name; this.name = name;
this.args = args; this.args = args;
this.accounts = accounts; this.accounts = accounts;
this.governanceAction = governanceAction;
} }
static fromTransactionInstruction( static fromTransactionInstruction(
@ -38,32 +45,38 @@ export class WormholeMultisigInstruction implements MultisigInstruction {
).decode(instruction.data); ).decode(instruction.data);
if (deserializedData) { if (deserializedData) {
let result = new WormholeMultisigInstruction( if (deserializedData.name === "postMessage") {
deserializedData.name, const decodedGovernanceAction: PythGovernanceAction | undefined =
deserializedData.data, decodeGovernancePayload((deserializedData.data as any).payload);
resolveAccountNames(
wormholeProgram.idl,
deserializedData.name,
instruction
)
);
if (result.name === "postMessage") { return new WormholeMultisigInstruction(
try { deserializedData.name,
const decoded: PythGovernanceAction = decodeGovernancePayload( deserializedData.data,
result.args.payload resolveAccountNames(
); wormholeProgram.idl,
result.args.governanceAction = decoded; deserializedData.name,
} catch { instruction
result.args.governanceAction = {}; ),
} decodedGovernanceAction
);
} else {
return new WormholeMultisigInstruction(
deserializedData.name,
deserializedData.data,
resolveAccountNames(
wormholeProgram.idl,
deserializedData.name,
instruction
),
undefined
);
} }
return result;
} else { } else {
return new WormholeMultisigInstruction( return new WormholeMultisigInstruction(
"Unrecognized instruction", UNRECOGNIZED_INSTRUCTION,
{}, { data: instruction.data },
{ named: {}, remaining: instruction.keys } { named: {}, remaining: instruction.keys },
undefined
); );
} }
} }

View File

@ -15,7 +15,7 @@ export function resolveAccountNames(
): { named: NamedAccounts; remaining: RemainingAccounts } { ): { named: NamedAccounts; remaining: RemainingAccounts } {
const ix = idl.instructions.find((ix) => ix.name == name); const ix = idl.instructions.find((ix) => ix.name == name);
if (!ix) { if (!ix) {
throw Error("Instruction name not found"); return { named: {}, remaining: instruction.keys };
} }
const named: NamedAccounts = {}; const named: NamedAccounts = {};
const remaining: RemainingAccounts = []; const remaining: RemainingAccounts = [];

View File

@ -7,6 +7,7 @@ import { WORMHOLE_ADDRESS } from "../wormhole";
import { PythMultisigInstruction } from "./PythMultisigInstruction"; import { PythMultisigInstruction } from "./PythMultisigInstruction";
import { WormholeMultisigInstruction } from "./WormholeMultisigInstruction"; import { WormholeMultisigInstruction } from "./WormholeMultisigInstruction";
export const UNRECOGNIZED_INSTRUCTION = "unrecognizedInstruction";
export enum MultisigInstructionProgram { export enum MultisigInstructionProgram {
PythOracle, PythOracle,
WormholeBridge, WormholeBridge,