Skip to content

Commit 8ef49e6

Browse files
authored
[xc-admin] Refactor wormhole messages as classes (#478)
* Refactor wormhole messages as classes * Type error * Export ExecutePostedVaa * Rename to UnknownGovernanceAction * Improve interface
1 parent fedb92e commit 8ef49e6

File tree

6 files changed

+136
-151
lines changed

6 files changed

+136
-151
lines changed

xc-admin/packages/xc-admin-common/src/__tests__/GovernancePayload.test.ts

Lines changed: 15 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,6 @@
11
import { ChainName } from "@certusone/wormhole-sdk";
2-
import {
3-
PACKET_DATA_SIZE,
4-
PublicKey,
5-
SystemProgram,
6-
TransactionInstruction,
7-
} from "@solana/web3.js";
8-
import {
9-
ActionName,
10-
decodeExecutePostedVaa,
11-
decodeHeader,
12-
encodeHeader,
13-
} from "..";
14-
import { encodeExecutePostedVaa } from "../governance_payload/ExecutePostedVaa";
2+
import { PACKET_DATA_SIZE, PublicKey, SystemProgram } from "@solana/web3.js";
3+
import { ActionName, decodeHeader, encodeHeader, ExecutePostedVaa } from "..";
154

165
test("GovernancePayload ser/de", (done) => {
176
jest.setTimeout(60000);
@@ -75,32 +64,25 @@ test("GovernancePayload ser/de", (done) => {
7564
).toThrow("Invalid header, action doesn't match module");
7665

7766
// Decode executePostVaa with empty instructions
78-
let expectedExecuteVaaArgs = {
79-
targetChainId: "pythnet" as ChainName,
80-
instructions: [] as TransactionInstruction[],
81-
};
82-
buffer = encodeExecutePostedVaa(expectedExecuteVaaArgs);
67+
let expectedExecutePostedVaa = new ExecutePostedVaa("pythnet", []);
68+
buffer = expectedExecutePostedVaa.encode();
8369
expect(
8470
buffer.equals(Buffer.from([80, 84, 71, 77, 0, 0, 0, 26, 0, 0, 0, 0]))
8571
).toBeTruthy();
86-
let executePostedVaaArgs = decodeExecutePostedVaa(buffer);
72+
let executePostedVaaArgs = ExecutePostedVaa.decode(buffer);
8773
expect(executePostedVaaArgs?.targetChainId).toBe("pythnet");
8874
expect(executePostedVaaArgs?.instructions.length).toBe(0);
8975

9076
// Decode executePostVaa with one system instruction
91-
expectedExecuteVaaArgs = {
92-
targetChainId: "pythnet" as ChainName,
93-
instructions: [
94-
SystemProgram.transfer({
95-
fromPubkey: new PublicKey(
96-
"AWQ18oKzd187aM2oMB4YirBcdgX1FgWfukmqEX91BRES"
97-
),
98-
toPubkey: new PublicKey("J25GT2knN8V2Wvg9jNrYBuj9SZdsLnU6bK7WCGrL7daj"),
99-
lamports: 890880,
100-
}),
101-
] as TransactionInstruction[],
102-
};
103-
buffer = encodeExecutePostedVaa(expectedExecuteVaaArgs);
77+
expectedExecutePostedVaa = new ExecutePostedVaa("pythnet", [
78+
SystemProgram.transfer({
79+
fromPubkey: new PublicKey("AWQ18oKzd187aM2oMB4YirBcdgX1FgWfukmqEX91BRES"),
80+
toPubkey: new PublicKey("J25GT2knN8V2Wvg9jNrYBuj9SZdsLnU6bK7WCGrL7daj"),
81+
lamports: 890880,
82+
}),
83+
]);
84+
85+
buffer = expectedExecutePostedVaa.encode();
10486
expect(
10587
buffer.equals(
10688
Buffer.from([
@@ -115,7 +97,7 @@ test("GovernancePayload ser/de", (done) => {
11597
])
11698
)
11799
).toBeTruthy();
118-
executePostedVaaArgs = decodeExecutePostedVaa(buffer);
100+
executePostedVaaArgs = ExecutePostedVaa.decode(buffer);
119101
expect(executePostedVaaArgs?.targetChainId).toBe("pythnet");
120102
expect(executePostedVaaArgs?.instructions.length).toBe(1);
121103
expect(

xc-admin/packages/xc-admin-common/src/__tests__/WormholeMultisigInstruction.test.ts

Lines changed: 41 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { ChainName } from "@certusone/wormhole-sdk";
21
import { createWormholeProgramInterface } from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole";
32
import { AnchorProvider, Wallet } from "@project-serum/anchor";
43
import {
@@ -16,11 +15,8 @@ import {
1615
MultisigInstructionProgram,
1716
MultisigParser,
1817
WORMHOLE_ADDRESS,
18+
ExecutePostedVaa,
1919
} from "..";
20-
import {
21-
encodeExecutePostedVaa,
22-
ExecutePostedVaaArgs,
23-
} from "../governance_payload/ExecutePostedVaa";
2420
import { WormholeMultisigInstruction } from "../multisig_transaction/WormholeMultisigInstruction";
2521

2622
test("Wormhole multisig instruction parse: send message without governance payload", (done) => {
@@ -184,19 +180,16 @@ test("Wormhole multisig instruction parse: send message with governance payload"
184180
);
185181
const parser = MultisigParser.fromCluster(cluster);
186182

187-
const executePostedVaaArgs: ExecutePostedVaaArgs = {
188-
targetChainId: "pythnet" as ChainName,
189-
instructions: [
190-
SystemProgram.transfer({
191-
fromPubkey: PublicKey.unique(),
192-
toPubkey: PublicKey.unique(),
193-
lamports: 890880,
194-
}),
195-
],
196-
};
183+
const executePostedVaa: ExecutePostedVaa = new ExecutePostedVaa("pythnet", [
184+
SystemProgram.transfer({
185+
fromPubkey: PublicKey.unique(),
186+
toPubkey: PublicKey.unique(),
187+
lamports: 890880,
188+
}),
189+
]);
197190

198191
wormholeProgram.methods
199-
.postMessage(0, encodeExecutePostedVaa(executePostedVaaArgs), 0)
192+
.postMessage(0, executePostedVaa.encode(), 0)
200193
.accounts({
201194
bridge: PublicKey.unique(),
202195
message: PublicKey.unique(),
@@ -319,45 +312,47 @@ test("Wormhole multisig instruction parse: send message with governance payload"
319312

320313
expect(parsedInstruction.args.nonce).toBe(0);
321314
expect(
322-
parsedInstruction.args.payload.equals(
323-
encodeExecutePostedVaa(executePostedVaaArgs)
324-
)
315+
parsedInstruction.args.payload.equals(executePostedVaa.encode())
325316
);
326317
expect(parsedInstruction.args.consistencyLevel).toBe(0);
327318

328-
expect(parsedInstruction.args.governanceName).toBe("ExecutePostedVaa");
329-
330-
expect(parsedInstruction.args.governanceArgs.targetChainId).toBe(
331-
"pythnet"
332-
);
333-
334-
(
335-
parsedInstruction.args.governanceArgs
336-
.instructions as TransactionInstruction[]
337-
).forEach((instruction, i) => {
338-
expect(
339-
instruction.programId.equals(
340-
executePostedVaaArgs.instructions[i].programId
341-
)
319+
if (
320+
parsedInstruction.args.governanceAction instanceof ExecutePostedVaa
321+
) {
322+
expect(parsedInstruction.args.governanceAction.targetChainId).toBe(
323+
"pythnet"
342324
);
343-
expect(
344-
instruction.data.equals(executePostedVaaArgs.instructions[i].data)
345-
);
346-
instruction.keys.forEach((account, j) => {
325+
326+
(
327+
parsedInstruction.args.governanceAction
328+
.instructions as TransactionInstruction[]
329+
).forEach((instruction, i) => {
347330
expect(
348-
account.pubkey.equals(
349-
executePostedVaaArgs.instructions[i].keys[j].pubkey
331+
instruction.programId.equals(
332+
executePostedVaa.instructions[i].programId
350333
)
351-
).toBeTruthy();
352-
expect(account.isSigner).toBe(
353-
executePostedVaaArgs.instructions[i].keys[j].isSigner
354334
);
355-
expect(account.isWritable).toBe(
356-
executePostedVaaArgs.instructions[i].keys[j].isWritable
335+
expect(
336+
instruction.data.equals(executePostedVaa.instructions[i].data)
357337
);
338+
instruction.keys.forEach((account, j) => {
339+
expect(
340+
account.pubkey.equals(
341+
executePostedVaa.instructions[i].keys[j].pubkey
342+
)
343+
).toBeTruthy();
344+
expect(account.isSigner).toBe(
345+
executePostedVaa.instructions[i].keys[j].isSigner
346+
);
347+
expect(account.isWritable).toBe(
348+
executePostedVaa.instructions[i].keys[j].isWritable
349+
);
350+
});
358351
});
359-
});
360-
done();
352+
done();
353+
} else {
354+
done("Not instance of ExecutePostedVaa");
355+
}
361356
} else {
362357
done("Not instance of WormholeInstruction");
363358
}

xc-admin/packages/xc-admin-common/src/governance_payload/ExecutePostedVaa.ts

Lines changed: 58 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as BufferLayout from "@solana/buffer-layout";
33
import {
44
encodeHeader,
55
governanceHeaderLayout,
6-
PythGovernanceHeader,
6+
PythGovernanceAction,
77
verifyHeader,
88
} from ".";
99
import { Layout } from "@solana/buffer-layout";
@@ -77,62 +77,69 @@ export const executePostedVaaLayout: BufferLayout.Structure<
7777
new Vector<InstructionData>(instructionDataLayout, "instructions"),
7878
]);
7979

80-
export type ExecutePostedVaaArgs = {
81-
targetChainId: ChainName;
82-
instructions: TransactionInstruction[];
83-
};
80+
export class ExecutePostedVaa implements PythGovernanceAction {
81+
readonly targetChainId: ChainName;
82+
readonly instructions: TransactionInstruction[];
83+
84+
constructor(
85+
targetChainId: ChainName,
86+
instructions: TransactionInstruction[]
87+
) {
88+
this.targetChainId = targetChainId;
89+
this.instructions = instructions;
90+
}
8491

85-
/** Decode ExecutePostedVaaArgs and return undefined if it failed */
86-
export function decodeExecutePostedVaa(data: Buffer): ExecutePostedVaaArgs {
87-
let deserialized = executePostedVaaLayout.decode(data);
92+
/** Decode ExecutePostedVaaArgs */
93+
static decode(data: Buffer): ExecutePostedVaa {
94+
let deserialized = executePostedVaaLayout.decode(data);
8895

89-
let header = verifyHeader(deserialized.header);
96+
let header = verifyHeader(deserialized.header);
97+
98+
let instructions: TransactionInstruction[] = deserialized.instructions.map(
99+
(ix) => {
100+
let programId: PublicKey = new PublicKey(ix.programId);
101+
let keys: AccountMeta[] = ix.accounts.map((acc) => {
102+
return {
103+
pubkey: new PublicKey(acc.pubkey),
104+
isSigner: Boolean(acc.isSigner),
105+
isWritable: Boolean(acc.isWritable),
106+
};
107+
});
108+
let data: Buffer = Buffer.from(ix.data);
109+
return { programId, keys, data };
110+
}
111+
);
112+
return new ExecutePostedVaa(header.targetChainId, instructions);
113+
}
90114

91-
let instructions: TransactionInstruction[] = deserialized.instructions.map(
92-
(ix) => {
93-
let programId: PublicKey = new PublicKey(ix.programId);
94-
let keys: AccountMeta[] = ix.accounts.map((acc) => {
115+
/** Encode ExecutePostedVaaArgs */
116+
encode(): Buffer {
117+
// PACKET_DATA_SIZE is the maximum transaction size of Solana, so our serialized payload will never be bigger than that
118+
const buffer = Buffer.alloc(PACKET_DATA_SIZE);
119+
const offset = encodeHeader(
120+
{ action: "ExecutePostedVaa", targetChainId: this.targetChainId },
121+
buffer
122+
);
123+
let instructions: InstructionData[] = this.instructions.map((ix) => {
124+
let programId = ix.programId.toBytes();
125+
let accounts: AccountMetadata[] = ix.keys.map((acc) => {
95126
return {
96-
pubkey: new PublicKey(acc.pubkey),
97-
isSigner: Boolean(acc.isSigner),
98-
isWritable: Boolean(acc.isWritable),
127+
pubkey: acc.pubkey.toBytes(),
128+
isSigner: acc.isSigner ? 1 : 0,
129+
isWritable: acc.isWritable ? 1 : 0,
99130
};
100131
});
101-
let data: Buffer = Buffer.from(ix.data);
102-
return { programId, keys, data };
103-
}
104-
);
105-
106-
return { targetChainId: header.targetChainId, instructions };
107-
}
108-
109-
/** Encode ExecutePostedVaaArgs */
110-
export function encodeExecutePostedVaa(src: ExecutePostedVaaArgs): Buffer {
111-
// PACKET_DATA_SIZE is the maximum transactin size of Solana, so our serialized payload will never be bigger than that
112-
const buffer = Buffer.alloc(PACKET_DATA_SIZE);
113-
const offset = encodeHeader(
114-
{ action: "ExecutePostedVaa", targetChainId: src.targetChainId },
115-
buffer
116-
);
117-
let instructions: InstructionData[] = src.instructions.map((ix) => {
118-
let programId = ix.programId.toBytes();
119-
let accounts: AccountMetadata[] = ix.keys.map((acc) => {
120-
return {
121-
pubkey: acc.pubkey.toBytes(),
122-
isSigner: acc.isSigner ? 1 : 0,
123-
isWritable: acc.isWritable ? 1 : 0,
124-
};
132+
let data = [...ix.data];
133+
return { programId, accounts, data };
125134
});
126-
let data = [...ix.data];
127-
return { programId, accounts, data };
128-
});
129135

130-
const span =
131-
offset +
132-
new Vector<InstructionData>(instructionDataLayout, "instructions").encode(
133-
instructions,
134-
buffer,
135-
offset
136-
);
137-
return buffer.subarray(0, span);
136+
const span =
137+
offset +
138+
new Vector<InstructionData>(instructionDataLayout, "instructions").encode(
139+
instructions,
140+
buffer,
141+
offset
142+
);
143+
return buffer.subarray(0, span);
144+
}
138145
}

xc-admin/packages/xc-admin-common/src/governance_payload/index.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ import {
55
toChainName,
66
} from "@certusone/wormhole-sdk";
77
import * as BufferLayout from "@solana/buffer-layout";
8-
import {
9-
decodeExecutePostedVaa,
10-
ExecutePostedVaaArgs,
11-
} from "./ExecutePostedVaa";
8+
import { ExecutePostedVaa } from "./ExecutePostedVaa";
9+
10+
export interface PythGovernanceAction {
11+
readonly targetChainId: ChainName;
12+
encode(): Buffer;
13+
}
1214

1315
export const ExecutorAction = {
1416
ExecutePostedVaa: 0,
@@ -134,17 +136,14 @@ export function verifyHeader(
134136
return governanceHeader;
135137
}
136138

137-
export function decodeGovernancePayload(data: Buffer): {
138-
name: string;
139-
args: ExecutePostedVaaArgs;
140-
} {
139+
export function decodeGovernancePayload(data: Buffer): PythGovernanceAction {
141140
const header = decodeHeader(data);
142141
switch (header.action) {
143142
case "ExecutePostedVaa":
144-
return { name: "ExecutePostedVaa", args: decodeExecutePostedVaa(data) };
143+
return ExecutePostedVaa.decode(data);
145144
default:
146145
throw "Not supported";
147146
}
148147
}
149148

150-
export { decodeExecutePostedVaa } from "./ExecutePostedVaa";
149+
export { ExecutePostedVaa } from "./ExecutePostedVaa";

xc-admin/packages/xc-admin-common/src/multisig_transaction/WormholeMultisigInstruction.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import { WormholeInstructionCoder } from "@certusone/wormhole-sdk/lib/cjs/solana
33
import { getPythClusterApiUrl } from "@pythnetwork/client/lib/cluster";
44
import { Connection, TransactionInstruction } from "@solana/web3.js";
55
import { MultisigInstruction, MultisigInstructionProgram } from ".";
6-
import { decodeGovernancePayload } from "../governance_payload";
6+
import {
7+
decodeGovernancePayload,
8+
PythGovernanceAction,
9+
} from "../governance_payload";
710
import { AnchorAccounts, resolveAccountNames } from "./anchor";
811

912
export class WormholeMultisigInstruction implements MultisigInstruction {
@@ -47,12 +50,12 @@ export class WormholeMultisigInstruction implements MultisigInstruction {
4750

4851
if (result.name === "postMessage") {
4952
try {
50-
const decoded = decodeGovernancePayload(result.args.payload);
51-
result.args.governanceName = decoded.name;
52-
result.args.governanceArgs = decoded.args;
53+
const decoded: PythGovernanceAction = decodeGovernancePayload(
54+
result.args.payload
55+
);
56+
result.args.governanceAction = decoded;
5357
} catch {
54-
result.args.governanceName = "Unrecognized governance message";
55-
result.args.governanceArgs = {};
58+
result.args.governanceAction = {};
5659
}
5760
}
5861
return result;

0 commit comments

Comments
 (0)