parent
411ed32859
commit
bab75170f9
|
@ -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(
|
||||
|
@ -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", []);
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
@ -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;
|
||||
|
||||
let instructions: TransactionInstruction[] = deserialized.map((ix) => {
|
||||
let programId: PublicKey = new PublicKey(ix.programId);
|
||||
let keys: AccountMeta[] = ix.accounts.map((acc) => {
|
||||
|
|
|
@ -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) {
|
||||
|
@ -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 =
|
||||
|
@ -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 */
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
@ -35,8 +39,8 @@ export class PythMultisigInstruction implements MultisigInstruction {
|
|||
);
|
||||
} else {
|
||||
return new PythMultisigInstruction(
|
||||
"Unrecognized instruction",
|
||||
{},
|
||||
UNRECOGNIZED_INSTRUCTION,
|
||||
{ data: instruction.data },
|
||||
{ named: {}, remaining: instruction.keys }
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = [];
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue