Skip to content

feat: support lazer governance #2731

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Jun 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,4 @@ target_chains/ton/sdk/js
contract_manager
lazer/contracts/solana
lazer/sdk/js
lazer/state_sdk/js
2 changes: 2 additions & 0 deletions governance/xc_admin/packages/proposer_server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,10 @@ app.post("/api/propose", async (req: Request, res: Response) => {
res.status(200).json({ proposalPubkey: proposalPubkey });
} catch (error) {
if (error instanceof Error) {
console.error(error);
res.status(500).json(error.message);
} else {
console.error(error);
res.status(500).json("An unknown error occurred");
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@pythnetwork/pyth-lazer-sdk": "workspace:*",
"@pythnetwork/pyth-solana-receiver": "workspace:*",
"@pythnetwork/solana-utils": "workspace:*",
"@pythnetwork/pyth-lazer-state-sdk": "workspace:*",
"@solana/buffer-layout": "^4.0.1",
"@solana/web3.js": "^1.73.0",
"@sqds/mesh": "^1.0.6",
Expand Down
4 changes: 4 additions & 0 deletions governance/xc_admin/packages/xc_admin_common/src/chains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,10 @@ export const RECEIVER_CHAINS = {
worldchain_testnet: 50123,
mezo_testnet: 50124,
hemi_testnet: 50125,

// Lazer
lazer_production: 10000,
lazer_staging: 10001,
};

// If there is any overlapping value the receiver chain will replace the wormhole
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { PythGovernanceActionImpl } from "./PythGovernanceAction";
import * as BufferLayout from "@solana/buffer-layout";
import { ChainName } from "../chains";
import { pyth_lazer_transaction } from "@pythnetwork/pyth-lazer-state-sdk/governance";

/** Executes a Lazer governance instruction with the specified directives */
export class LazerExecute extends PythGovernanceActionImpl {
static layout: BufferLayout.Structure<
Readonly<{
governanceInstruction: Uint8Array;
}>
> = BufferLayout.struct([
BufferLayout.blob(new BufferLayout.GreedyCount(), "governanceInstruction"),
]);

constructor(
targetChainId: ChainName,
readonly directives: pyth_lazer_transaction.IGovernanceDirective[],
readonly minExecutionTimestamp?: Date,
readonly maxExecutionTimestamp?: Date,
readonly governanceSequenceNo?: number,
) {
super(targetChainId, "LazerExecute");
}

static decode(data: Buffer): LazerExecute | undefined {
const decoded = PythGovernanceActionImpl.decodeWithPayload(
data,
"LazerExecute",
this.layout,
);
if (!decoded) return undefined;

try {
// Decode the protobuf GovernanceInstruction
const governanceInstruction =
pyth_lazer_transaction.GovernanceInstruction.decode(
decoded[1].governanceInstruction,
);

return new LazerExecute(
decoded[0].targetChainId,
governanceInstruction.directives || [],
governanceInstruction.minExecutionTimestamp
? new Date(
governanceInstruction.minExecutionTimestamp.seconds * 1000 +
(governanceInstruction.minExecutionTimestamp.nanos || 0) /
1000000,
)
: undefined,
governanceInstruction.maxExecutionTimestamp
? new Date(
governanceInstruction.maxExecutionTimestamp.seconds * 1000 +
(governanceInstruction.maxExecutionTimestamp.nanos || 0) /
1000000,
)
: undefined,
governanceInstruction.governanceSequenceNo || undefined,
);
} catch (error) {
console.error("Failed to decode Lazer governance instruction:", error);
return undefined;
}
}

encode(): Buffer {
try {
// Create the GovernanceInstruction protobuf message
const governanceInstruction =
pyth_lazer_transaction.GovernanceInstruction.create({
directives: this.directives,
minExecutionTimestamp: this.minExecutionTimestamp
? {
seconds: Math.floor(
this.minExecutionTimestamp.getTime() / 1000,
),
nanos: (this.minExecutionTimestamp.getTime() % 1000) * 1000000,
}
: undefined,
maxExecutionTimestamp: this.maxExecutionTimestamp
? {
seconds: Math.floor(
this.maxExecutionTimestamp.getTime() / 1000,
),
nanos: (this.maxExecutionTimestamp.getTime() % 1000) * 1000000,
}
: undefined,
governanceSequenceNo: this.governanceSequenceNo,
});

// Validate the message before encoding
const error = pyth_lazer_transaction.GovernanceInstruction.verify(
governanceInstruction,
);
if (error) {
throw new Error(`GovernanceInstruction validation failed: ${error}`);
}

// Encode the protobuf message to bytes
const encodedInstruction =
pyth_lazer_transaction.GovernanceInstruction.encode(
governanceInstruction,
).finish();

// Create a layout with the known instruction length for encoding
const layout_with_known_span: BufferLayout.Structure<
Readonly<{
governanceInstruction: Uint8Array;
}>
> = BufferLayout.struct([
BufferLayout.blob(encodedInstruction.length, "governanceInstruction"),
]);

return super.encodeWithPayload(layout_with_known_span, {
governanceInstruction: encodedInstruction,
});
} catch (error) {
console.error("LazerExecute encoding error:", error);
console.error("Directives:", JSON.stringify(this.directives, null, 2));
console.error("minExecutionTimestamp:", this.minExecutionTimestamp);
console.error("maxExecutionTimestamp:", this.maxExecutionTimestamp);
console.error("governanceSequenceNo:", this.governanceSequenceNo);
throw error;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ export const EvmExecutorAction = {
Execute: 0,
} as const;

export const LazerExecutorAction = {
LazerExecute: 0,
} as const;

/** Helper to get the ActionName from a (moduleId, actionId) tuple*/
export function toActionName(
deserialized: Readonly<{ moduleId: number; actionId: number }>,
Expand Down Expand Up @@ -58,14 +62,20 @@ export function toActionName(
deserialized.actionId == 0
) {
return "Execute";
} else if (
deserialized.moduleId == MODULE_LAZER_EXECUTOR &&
deserialized.actionId == 0
) {
return "LazerExecute";
}
return undefined;
}

export declare type ActionName =
| keyof typeof ExecutorAction
| keyof typeof TargetAction
| keyof typeof EvmExecutorAction;
| keyof typeof EvmExecutorAction
| keyof typeof LazerExecutorAction;

/** Governance header that should be in every Pyth crosschain governance message*/
export class PythGovernanceHeader {
Expand Down Expand Up @@ -131,10 +141,15 @@ export class PythGovernanceHeader {
} else if (this.action in TargetAction) {
module = MODULE_TARGET;
action = TargetAction[this.action as keyof typeof TargetAction];
} else {
} else if (this.action in EvmExecutorAction) {
module = MODULE_EVM_EXECUTOR;
action = EvmExecutorAction[this.action as keyof typeof EvmExecutorAction];
} else {
module = MODULE_LAZER_EXECUTOR;
action =
LazerExecutorAction[this.action as keyof typeof LazerExecutorAction];
}

if (toChainId(this.targetChainId) === undefined)
throw new Error(`Invalid chain id ${this.targetChainId}`);
const span = PythGovernanceHeader.layout.encode(
Expand All @@ -154,7 +169,13 @@ export const MAGIC_NUMBER = 0x4d475450;
export const MODULE_EXECUTOR = 0;
export const MODULE_TARGET = 1;
export const MODULE_EVM_EXECUTOR = 2;
export const MODULES = [MODULE_EXECUTOR, MODULE_TARGET, MODULE_EVM_EXECUTOR];
export const MODULE_LAZER_EXECUTOR = 3;
export const MODULES = [
MODULE_EXECUTOR,
MODULE_TARGET,
MODULE_EVM_EXECUTOR,
MODULE_LAZER_EXECUTOR,
];

export interface PythGovernanceAction {
readonly targetChainId: ChainName;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
import { EvmExecute } from "./ExecuteAction";
import { SetTransactionFee } from "./SetTransactionFee";
import { WithdrawFee } from "./WithdrawFee";
import { LazerExecute } from "./LazerExecute";

/** Decode a governance payload */
export function decodeGovernancePayload(
Expand Down Expand Up @@ -75,6 +76,8 @@ export function decodeGovernancePayload(
}
case "Execute":
return EvmExecute.decode(data);
case "LazerExecute":
return LazerExecute.decode(data);
case "SetTransactionFee":
return SetTransactionFee.decode(data);
case "WithdrawFee":
Expand All @@ -96,3 +99,4 @@ export * from "./SetTransactionFee";
export * from "./SetWormholeAddress";
export * from "./ExecuteAction";
export * from "./WithdrawFee";
export * from "./LazerExecute";
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
DownloadableProduct,
PriceRawConfig,
RawConfig,
ValidationResult,
CoreValidationResult,
} from "../types";
import { Program } from "@coral-xyz/anchor";
import { PythOracle } from "@pythnetwork/client/lib/anchor";
Expand Down Expand Up @@ -309,7 +309,7 @@ export function validateUploadedConfig(
existingConfig: Record<string, DownloadableProduct>,
uploadedConfig: Record<string, DownloadableProduct>,
cluster: PythCluster,
): ValidationResult {
): CoreValidationResult {
try {
const existingSymbols = new Set(Object.keys(existingConfig));
const changes: Record<
Expand Down
Loading
Loading