[xc-admin] Add encoder for governance messages (#462)
* Add encoder * Cleanup * Update test * Ci * CI * Cleanup * More cleanup
This commit is contained in:
parent
5efa611a97
commit
097943f657
|
@ -0,0 +1,18 @@
|
||||||
|
name: Check Xc Admin
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths: [xc-admin/**]
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths: [xc-admin/**]
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: xc-admin/
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Run xc-admin tests
|
||||||
|
run: |
|
||||||
|
npm ci && npm run test
|
|
@ -4,6 +4,9 @@
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
|
"scripts": {
|
||||||
|
"test": "cd packages/xc-admin-common && npm run test"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"lerna": "^6.3.0"
|
"lerna": "^6.3.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,61 @@
|
||||||
import { PublicKey, SystemProgram } from "@solana/web3.js";
|
import { ChainName } from "@certusone/wormhole-sdk";
|
||||||
import { decodeExecutePostedVaa, decodeHeader } from "..";
|
import {
|
||||||
|
PACKET_DATA_SIZE,
|
||||||
|
PublicKey,
|
||||||
|
SystemProgram,
|
||||||
|
TransactionInstruction,
|
||||||
|
} from "@solana/web3.js";
|
||||||
|
import {
|
||||||
|
ActionName,
|
||||||
|
decodeExecutePostedVaa,
|
||||||
|
decodeHeader,
|
||||||
|
encodeHeader,
|
||||||
|
} from "..";
|
||||||
|
import { encodeExecutePostedVaa } from "../governance_payload/ExecutePostedVaa";
|
||||||
|
|
||||||
test("GovernancePayload", (done) => {
|
test("GovernancePayload ser/de", (done) => {
|
||||||
jest.setTimeout(60000);
|
jest.setTimeout(60000);
|
||||||
|
|
||||||
let governanceHeader = decodeHeader(
|
// Valid header 1
|
||||||
Buffer.from([80, 84, 71, 77, 0, 0, 0, 26, 0, 0, 0, 0])
|
let expectedGovernanceHeader = {
|
||||||
);
|
targetChainId: "pythnet" as ChainName,
|
||||||
|
action: "ExecutePostedVaa" as ActionName,
|
||||||
|
};
|
||||||
|
let buffer = Buffer.alloc(PACKET_DATA_SIZE);
|
||||||
|
let span = encodeHeader(expectedGovernanceHeader, buffer);
|
||||||
|
expect(
|
||||||
|
buffer.subarray(0, span).equals(Buffer.from([80, 84, 71, 77, 0, 0, 0, 26]))
|
||||||
|
).toBeTruthy();
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
governanceHeader = decodeHeader(
|
// Valid header 2
|
||||||
Buffer.from([80, 84, 71, 77, 0, 0, 0, 0, 0, 0, 0, 0])
|
expectedGovernanceHeader = {
|
||||||
);
|
targetChainId: "unset" as ChainName,
|
||||||
|
action: "ExecutePostedVaa" as ActionName,
|
||||||
|
};
|
||||||
|
buffer = Buffer.alloc(PACKET_DATA_SIZE);
|
||||||
|
span = encodeHeader(expectedGovernanceHeader, buffer);
|
||||||
|
expect(
|
||||||
|
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");
|
||||||
|
|
||||||
governanceHeader = decodeHeader(
|
// Valid header 3
|
||||||
Buffer.from([80, 84, 71, 77, 1, 3, 0, 1, 0, 0, 0, 0])
|
expectedGovernanceHeader = {
|
||||||
);
|
targetChainId: "solana" as ChainName,
|
||||||
|
action: "SetFee" as ActionName,
|
||||||
|
};
|
||||||
|
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");
|
||||||
|
|
||||||
|
@ -40,27 +77,49 @@ test("GovernancePayload", (done) => {
|
||||||
);
|
);
|
||||||
expect(governanceHeader).toBeUndefined();
|
expect(governanceHeader).toBeUndefined();
|
||||||
|
|
||||||
// Decode executePostVaa
|
// Decode executePostVaa with empty instructions
|
||||||
let executePostedVaaArgs = decodeExecutePostedVaa(
|
let expectedExecuteVaaArgs = {
|
||||||
Buffer.from([80, 84, 71, 77, 0, 0, 0, 26, 0, 0, 0, 0])
|
targetChainId: "pythnet" as ChainName,
|
||||||
);
|
instructions: [] as TransactionInstruction[],
|
||||||
expect(executePostedVaaArgs?.header.targetChainId).toBe("pythnet");
|
};
|
||||||
expect(executePostedVaaArgs?.header.action).toBe("ExecutePostedVaa");
|
buffer = encodeExecutePostedVaa(expectedExecuteVaaArgs);
|
||||||
|
expect(
|
||||||
|
buffer.equals(Buffer.from([80, 84, 71, 77, 0, 0, 0, 26, 0, 0, 0, 0]))
|
||||||
|
).toBeTruthy();
|
||||||
|
let executePostedVaaArgs = decodeExecutePostedVaa(buffer);
|
||||||
|
expect(executePostedVaaArgs?.targetChainId).toBe("pythnet");
|
||||||
expect(executePostedVaaArgs?.instructions.length).toBe(0);
|
expect(executePostedVaaArgs?.instructions.length).toBe(0);
|
||||||
|
|
||||||
executePostedVaaArgs = decodeExecutePostedVaa(
|
// Decode executePostVaa with one system instruction
|
||||||
Buffer.from([
|
expectedExecuteVaaArgs = {
|
||||||
80, 84, 71, 77, 0, 0, 0, 26, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
targetChainId: "pythnet" as ChainName,
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0,
|
instructions: [
|
||||||
141, 65, 8, 219, 216, 57, 229, 94, 74, 17, 138, 50, 121, 176, 38, 178, 50,
|
SystemProgram.transfer({
|
||||||
229, 210, 103, 232, 253, 133, 66, 14, 47, 228, 224, 162, 147, 232, 251, 1,
|
fromPubkey: new PublicKey(
|
||||||
1, 252, 221, 21, 33, 156, 1, 72, 252, 246, 229, 150, 218, 109, 165, 127,
|
"AWQ18oKzd187aM2oMB4YirBcdgX1FgWfukmqEX91BRES"
|
||||||
11, 165, 252, 140, 6, 121, 57, 204, 91, 119, 165, 106, 241, 234, 131, 75,
|
),
|
||||||
180, 0, 1, 12, 0, 0, 0, 2, 0, 0, 0, 0, 152, 13, 0, 0, 0, 0, 0,
|
toPubkey: new PublicKey("J25GT2knN8V2Wvg9jNrYBuj9SZdsLnU6bK7WCGrL7daj"),
|
||||||
])
|
lamports: 890880,
|
||||||
);
|
}),
|
||||||
expect(executePostedVaaArgs?.header.targetChainId).toBe("pythnet");
|
] as TransactionInstruction[],
|
||||||
expect(executePostedVaaArgs?.header.action).toBe("ExecutePostedVaa");
|
};
|
||||||
|
buffer = encodeExecutePostedVaa(expectedExecuteVaaArgs);
|
||||||
|
expect(
|
||||||
|
buffer.equals(
|
||||||
|
Buffer.from([
|
||||||
|
80, 84, 71, 77, 0, 0, 0, 26, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0,
|
||||||
|
0, 0, 141, 65, 8, 219, 216, 57, 229, 94, 74, 17, 138, 50, 121, 176, 38,
|
||||||
|
178, 50, 229, 210, 103, 232, 253, 133, 66, 14, 47, 228, 224, 162, 147,
|
||||||
|
232, 251, 1, 1, 252, 221, 21, 33, 156, 1, 72, 252, 246, 229, 150, 218,
|
||||||
|
109, 165, 127, 11, 165, 252, 140, 6, 121, 57, 204, 91, 119, 165, 106,
|
||||||
|
241, 234, 131, 75, 180, 0, 1, 12, 0, 0, 0, 2, 0, 0, 0, 0, 152, 13, 0, 0,
|
||||||
|
0, 0, 0,
|
||||||
|
])
|
||||||
|
)
|
||||||
|
).toBeTruthy();
|
||||||
|
executePostedVaaArgs = decodeExecutePostedVaa(buffer);
|
||||||
|
expect(executePostedVaaArgs?.targetChainId).toBe("pythnet");
|
||||||
expect(executePostedVaaArgs?.instructions.length).toBe(1);
|
expect(executePostedVaaArgs?.instructions.length).toBe(1);
|
||||||
expect(
|
expect(
|
||||||
executePostedVaaArgs?.instructions[0].programId.equals(
|
executePostedVaaArgs?.instructions[0].programId.equals(
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
import { ChainId } 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 { governanceHeaderLayout, PythGovernanceHeader, verifyHeader } from ".";
|
import {
|
||||||
|
encodeHeader,
|
||||||
|
governanceHeaderLayout,
|
||||||
|
PythGovernanceHeader,
|
||||||
|
verifyHeader,
|
||||||
|
} from ".";
|
||||||
import { Layout } from "@solana/buffer-layout";
|
import { Layout } from "@solana/buffer-layout";
|
||||||
import {
|
import {
|
||||||
AccountMeta,
|
AccountMeta,
|
||||||
|
PACKET_DATA_SIZE,
|
||||||
PublicKey,
|
PublicKey,
|
||||||
TransactionInstruction,
|
TransactionInstruction,
|
||||||
} from "@solana/web3.js";
|
} from "@solana/web3.js";
|
||||||
|
@ -21,10 +27,10 @@ class Vector<T> extends Layout<T[]> {
|
||||||
return BufferLayout.seq(this.element, length).decode(b, (offset || 0) + 4);
|
return BufferLayout.seq(this.element, length).decode(b, (offset || 0) + 4);
|
||||||
}
|
}
|
||||||
encode(src: T[], b: Uint8Array, offset?: number | undefined): number {
|
encode(src: T[], b: Uint8Array, offset?: number | undefined): number {
|
||||||
return BufferLayout.struct<Readonly<{ length: number; src: T[] }>>([
|
return BufferLayout.struct<Readonly<{ length: number; elements: T[] }>>([
|
||||||
BufferLayout.u32("length"),
|
BufferLayout.u32("length"),
|
||||||
BufferLayout.seq(this.element, src.length, "elements"),
|
BufferLayout.seq(this.element, src.length, "elements"),
|
||||||
]).encode({ length: src.length, src }, b, offset);
|
]).encode({ length: src.length, elements: src }, b, offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
getSpan(b: Buffer, offset?: number): number {
|
getSpan(b: Buffer, offset?: number): number {
|
||||||
|
@ -72,7 +78,7 @@ export const executePostedVaaLayout: BufferLayout.Structure<
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type ExecutePostedVaaArgs = {
|
export type ExecutePostedVaaArgs = {
|
||||||
header: PythGovernanceHeader;
|
targetChainId: ChainName;
|
||||||
instructions: TransactionInstruction[];
|
instructions: TransactionInstruction[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -103,5 +109,36 @@ export function decodeExecutePostedVaa(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return { header, instructions };
|
return { targetChainId: header.targetChainId, instructions };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Encode ExecutePostedVaaArgs */
|
||||||
|
export function encodeExecutePostedVaa(src: ExecutePostedVaaArgs): Buffer {
|
||||||
|
// PACKET_DATA_SIZE is the maximum transactin size of Solana, so our serialized payload will never be bigger than that
|
||||||
|
const buffer = Buffer.alloc(PACKET_DATA_SIZE);
|
||||||
|
const offset = encodeHeader(
|
||||||
|
{ action: "ExecutePostedVaa", targetChainId: src.targetChainId },
|
||||||
|
buffer
|
||||||
|
);
|
||||||
|
let instructions: InstructionData[] = src.instructions.map((ix) => {
|
||||||
|
let programId = ix.programId.toBytes();
|
||||||
|
let accounts: AccountMetadata[] = ix.keys.map((acc) => {
|
||||||
|
return {
|
||||||
|
pubkey: acc.pubkey.toBytes(),
|
||||||
|
isSigner: acc.isSigner ? 1 : 0,
|
||||||
|
isWritable: acc.isWritable ? 1 : 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
let data = [...ix.data];
|
||||||
|
return { programId, accounts, data };
|
||||||
|
});
|
||||||
|
|
||||||
|
const span =
|
||||||
|
offset +
|
||||||
|
new Vector<InstructionData>(instructionDataLayout, "instructions").encode(
|
||||||
|
instructions,
|
||||||
|
buffer,
|
||||||
|
offset
|
||||||
|
);
|
||||||
|
return buffer.subarray(0, span);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,23 @@
|
||||||
import { ChainId, ChainName, toChainName } from "@certusone/wormhole-sdk";
|
import {
|
||||||
|
ChainId,
|
||||||
|
ChainName,
|
||||||
|
toChainId,
|
||||||
|
toChainName,
|
||||||
|
} from "@certusone/wormhole-sdk";
|
||||||
import * as BufferLayout from "@solana/buffer-layout";
|
import * as BufferLayout from "@solana/buffer-layout";
|
||||||
|
|
||||||
export declare const ExecutorAction: {
|
export const ExecutorAction = {
|
||||||
readonly ExecutePostedVaa: 0;
|
ExecutePostedVaa: 0,
|
||||||
};
|
} as const;
|
||||||
|
|
||||||
export declare const TargetAction: {
|
export const TargetAction = {
|
||||||
readonly UpgradeContract: 0;
|
UpgradeContract: 0,
|
||||||
readonly AuthorizeGovernanceDataSourceTransfer: 1;
|
AuthorizeGovernanceDataSourceTransfer: 1,
|
||||||
readonly SetDataSources: 2;
|
SetDataSources: 2,
|
||||||
readonly SetFee: 3;
|
SetFee: 3,
|
||||||
readonly SetValidPeriod: 4;
|
SetValidPeriod: 4,
|
||||||
readonly RequestGovernanceDataSourceTransfer: 5;
|
RequestGovernanceDataSourceTransfer: 5,
|
||||||
};
|
} as const;
|
||||||
|
|
||||||
export function toActionName(
|
export function toActionName(
|
||||||
deserialized: Readonly<{ moduleId: number; actionId: number }>
|
deserialized: Readonly<{ moduleId: number; actionId: number }>
|
||||||
|
@ -75,6 +80,30 @@ export function decodeHeader(data: Buffer): PythGovernanceHeader | undefined {
|
||||||
return verifyHeader(deserialized);
|
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(
|
||||||
|
{
|
||||||
|
magicNumber: MAGIC_NUMBER,
|
||||||
|
module,
|
||||||
|
action,
|
||||||
|
chain: toChainId(src.targetChainId),
|
||||||
|
},
|
||||||
|
buffer
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function verifyHeader(
|
export function verifyHeader(
|
||||||
deserialized: Readonly<{
|
deserialized: Readonly<{
|
||||||
magicNumber: number;
|
magicNumber: number;
|
||||||
|
|
Loading…
Reference in New Issue