Xc admin/add wormhole message header deser (#461)
* Checkpoint * Checkpoint * Fix tests * Revert changes to remote executor * Fix precommit * Add comments * Solve typo * MetaData -> Metadata * Address feedback * Fix imports
This commit is contained in:
parent
e12567c544
commit
5efa611a97
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,5 @@
|
||||||
|
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||||
|
module.exports = {
|
||||||
|
preset: "ts-jest",
|
||||||
|
testEnvironment: "node",
|
||||||
|
};
|
|
@ -13,7 +13,25 @@
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/pyth-network/pyth-crosschain/issues"
|
"url": "https://github.com/pyth-network/pyth-crosschain/issues"
|
||||||
},
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"format": "prettier --write \"src/**/*.ts\"",
|
||||||
|
"test": "jest"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@certusone/wormhole-sdk": "^0.9.8",
|
||||||
|
"@solana/buffer-layout": "^4.0.1",
|
||||||
|
"@solana/web3.js": "^1.73.0",
|
||||||
|
"@sqds/mesh": "^1.0.6",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"typescript": "^4.9.4"
|
"typescript": "^4.9.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bn.js": "^5.1.1",
|
||||||
|
"@types/jest": "^29.2.5",
|
||||||
|
"@types/lodash": "^4.14.191",
|
||||||
|
"jest": "^29.3.1",
|
||||||
|
"prettier": "^2.8.1",
|
||||||
|
"ts-jest": "^29.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
import { PublicKey, SystemProgram } from "@solana/web3.js";
|
||||||
|
import { decodeExecutePostedVaa, decodeHeader } from "..";
|
||||||
|
|
||||||
|
test("GovernancePayload", (done) => {
|
||||||
|
jest.setTimeout(60000);
|
||||||
|
|
||||||
|
let governanceHeader = decodeHeader(
|
||||||
|
Buffer.from([80, 84, 71, 77, 0, 0, 0, 26, 0, 0, 0, 0])
|
||||||
|
);
|
||||||
|
expect(governanceHeader?.targetChainId).toBe("pythnet");
|
||||||
|
expect(governanceHeader?.action).toBe("ExecutePostedVaa");
|
||||||
|
|
||||||
|
governanceHeader = decodeHeader(
|
||||||
|
Buffer.from([80, 84, 71, 77, 0, 0, 0, 0, 0, 0, 0, 0])
|
||||||
|
);
|
||||||
|
expect(governanceHeader?.targetChainId).toBe("unset");
|
||||||
|
expect(governanceHeader?.action).toBe("ExecutePostedVaa");
|
||||||
|
|
||||||
|
governanceHeader = decodeHeader(
|
||||||
|
Buffer.from([80, 84, 71, 77, 1, 3, 0, 1, 0, 0, 0, 0])
|
||||||
|
);
|
||||||
|
expect(governanceHeader?.targetChainId).toBe("solana");
|
||||||
|
expect(governanceHeader?.action).toBe("SetFee");
|
||||||
|
|
||||||
|
// Wrong magic number
|
||||||
|
governanceHeader = decodeHeader(
|
||||||
|
Buffer.from([0, 0, 0, 0, 0, 0, 0, 26, 0, 0, 0, 0])
|
||||||
|
);
|
||||||
|
expect(governanceHeader).toBeUndefined();
|
||||||
|
|
||||||
|
// Wrong chain
|
||||||
|
governanceHeader = decodeHeader(
|
||||||
|
Buffer.from([80, 84, 71, 77, 0, 0, 255, 255, 0, 0, 0, 0])
|
||||||
|
);
|
||||||
|
expect(governanceHeader).toBeUndefined();
|
||||||
|
|
||||||
|
// Wrong module/action combination
|
||||||
|
governanceHeader = decodeHeader(
|
||||||
|
Buffer.from([80, 84, 71, 77, 0, 1, 0, 26, 0, 0, 0, 0])
|
||||||
|
);
|
||||||
|
expect(governanceHeader).toBeUndefined();
|
||||||
|
|
||||||
|
// Decode executePostVaa
|
||||||
|
let executePostedVaaArgs = decodeExecutePostedVaa(
|
||||||
|
Buffer.from([80, 84, 71, 77, 0, 0, 0, 26, 0, 0, 0, 0])
|
||||||
|
);
|
||||||
|
expect(executePostedVaaArgs?.header.targetChainId).toBe("pythnet");
|
||||||
|
expect(executePostedVaaArgs?.header.action).toBe("ExecutePostedVaa");
|
||||||
|
expect(executePostedVaaArgs?.instructions.length).toBe(0);
|
||||||
|
|
||||||
|
executePostedVaaArgs = decodeExecutePostedVaa(
|
||||||
|
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,
|
||||||
|
])
|
||||||
|
);
|
||||||
|
expect(executePostedVaaArgs?.header.targetChainId).toBe("pythnet");
|
||||||
|
expect(executePostedVaaArgs?.header.action).toBe("ExecutePostedVaa");
|
||||||
|
expect(executePostedVaaArgs?.instructions.length).toBe(1);
|
||||||
|
expect(
|
||||||
|
executePostedVaaArgs?.instructions[0].programId.equals(
|
||||||
|
SystemProgram.programId
|
||||||
|
)
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
executePostedVaaArgs?.instructions[0].keys[0].pubkey.equals(
|
||||||
|
new PublicKey("AWQ18oKzd187aM2oMB4YirBcdgX1FgWfukmqEX91BRES")
|
||||||
|
)
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(executePostedVaaArgs?.instructions[0].keys[0].isSigner).toBeTruthy();
|
||||||
|
expect(executePostedVaaArgs?.instructions[0].keys[0].isWritable).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
executePostedVaaArgs?.instructions[0].keys[1].pubkey.equals(
|
||||||
|
new PublicKey("J25GT2knN8V2Wvg9jNrYBuj9SZdsLnU6bK7WCGrL7daj")
|
||||||
|
)
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(!executePostedVaaArgs?.instructions[0].keys[1].isSigner).toBeTruthy();
|
||||||
|
expect(executePostedVaaArgs?.instructions[0].keys[1].isWritable).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
executePostedVaaArgs?.instructions[0].data.equals(
|
||||||
|
Buffer.from([2, 0, 0, 0, 0, 152, 13, 0, 0, 0, 0, 0])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
|
@ -0,0 +1,107 @@
|
||||||
|
import { ChainId } from "@certusone/wormhole-sdk";
|
||||||
|
import * as BufferLayout from "@solana/buffer-layout";
|
||||||
|
import { governanceHeaderLayout, PythGovernanceHeader, verifyHeader } from ".";
|
||||||
|
import { Layout } from "@solana/buffer-layout";
|
||||||
|
import {
|
||||||
|
AccountMeta,
|
||||||
|
PublicKey,
|
||||||
|
TransactionInstruction,
|
||||||
|
} from "@solana/web3.js";
|
||||||
|
|
||||||
|
class Vector<T> extends Layout<T[]> {
|
||||||
|
private element: Layout<T>;
|
||||||
|
|
||||||
|
constructor(element: Layout<T>, property?: string) {
|
||||||
|
super(-1, property);
|
||||||
|
this.element = element;
|
||||||
|
}
|
||||||
|
|
||||||
|
decode(b: Uint8Array, offset?: number | undefined): T[] {
|
||||||
|
const length = BufferLayout.u32().decode(b, offset);
|
||||||
|
return BufferLayout.seq(this.element, length).decode(b, (offset || 0) + 4);
|
||||||
|
}
|
||||||
|
encode(src: T[], b: Uint8Array, offset?: number | undefined): number {
|
||||||
|
return BufferLayout.struct<Readonly<{ length: number; src: T[] }>>([
|
||||||
|
BufferLayout.u32("length"),
|
||||||
|
BufferLayout.seq(this.element, src.length, "elements"),
|
||||||
|
]).encode({ length: src.length, src }, b, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSpan(b: Buffer, offset?: number): number {
|
||||||
|
const length = BufferLayout.u32().decode(b, offset);
|
||||||
|
return 4 + this.element.span * length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InstructionData = {
|
||||||
|
programId: Uint8Array;
|
||||||
|
accounts: AccountMetadata[];
|
||||||
|
data: number[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AccountMetadata = {
|
||||||
|
pubkey: Uint8Array;
|
||||||
|
isSigner: number;
|
||||||
|
isWritable: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const accountMetaLayout = BufferLayout.struct<AccountMetadata>([
|
||||||
|
BufferLayout.blob(32, "pubkey"),
|
||||||
|
BufferLayout.u8("isSigner"),
|
||||||
|
BufferLayout.u8("isWritable"),
|
||||||
|
]);
|
||||||
|
export const instructionDataLayout = BufferLayout.struct<InstructionData>([
|
||||||
|
BufferLayout.blob(32, "programId"),
|
||||||
|
new Vector<AccountMetadata>(accountMetaLayout, "accounts"),
|
||||||
|
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 type ExecutePostedVaaArgs = {
|
||||||
|
header: PythGovernanceHeader;
|
||||||
|
instructions: TransactionInstruction[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Decode ExecutePostedVaaArgs and return undefined if it failed */
|
||||||
|
export function decodeExecutePostedVaa(
|
||||||
|
data: Buffer
|
||||||
|
): ExecutePostedVaaArgs | undefined {
|
||||||
|
let deserialized = executePostedVaaLayout.decode(data);
|
||||||
|
|
||||||
|
let header = verifyHeader(deserialized.header);
|
||||||
|
|
||||||
|
if (!header) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return { header, instructions };
|
||||||
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
import { ChainId, ChainName, toChainName } from "@certusone/wormhole-sdk";
|
||||||
|
import * as BufferLayout from "@solana/buffer-layout";
|
||||||
|
|
||||||
|
export declare const ExecutorAction: {
|
||||||
|
readonly ExecutePostedVaa: 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
export declare const TargetAction: {
|
||||||
|
readonly UpgradeContract: 0;
|
||||||
|
readonly AuthorizeGovernanceDataSourceTransfer: 1;
|
||||||
|
readonly SetDataSources: 2;
|
||||||
|
readonly SetFee: 3;
|
||||||
|
readonly SetValidPeriod: 4;
|
||||||
|
readonly RequestGovernanceDataSourceTransfer: 5;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function toActionName(
|
||||||
|
deserialized: Readonly<{ moduleId: number; actionId: number }>
|
||||||
|
): ActionName {
|
||||||
|
if (deserialized.moduleId == MODULE_EXECUTOR && deserialized.actionId == 0) {
|
||||||
|
return "ExecutePostedVaa";
|
||||||
|
} else if (deserialized.moduleId == MODULE_TARGET) {
|
||||||
|
switch (deserialized.actionId) {
|
||||||
|
case 0:
|
||||||
|
return "UpgradeContract";
|
||||||
|
case 1:
|
||||||
|
return "AuthorizeGovernanceDataSourceTransfer";
|
||||||
|
case 2:
|
||||||
|
return "SetDataSources";
|
||||||
|
case 3:
|
||||||
|
return "SetFee";
|
||||||
|
case 4:
|
||||||
|
return "SetValidPeriod";
|
||||||
|
case 5:
|
||||||
|
return "RequestGovernanceDataSourceTransfer";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error("Invalid header, action doesn't match module");
|
||||||
|
}
|
||||||
|
export declare type ActionName =
|
||||||
|
| keyof typeof ExecutorAction
|
||||||
|
| keyof typeof TargetAction;
|
||||||
|
|
||||||
|
export type PythGovernanceHeader = {
|
||||||
|
targetChainId: ChainName;
|
||||||
|
action: ActionName;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MAGIC_NUMBER = 0x4d475450;
|
||||||
|
export const MODULE_EXECUTOR = 0;
|
||||||
|
export const MODULE_TARGET = 1;
|
||||||
|
|
||||||
|
export function governanceHeaderLayout(): BufferLayout.Structure<
|
||||||
|
Readonly<{
|
||||||
|
magicNumber: number;
|
||||||
|
module: number;
|
||||||
|
action: number;
|
||||||
|
chain: ChainId;
|
||||||
|
}>
|
||||||
|
> {
|
||||||
|
return BufferLayout.struct(
|
||||||
|
[
|
||||||
|
BufferLayout.u32("magicNumber"),
|
||||||
|
BufferLayout.u8("module"),
|
||||||
|
BufferLayout.u8("action"),
|
||||||
|
BufferLayout.u16be("chain"),
|
||||||
|
],
|
||||||
|
"header"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Decode Pyth Governance Header and return undefined if the header is invalid */
|
||||||
|
export function decodeHeader(data: Buffer): PythGovernanceHeader | undefined {
|
||||||
|
let deserialized = governanceHeaderLayout().decode(data);
|
||||||
|
return verifyHeader(deserialized);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function verifyHeader(
|
||||||
|
deserialized: Readonly<{
|
||||||
|
magicNumber: number;
|
||||||
|
module: number;
|
||||||
|
action: number;
|
||||||
|
chain: ChainId;
|
||||||
|
}>
|
||||||
|
) {
|
||||||
|
if (deserialized.magicNumber !== MAGIC_NUMBER) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!toChainName(deserialized.chain)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let governanceHeader: PythGovernanceHeader = {
|
||||||
|
targetChainId: toChainName(deserialized.chain),
|
||||||
|
action: toActionName({
|
||||||
|
actionId: deserialized.action,
|
||||||
|
moduleId: deserialized.module,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
return governanceHeader;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { decodeExecutePostedVaa } from "./ExecutePostedVaa";
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./multisig";
|
||||||
|
export * from "./governance_payload";
|
|
@ -0,0 +1,99 @@
|
||||||
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
import Squads, {
|
||||||
|
DEFAULT_MULTISIG_PROGRAM_ID,
|
||||||
|
getIxPDA,
|
||||||
|
getTxPDA,
|
||||||
|
} from "@sqds/mesh";
|
||||||
|
import { InstructionAccount, TransactionAccount } from "@sqds/mesh/lib/types";
|
||||||
|
import BN from "bn.js";
|
||||||
|
import lodash from "lodash";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all active proposals for vault `vault` using Squads client `squad`
|
||||||
|
* @param squad Squads client
|
||||||
|
* @param vault vault public key. It needs to exist in the instance of squads that `squad` is targeting
|
||||||
|
* @param offset (optional) ignore all proposals with `proposal_index < offset`
|
||||||
|
* @returns All the proposal accounts as `TransactionAccount`
|
||||||
|
*/
|
||||||
|
export async function getActiveProposals(
|
||||||
|
squad: Squads,
|
||||||
|
vault: PublicKey,
|
||||||
|
offset: number = 1
|
||||||
|
): Promise<TransactionAccount[]> {
|
||||||
|
const msAccount = await squad.getMultisig(vault);
|
||||||
|
let txKeys = lodash
|
||||||
|
.range(offset, msAccount.transactionIndex + 1)
|
||||||
|
.map((i) => getTxPDA(vault, new BN(i), DEFAULT_MULTISIG_PROGRAM_ID)[0]);
|
||||||
|
let msTransactions = await squad.getTransactions(txKeys);
|
||||||
|
return msTransactions
|
||||||
|
.filter(
|
||||||
|
(x: TransactionAccount | null): x is TransactionAccount => x != null
|
||||||
|
)
|
||||||
|
.filter((x) => lodash.isEqual(x.status, { active: {} }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all the instructions for many proposals in one RPC call
|
||||||
|
* @param squad Squads client
|
||||||
|
* @param txAccounts transaction (proposal) accounts
|
||||||
|
* @returns `InstructionAccount[][]`, `result[0]` is the array of all the instructions from proposal 0
|
||||||
|
*/
|
||||||
|
export async function getManyProposalsInstructions(
|
||||||
|
squad: Squads,
|
||||||
|
txAccounts: TransactionAccount[]
|
||||||
|
): Promise<InstructionAccount[][]> {
|
||||||
|
let allIxsKeys = [];
|
||||||
|
let ownerTransaction = [];
|
||||||
|
for (let [index, txAccount] of txAccounts.entries()) {
|
||||||
|
let ixKeys = lodash
|
||||||
|
.range(1, txAccount.instructionIndex + 1)
|
||||||
|
.map(
|
||||||
|
(i) =>
|
||||||
|
getIxPDA(
|
||||||
|
txAccount.publicKey,
|
||||||
|
new BN(i),
|
||||||
|
DEFAULT_MULTISIG_PROGRAM_ID
|
||||||
|
)[0]
|
||||||
|
);
|
||||||
|
for (let ixKey of ixKeys) {
|
||||||
|
allIxsKeys.push(ixKey);
|
||||||
|
ownerTransaction.push(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let allTxIxsAccounts = await squad.getInstructions(allIxsKeys);
|
||||||
|
let ixAccountsByTx: InstructionAccount[][] = Array.from(
|
||||||
|
Array(txAccounts.length),
|
||||||
|
() => []
|
||||||
|
);
|
||||||
|
|
||||||
|
for (let i = 0; i < allTxIxsAccounts.length; i++) {
|
||||||
|
const toAdd = allTxIxsAccounts[i];
|
||||||
|
if (toAdd) {
|
||||||
|
ixAccountsByTx[ownerTransaction[i]].push(toAdd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ixAccountsByTx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all the instructions for one proposal
|
||||||
|
* @param squad Squads client
|
||||||
|
* @param txAccount transaction (proposal) account
|
||||||
|
* @returns All the instructions of the proposal
|
||||||
|
*/
|
||||||
|
export async function getProposalInstructions(
|
||||||
|
squad: Squads,
|
||||||
|
txAccount: TransactionAccount
|
||||||
|
): Promise<InstructionAccount[]> {
|
||||||
|
let ixKeys = lodash
|
||||||
|
.range(1, txAccount.instructionIndex + 1)
|
||||||
|
.map(
|
||||||
|
(i) =>
|
||||||
|
getIxPDA(txAccount.publicKey, new BN(i), DEFAULT_MULTISIG_PROGRAM_ID)[0]
|
||||||
|
);
|
||||||
|
let txIxs = await squad.getInstructions(ixKeys);
|
||||||
|
return txIxs.filter(
|
||||||
|
(x: InstructionAccount | null): x is InstructionAccount => x != null
|
||||||
|
);
|
||||||
|
}
|
|
@ -11,5 +11,6 @@
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"noErrorTruncation": true
|
"noErrorTruncation": true
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts"]
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["src/__tests__/"]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue