[xc-admin] Header becomes class (#484)
* Header becomes class 2 * Revert test * Cleanup * Cleanup
This commit is contained in:
parent
31ab162168
commit
411ed32859
|
@ -1,66 +1,60 @@
|
||||||
import { ChainName } from "@certusone/wormhole-sdk";
|
import { PublicKey, SystemProgram } from "@solana/web3.js";
|
||||||
import { PACKET_DATA_SIZE, PublicKey, SystemProgram } from "@solana/web3.js";
|
import { PythGovernanceHeader, ExecutePostedVaa } from "..";
|
||||||
import { ActionName, decodeHeader, encodeHeader, ExecutePostedVaa } from "..";
|
|
||||||
|
|
||||||
test("GovernancePayload ser/de", (done) => {
|
test("GovernancePayload ser/de", (done) => {
|
||||||
jest.setTimeout(60000);
|
jest.setTimeout(60000);
|
||||||
|
|
||||||
// Valid header 1
|
// Valid header 1
|
||||||
let expectedGovernanceHeader = {
|
let expectedGovernanceHeader = new PythGovernanceHeader(
|
||||||
targetChainId: "pythnet" as ChainName,
|
"pythnet",
|
||||||
action: "ExecutePostedVaa" as ActionName,
|
"ExecutePostedVaa"
|
||||||
};
|
);
|
||||||
let buffer = Buffer.alloc(PACKET_DATA_SIZE);
|
let buffer = expectedGovernanceHeader.encode();
|
||||||
let span = encodeHeader(expectedGovernanceHeader, buffer);
|
|
||||||
expect(
|
expect(
|
||||||
buffer.subarray(0, span).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 = decodeHeader(buffer.subarray(0, span));
|
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 = {
|
expectedGovernanceHeader = new PythGovernanceHeader(
|
||||||
targetChainId: "unset" as ChainName,
|
"unset",
|
||||||
action: "ExecutePostedVaa" as ActionName,
|
"ExecutePostedVaa"
|
||||||
};
|
);
|
||||||
buffer = Buffer.alloc(PACKET_DATA_SIZE);
|
buffer = expectedGovernanceHeader.encode();
|
||||||
span = encodeHeader(expectedGovernanceHeader, buffer);
|
expect(buffer.equals(Buffer.from([80, 84, 71, 77, 0, 0, 0, 0]))).toBeTruthy();
|
||||||
expect(
|
governanceHeader = PythGovernanceHeader.decode(buffer);
|
||||||
buffer.subarray(0, span).equals(Buffer.from([80, 84, 71, 77, 0, 0, 0, 0]))
|
|
||||||
).toBeTruthy();
|
|
||||||
governanceHeader = decodeHeader(buffer.subarray(0, span));
|
|
||||||
expect(governanceHeader?.targetChainId).toBe("unset");
|
expect(governanceHeader?.targetChainId).toBe("unset");
|
||||||
expect(governanceHeader?.action).toBe("ExecutePostedVaa");
|
expect(governanceHeader?.action).toBe("ExecutePostedVaa");
|
||||||
|
|
||||||
// Valid header 3
|
// Valid header 3
|
||||||
expectedGovernanceHeader = {
|
expectedGovernanceHeader = new PythGovernanceHeader("solana", "SetFee");
|
||||||
targetChainId: "solana" as ChainName,
|
buffer = expectedGovernanceHeader.encode();
|
||||||
action: "SetFee" as ActionName,
|
expect(buffer.equals(Buffer.from([80, 84, 71, 77, 1, 3, 0, 1]))).toBeTruthy();
|
||||||
};
|
governanceHeader = PythGovernanceHeader.decode(buffer);
|
||||||
buffer = Buffer.alloc(PACKET_DATA_SIZE);
|
|
||||||
span = encodeHeader(expectedGovernanceHeader, buffer);
|
|
||||||
expect(
|
|
||||||
buffer.subarray(0, span).equals(Buffer.from([80, 84, 71, 77, 1, 3, 0, 1]))
|
|
||||||
).toBeTruthy();
|
|
||||||
governanceHeader = decodeHeader(buffer.subarray(0, span));
|
|
||||||
expect(governanceHeader?.targetChainId).toBe("solana");
|
expect(governanceHeader?.targetChainId).toBe("solana");
|
||||||
expect(governanceHeader?.action).toBe("SetFee");
|
expect(governanceHeader?.action).toBe("SetFee");
|
||||||
|
|
||||||
// Wrong magic number
|
// Wrong magic number
|
||||||
expect(() =>
|
expect(() =>
|
||||||
decodeHeader(Buffer.from([0, 0, 0, 0, 0, 0, 0, 26, 0, 0, 0, 0]))
|
PythGovernanceHeader.decode(
|
||||||
|
Buffer.from([0, 0, 0, 0, 0, 0, 0, 26, 0, 0, 0, 0])
|
||||||
|
)
|
||||||
).toThrow("Wrong magic number");
|
).toThrow("Wrong magic number");
|
||||||
|
|
||||||
// Wrong chain
|
// Wrong chain
|
||||||
expect(() =>
|
expect(() =>
|
||||||
decodeHeader(Buffer.from([80, 84, 71, 77, 0, 0, 255, 255, 0, 0, 0, 0]))
|
PythGovernanceHeader.decode(
|
||||||
|
Buffer.from([80, 84, 71, 77, 0, 0, 255, 255, 0, 0, 0, 0])
|
||||||
|
)
|
||||||
).toThrow("Chain Id not found");
|
).toThrow("Chain Id not found");
|
||||||
|
|
||||||
// Wrong module/action combination
|
// Wrong module/action combination
|
||||||
expect(() =>
|
expect(() =>
|
||||||
decodeHeader(Buffer.from([80, 84, 71, 77, 0, 1, 0, 26, 0, 0, 0, 0]))
|
PythGovernanceHeader.decode(
|
||||||
|
Buffer.from([80, 84, 71, 77, 0, 1, 0, 26, 0, 0, 0, 0])
|
||||||
|
)
|
||||||
).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
|
||||||
|
|
|
@ -1,11 +1,6 @@
|
||||||
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 {
|
import { PythGovernanceAction, PythGovernanceHeader } from ".";
|
||||||
encodeHeader,
|
|
||||||
governanceHeaderLayout,
|
|
||||||
PythGovernanceAction,
|
|
||||||
verifyHeader,
|
|
||||||
} from ".";
|
|
||||||
import { Layout } from "@solana/buffer-layout";
|
import { Layout } from "@solana/buffer-layout";
|
||||||
import {
|
import {
|
||||||
AccountMeta,
|
AccountMeta,
|
||||||
|
@ -56,30 +51,20 @@ export const accountMetaLayout = BufferLayout.struct<AccountMetadata>([
|
||||||
BufferLayout.u8("isSigner"),
|
BufferLayout.u8("isSigner"),
|
||||||
BufferLayout.u8("isWritable"),
|
BufferLayout.u8("isWritable"),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const instructionDataLayout = BufferLayout.struct<InstructionData>([
|
export const instructionDataLayout = BufferLayout.struct<InstructionData>([
|
||||||
BufferLayout.blob(32, "programId"),
|
BufferLayout.blob(32, "programId"),
|
||||||
new Vector<AccountMetadata>(accountMetaLayout, "accounts"),
|
new Vector<AccountMetadata>(accountMetaLayout, "accounts"),
|
||||||
new Vector<number>(BufferLayout.u8(), "data"),
|
new Vector<number>(BufferLayout.u8(), "data"),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const executePostedVaaLayout: BufferLayout.Structure<
|
|
||||||
Readonly<{
|
|
||||||
header: Readonly<{
|
|
||||||
magicNumber: number;
|
|
||||||
module: number;
|
|
||||||
action: number;
|
|
||||||
chain: ChainId;
|
|
||||||
}>;
|
|
||||||
instructions: InstructionData[];
|
|
||||||
}>
|
|
||||||
> = BufferLayout.struct([
|
|
||||||
governanceHeaderLayout(),
|
|
||||||
new Vector<InstructionData>(instructionDataLayout, "instructions"),
|
|
||||||
]);
|
|
||||||
|
|
||||||
export class ExecutePostedVaa implements PythGovernanceAction {
|
export class ExecutePostedVaa implements PythGovernanceAction {
|
||||||
readonly targetChainId: ChainName;
|
readonly targetChainId: ChainName;
|
||||||
readonly instructions: TransactionInstruction[];
|
readonly instructions: TransactionInstruction[];
|
||||||
|
static layout: Vector<InstructionData> = new Vector<InstructionData>(
|
||||||
|
instructionDataLayout,
|
||||||
|
"instructions"
|
||||||
|
);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
targetChainId: ChainName,
|
targetChainId: ChainName,
|
||||||
|
@ -89,37 +74,36 @@ export class ExecutePostedVaa implements PythGovernanceAction {
|
||||||
this.instructions = instructions;
|
this.instructions = instructions;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Decode ExecutePostedVaaArgs */
|
/** Decode ExecutePostedVaa */
|
||||||
static decode(data: Buffer): ExecutePostedVaa {
|
static decode(data: Buffer): ExecutePostedVaa {
|
||||||
let deserialized = executePostedVaaLayout.decode(data);
|
let header = PythGovernanceHeader.decode(data);
|
||||||
|
let deserialized = this.layout.decode(
|
||||||
let header = verifyHeader(deserialized.header);
|
data.subarray(PythGovernanceHeader.span)
|
||||||
|
|
||||||
let instructions: TransactionInstruction[] = deserialized.instructions.map(
|
|
||||||
(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 };
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
let instructions: TransactionInstruction[] = deserialized.map((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);
|
return new ExecutePostedVaa(header.targetChainId, instructions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Encode ExecutePostedVaaArgs */
|
/** Encode ExecutePostedVaa */
|
||||||
encode(): Buffer {
|
encode(): Buffer {
|
||||||
// PACKET_DATA_SIZE is the maximum transaction size of Solana, so our serialized payload will never be bigger than that
|
const headerBuffer = new PythGovernanceHeader(
|
||||||
|
this.targetChainId,
|
||||||
|
"ExecutePostedVaa"
|
||||||
|
).encode();
|
||||||
|
|
||||||
|
// The code will crash if the payload is actually bigger than PACKET_DATA_SIZE. But PACKET_DATA_SIZE is the maximum transaction size of Solana, so our serialized payload should never be bigger than this anyway
|
||||||
const buffer = Buffer.alloc(PACKET_DATA_SIZE);
|
const buffer = Buffer.alloc(PACKET_DATA_SIZE);
|
||||||
const offset = encodeHeader(
|
|
||||||
{ action: "ExecutePostedVaa", targetChainId: this.targetChainId },
|
|
||||||
buffer
|
|
||||||
);
|
|
||||||
let instructions: InstructionData[] = this.instructions.map((ix) => {
|
let instructions: InstructionData[] = this.instructions.map((ix) => {
|
||||||
let programId = ix.programId.toBytes();
|
let programId = ix.programId.toBytes();
|
||||||
let accounts: AccountMetadata[] = ix.keys.map((acc) => {
|
let accounts: AccountMetadata[] = ix.keys.map((acc) => {
|
||||||
|
@ -133,13 +117,7 @@ export class ExecutePostedVaa implements PythGovernanceAction {
|
||||||
return { programId, accounts, data };
|
return { programId, accounts, data };
|
||||||
});
|
});
|
||||||
|
|
||||||
const span =
|
const span = ExecutePostedVaa.layout.encode(instructions, buffer);
|
||||||
offset +
|
return Buffer.concat([headerBuffer, buffer.subarray(0, span)]);
|
||||||
new Vector<InstructionData>(instructionDataLayout, "instructions").encode(
|
|
||||||
instructions,
|
|
||||||
buffer,
|
|
||||||
offset
|
|
||||||
);
|
|
||||||
return buffer.subarray(0, span);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ 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 { PACKET_DATA_SIZE } from "@solana/web3.js";
|
||||||
import { ExecutePostedVaa } from "./ExecutePostedVaa";
|
import { ExecutePostedVaa } from "./ExecutePostedVaa";
|
||||||
|
|
||||||
export interface PythGovernanceAction {
|
export interface PythGovernanceAction {
|
||||||
|
@ -12,6 +13,7 @@ export interface PythGovernanceAction {
|
||||||
encode(): Buffer;
|
encode(): Buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Each of the actions that can be directed to the Executor Module */
|
||||||
export const ExecutorAction = {
|
export const ExecutorAction = {
|
||||||
ExecutePostedVaa: 0,
|
ExecutePostedVaa: 0,
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -25,6 +27,7 @@ export const TargetAction = {
|
||||||
RequestGovernanceDataSourceTransfer: 5,
|
RequestGovernanceDataSourceTransfer: 5,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
/** 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 {
|
||||||
|
@ -48,28 +51,23 @@ export function toActionName(
|
||||||
}
|
}
|
||||||
throw new Error("Invalid header, action doesn't match module");
|
throw new Error("Invalid header, action doesn't match module");
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare type ActionName =
|
export declare type ActionName =
|
||||||
| keyof typeof ExecutorAction
|
| keyof typeof ExecutorAction
|
||||||
| keyof typeof TargetAction;
|
| keyof typeof TargetAction;
|
||||||
|
|
||||||
export type PythGovernanceHeader = {
|
/** Governance header that should be in every Pyth crosschain governance message*/
|
||||||
targetChainId: ChainName;
|
export class PythGovernanceHeader {
|
||||||
action: ActionName;
|
readonly targetChainId: ChainName;
|
||||||
};
|
readonly action: ActionName;
|
||||||
|
static layout: BufferLayout.Structure<
|
||||||
export const MAGIC_NUMBER = 0x4d475450;
|
Readonly<{
|
||||||
export const MODULE_EXECUTOR = 0;
|
magicNumber: number;
|
||||||
export const MODULE_TARGET = 1;
|
module: number;
|
||||||
|
action: number;
|
||||||
export function governanceHeaderLayout(): BufferLayout.Structure<
|
chain: ChainId;
|
||||||
Readonly<{
|
}>
|
||||||
magicNumber: number;
|
> = BufferLayout.struct(
|
||||||
module: number;
|
|
||||||
action: number;
|
|
||||||
chain: ChainId;
|
|
||||||
}>
|
|
||||||
> {
|
|
||||||
return BufferLayout.struct(
|
|
||||||
[
|
[
|
||||||
BufferLayout.u32("magicNumber"),
|
BufferLayout.u32("magicNumber"),
|
||||||
BufferLayout.u8("module"),
|
BufferLayout.u8("module"),
|
||||||
|
@ -78,66 +76,66 @@ export function governanceHeaderLayout(): BufferLayout.Structure<
|
||||||
],
|
],
|
||||||
"header"
|
"header"
|
||||||
);
|
);
|
||||||
}
|
/** Span of the serialized governance header */
|
||||||
|
static span = 8;
|
||||||
|
|
||||||
/** Decode Pyth Governance Header and return undefined if the header is invalid */
|
constructor(targetChainId: ChainName, action: ActionName) {
|
||||||
export function decodeHeader(data: Buffer): PythGovernanceHeader {
|
this.targetChainId = targetChainId;
|
||||||
let deserialized = governanceHeaderLayout().decode(data);
|
this.action = action;
|
||||||
return verifyHeader(deserialized);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function encodeHeader(
|
|
||||||
src: PythGovernanceHeader,
|
|
||||||
buffer: Buffer
|
|
||||||
): number {
|
|
||||||
let module: number;
|
|
||||||
let action: number;
|
|
||||||
if (src.action in ExecutorAction) {
|
|
||||||
module = MODULE_EXECUTOR;
|
|
||||||
action = ExecutorAction[src.action as keyof typeof ExecutorAction];
|
|
||||||
} else {
|
|
||||||
module = MODULE_TARGET;
|
|
||||||
action = TargetAction[src.action as keyof typeof TargetAction];
|
|
||||||
}
|
}
|
||||||
return governanceHeaderLayout().encode(
|
/** Decode Pyth Governance Header */
|
||||||
{
|
static decode(data: Buffer): PythGovernanceHeader {
|
||||||
magicNumber: MAGIC_NUMBER,
|
let deserialized = this.layout.decode(data);
|
||||||
module,
|
if (deserialized.magicNumber !== MAGIC_NUMBER) {
|
||||||
action,
|
throw new Error("Wrong magic number");
|
||||||
chain: toChainId(src.targetChainId),
|
}
|
||||||
},
|
|
||||||
buffer
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function verifyHeader(
|
if (!toChainName(deserialized.chain)) {
|
||||||
deserialized: Readonly<{
|
throw new Error("Chain Id not found");
|
||||||
magicNumber: number;
|
}
|
||||||
module: number;
|
|
||||||
action: number;
|
return new PythGovernanceHeader(
|
||||||
chain: ChainId;
|
toChainName(deserialized.chain),
|
||||||
}>
|
toActionName({
|
||||||
): PythGovernanceHeader {
|
actionId: deserialized.action,
|
||||||
if (deserialized.magicNumber !== MAGIC_NUMBER) {
|
moduleId: deserialized.module,
|
||||||
throw new Error("Wrong magic number");
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!toChainName(deserialized.chain)) {
|
/** Encode Pyth Governance Header */
|
||||||
throw new Error("Chain Id not found");
|
encode(): Buffer {
|
||||||
|
// The code will crash if the payload is actually bigger than PACKET_DATA_SIZE. But PACKET_DATA_SIZE is the maximum transaction size of Solana, so our serialized payload should never be bigger than this anyway
|
||||||
|
const buffer = Buffer.alloc(PACKET_DATA_SIZE);
|
||||||
|
let module: number;
|
||||||
|
let action: number;
|
||||||
|
if (this.action in ExecutorAction) {
|
||||||
|
module = MODULE_EXECUTOR;
|
||||||
|
action = ExecutorAction[this.action as keyof typeof ExecutorAction];
|
||||||
|
} else {
|
||||||
|
module = MODULE_TARGET;
|
||||||
|
action = TargetAction[this.action as keyof typeof TargetAction];
|
||||||
|
}
|
||||||
|
const span = PythGovernanceHeader.layout.encode(
|
||||||
|
{
|
||||||
|
magicNumber: MAGIC_NUMBER,
|
||||||
|
module,
|
||||||
|
action,
|
||||||
|
chain: toChainId(this.targetChainId),
|
||||||
|
},
|
||||||
|
buffer
|
||||||
|
);
|
||||||
|
return buffer.subarray(0, span);
|
||||||
}
|
}
|
||||||
|
|
||||||
let governanceHeader: PythGovernanceHeader = {
|
|
||||||
targetChainId: toChainName(deserialized.chain),
|
|
||||||
action: toActionName({
|
|
||||||
actionId: deserialized.action,
|
|
||||||
moduleId: deserialized.module,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
return governanceHeader;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const MAGIC_NUMBER = 0x4d475450;
|
||||||
|
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 {
|
||||||
const header = decodeHeader(data);
|
const header = PythGovernanceHeader.decode(data);
|
||||||
switch (header.action) {
|
switch (header.action) {
|
||||||
case "ExecutePostedVaa":
|
case "ExecutePostedVaa":
|
||||||
return ExecutePostedVaa.decode(data);
|
return ExecutePostedVaa.decode(data);
|
||||||
|
|
Loading…
Reference in New Issue