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": {
|
||||
"url": "https://github.com/pyth-network/pyth-crosschain/issues"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"format": "prettier --write \"src/**/*.ts\"",
|
||||
"test": "jest"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"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,
|
||||
"noErrorTruncation": true
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["src/__tests__/"]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue