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]))
|
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", []);
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 = [];
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue