Skip to content

Commit ebf8dd6

Browse files
authored
Merge pull request #2731 from pyth-network/lazer-governance
feat: support lazer governance
2 parents 06f6f2e + 5e52280 commit ebf8dd6

30 files changed

+38169
-319
lines changed

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,4 @@ target_chains/ton/sdk/js
3838
contract_manager
3939
lazer/contracts/solana
4040
lazer/sdk/js
41+
lazer/state_sdk/js

governance/xc_admin/packages/proposer_server/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,10 @@ app.post("/api/propose", async (req: Request, res: Response) => {
9090
res.status(200).json({ proposalPubkey: proposalPubkey });
9191
} catch (error) {
9292
if (error instanceof Error) {
93+
console.error(error);
9394
res.status(500).json(error.message);
9495
} else {
96+
console.error(error);
9597
res.status(500).json("An unknown error occurred");
9698
}
9799
}

governance/xc_admin/packages/xc_admin_common/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"@pythnetwork/pyth-lazer-sdk": "workspace:*",
3535
"@pythnetwork/pyth-solana-receiver": "workspace:*",
3636
"@pythnetwork/solana-utils": "workspace:*",
37+
"@pythnetwork/pyth-lazer-state-sdk": "workspace:*",
3738
"@solana/buffer-layout": "^4.0.1",
3839
"@solana/web3.js": "^1.73.0",
3940
"@sqds/mesh": "^1.0.6",

governance/xc_admin/packages/xc_admin_common/src/chains.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,10 @@ export const RECEIVER_CHAINS = {
245245
worldchain_testnet: 50123,
246246
mezo_testnet: 50124,
247247
hemi_testnet: 50125,
248+
249+
// Lazer
250+
lazer_production: 10000,
251+
lazer_staging: 10001,
248252
};
249253

250254
// If there is any overlapping value the receiver chain will replace the wormhole
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { PythGovernanceActionImpl } from "./PythGovernanceAction";
2+
import * as BufferLayout from "@solana/buffer-layout";
3+
import { ChainName } from "../chains";
4+
import { pyth_lazer_transaction } from "@pythnetwork/pyth-lazer-state-sdk/governance";
5+
6+
/** Executes a Lazer governance instruction with the specified directives */
7+
export class LazerExecute extends PythGovernanceActionImpl {
8+
static layout: BufferLayout.Structure<
9+
Readonly<{
10+
governanceInstruction: Uint8Array;
11+
}>
12+
> = BufferLayout.struct([
13+
BufferLayout.blob(new BufferLayout.GreedyCount(), "governanceInstruction"),
14+
]);
15+
16+
constructor(
17+
targetChainId: ChainName,
18+
readonly directives: pyth_lazer_transaction.IGovernanceDirective[],
19+
readonly minExecutionTimestamp?: Date,
20+
readonly maxExecutionTimestamp?: Date,
21+
readonly governanceSequenceNo?: number,
22+
) {
23+
super(targetChainId, "LazerExecute");
24+
}
25+
26+
static decode(data: Buffer): LazerExecute | undefined {
27+
const decoded = PythGovernanceActionImpl.decodeWithPayload(
28+
data,
29+
"LazerExecute",
30+
this.layout,
31+
);
32+
if (!decoded) return undefined;
33+
34+
try {
35+
// Decode the protobuf GovernanceInstruction
36+
const governanceInstruction =
37+
pyth_lazer_transaction.GovernanceInstruction.decode(
38+
decoded[1].governanceInstruction,
39+
);
40+
41+
return new LazerExecute(
42+
decoded[0].targetChainId,
43+
governanceInstruction.directives || [],
44+
governanceInstruction.minExecutionTimestamp
45+
? new Date(
46+
governanceInstruction.minExecutionTimestamp.seconds * 1000 +
47+
(governanceInstruction.minExecutionTimestamp.nanos || 0) /
48+
1000000,
49+
)
50+
: undefined,
51+
governanceInstruction.maxExecutionTimestamp
52+
? new Date(
53+
governanceInstruction.maxExecutionTimestamp.seconds * 1000 +
54+
(governanceInstruction.maxExecutionTimestamp.nanos || 0) /
55+
1000000,
56+
)
57+
: undefined,
58+
governanceInstruction.governanceSequenceNo || undefined,
59+
);
60+
} catch (error) {
61+
console.error("Failed to decode Lazer governance instruction:", error);
62+
return undefined;
63+
}
64+
}
65+
66+
encode(): Buffer {
67+
try {
68+
// Create the GovernanceInstruction protobuf message
69+
const governanceInstruction =
70+
pyth_lazer_transaction.GovernanceInstruction.create({
71+
directives: this.directives,
72+
minExecutionTimestamp: this.minExecutionTimestamp
73+
? {
74+
seconds: Math.floor(
75+
this.minExecutionTimestamp.getTime() / 1000,
76+
),
77+
nanos: (this.minExecutionTimestamp.getTime() % 1000) * 1000000,
78+
}
79+
: undefined,
80+
maxExecutionTimestamp: this.maxExecutionTimestamp
81+
? {
82+
seconds: Math.floor(
83+
this.maxExecutionTimestamp.getTime() / 1000,
84+
),
85+
nanos: (this.maxExecutionTimestamp.getTime() % 1000) * 1000000,
86+
}
87+
: undefined,
88+
governanceSequenceNo: this.governanceSequenceNo,
89+
});
90+
91+
// Validate the message before encoding
92+
const error = pyth_lazer_transaction.GovernanceInstruction.verify(
93+
governanceInstruction,
94+
);
95+
if (error) {
96+
throw new Error(`GovernanceInstruction validation failed: ${error}`);
97+
}
98+
99+
// Encode the protobuf message to bytes
100+
const encodedInstruction =
101+
pyth_lazer_transaction.GovernanceInstruction.encode(
102+
governanceInstruction,
103+
).finish();
104+
105+
// Create a layout with the known instruction length for encoding
106+
const layout_with_known_span: BufferLayout.Structure<
107+
Readonly<{
108+
governanceInstruction: Uint8Array;
109+
}>
110+
> = BufferLayout.struct([
111+
BufferLayout.blob(encodedInstruction.length, "governanceInstruction"),
112+
]);
113+
114+
return super.encodeWithPayload(layout_with_known_span, {
115+
governanceInstruction: encodedInstruction,
116+
});
117+
} catch (error) {
118+
console.error("LazerExecute encoding error:", error);
119+
console.error("Directives:", JSON.stringify(this.directives, null, 2));
120+
console.error("minExecutionTimestamp:", this.minExecutionTimestamp);
121+
console.error("maxExecutionTimestamp:", this.maxExecutionTimestamp);
122+
console.error("governanceSequenceNo:", this.governanceSequenceNo);
123+
throw error;
124+
}
125+
}
126+
}

governance/xc_admin/packages/xc_admin_common/src/governance_payload/PythGovernanceAction.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ export const EvmExecutorAction = {
2424
Execute: 0,
2525
} as const;
2626

27+
export const LazerExecutorAction = {
28+
LazerExecute: 0,
29+
} as const;
30+
2731
/** Helper to get the ActionName from a (moduleId, actionId) tuple*/
2832
export function toActionName(
2933
deserialized: Readonly<{ moduleId: number; actionId: number }>,
@@ -58,14 +62,20 @@ export function toActionName(
5862
deserialized.actionId == 0
5963
) {
6064
return "Execute";
65+
} else if (
66+
deserialized.moduleId == MODULE_LAZER_EXECUTOR &&
67+
deserialized.actionId == 0
68+
) {
69+
return "LazerExecute";
6170
}
6271
return undefined;
6372
}
6473

6574
export declare type ActionName =
6675
| keyof typeof ExecutorAction
6776
| keyof typeof TargetAction
68-
| keyof typeof EvmExecutorAction;
77+
| keyof typeof EvmExecutorAction
78+
| keyof typeof LazerExecutorAction;
6979

7080
/** Governance header that should be in every Pyth crosschain governance message*/
7181
export class PythGovernanceHeader {
@@ -131,10 +141,15 @@ export class PythGovernanceHeader {
131141
} else if (this.action in TargetAction) {
132142
module = MODULE_TARGET;
133143
action = TargetAction[this.action as keyof typeof TargetAction];
134-
} else {
144+
} else if (this.action in EvmExecutorAction) {
135145
module = MODULE_EVM_EXECUTOR;
136146
action = EvmExecutorAction[this.action as keyof typeof EvmExecutorAction];
147+
} else {
148+
module = MODULE_LAZER_EXECUTOR;
149+
action =
150+
LazerExecutorAction[this.action as keyof typeof LazerExecutorAction];
137151
}
152+
138153
if (toChainId(this.targetChainId) === undefined)
139154
throw new Error(`Invalid chain id ${this.targetChainId}`);
140155
const span = PythGovernanceHeader.layout.encode(
@@ -154,7 +169,13 @@ export const MAGIC_NUMBER = 0x4d475450;
154169
export const MODULE_EXECUTOR = 0;
155170
export const MODULE_TARGET = 1;
156171
export const MODULE_EVM_EXECUTOR = 2;
157-
export const MODULES = [MODULE_EXECUTOR, MODULE_TARGET, MODULE_EVM_EXECUTOR];
172+
export const MODULE_LAZER_EXECUTOR = 3;
173+
export const MODULES = [
174+
MODULE_EXECUTOR,
175+
MODULE_TARGET,
176+
MODULE_EVM_EXECUTOR,
177+
MODULE_LAZER_EXECUTOR,
178+
];
158179

159180
export interface PythGovernanceAction {
160181
readonly targetChainId: ChainName;

governance/xc_admin/packages/xc_admin_common/src/governance_payload/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
import { EvmExecute } from "./ExecuteAction";
2323
import { SetTransactionFee } from "./SetTransactionFee";
2424
import { WithdrawFee } from "./WithdrawFee";
25+
import { LazerExecute } from "./LazerExecute";
2526

2627
/** Decode a governance payload */
2728
export function decodeGovernancePayload(
@@ -75,6 +76,8 @@ export function decodeGovernancePayload(
7576
}
7677
case "Execute":
7778
return EvmExecute.decode(data);
79+
case "LazerExecute":
80+
return LazerExecute.decode(data);
7881
case "SetTransactionFee":
7982
return SetTransactionFee.decode(data);
8083
case "WithdrawFee":
@@ -96,3 +99,4 @@ export * from "./SetTransactionFee";
9699
export * from "./SetWormholeAddress";
97100
export * from "./ExecuteAction";
98101
export * from "./WithdrawFee";
102+
export * from "./LazerExecute";

governance/xc_admin/packages/xc_admin_common/src/programs/core/core_functions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import {
3232
DownloadableProduct,
3333
PriceRawConfig,
3434
RawConfig,
35-
ValidationResult,
35+
CoreValidationResult,
3636
} from "../types";
3737
import { Program } from "@coral-xyz/anchor";
3838
import { PythOracle } from "@pythnetwork/client/lib/anchor";
@@ -309,7 +309,7 @@ export function validateUploadedConfig(
309309
existingConfig: Record<string, DownloadableProduct>,
310310
uploadedConfig: Record<string, DownloadableProduct>,
311311
cluster: PythCluster,
312-
): ValidationResult {
312+
): CoreValidationResult {
313313
try {
314314
const existingSymbols = new Set(Object.keys(existingConfig));
315315
const changes: Record<

0 commit comments

Comments
 (0)