Skip to content

feat(xc_admin_frontend): add Lazer integration #2722

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

Open
wants to merge 27 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f581b9c
feat: add initial Lazer integration
cctdaniel May 23, 2025
206a01b
refactor: improve code formatting and readability in PythCore, PythLa…
cctdaniel May 23, 2025
3f3f890
chore: format
cctdaniel May 23, 2025
bc325e1
feat: enhance proposal modal with detailed change tracking for shards…
cctdaniel May 23, 2025
bec5469
fix: format
cctdaniel May 23, 2025
5c8d566
refactor: revert placeholder implementation in generateInstructions
cctdaniel May 23, 2025
76c6620
feat: add governance payload functions for managing feeds and publishers
cctdaniel May 26, 2025
df0810e
fix: ci
cctdaniel May 26, 2025
139ab04
revert fix
cctdaniel May 26, 2025
0183b20
fix: ci
cctdaniel May 26, 2025
c112764
fix: format
cctdaniel May 26, 2025
40b21cd
fix: update LazerFeedMetadata type and add conversion function for pr…
cctdaniel May 27, 2025
8bcdf9c
fix: format
cctdaniel May 27, 2025
16b278a
fix: ci
cctdaniel May 27, 2025
166cad3
fix: ci
cctdaniel May 27, 2025
cdc2f33
fix: ci
cctdaniel May 27, 2025
77e6f4a
feat: add Lazer State SDK and update TypeScript configurations
cctdaniel May 29, 2025
8e671f0
fix: format
cctdaniel May 29, 2025
056e811
fix: format
cctdaniel May 29, 2025
146f4c0
fix: update .prettierignore to exclude lazer/state_sdk/js
cctdaniel May 29, 2025
e454ab8
fix: update .prettierignore to exclude lazer/state_sdk/js
cctdaniel May 29, 2025
555df18
fix: format
cctdaniel May 29, 2025
c3bd411
fix: update validation types and improve Lazer configuration handling
cctdaniel Jun 5, 2025
273c4fa
fix: update download filename for Lazer configuration export
cctdaniel Jun 5, 2025
1e8558d
feat: add lazer support to xc-admin-common
cctdaniel Jun 12, 2025
5f2ba2e
feat: add Lazer environment switch and context management
cctdaniel Jun 12, 2025
b2be32c
fix: improve error handling and logging in proposer server; refactor …
cctdaniel Jun 12, 2025
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";
12 changes: 9 additions & 3 deletions governance/xc_admin/packages/xc_admin_common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ export * from "./deterministic_stake_accounts";
export * from "./price_store";
export { default as lazerIdl } from "./multisig_transaction/idl/lazer.json";

export {
ProgramType,
PROGRAM_TYPE_NAMES,
export { ProgramType, PROGRAM_TYPE_NAMES } from "./programs/types";

export type {
PriceRawConfig,
ProductRawConfig,
MappingRawConfig,
Expand All @@ -29,7 +29,13 @@ export {
ProgramInstructionAccounts,
InstructionAccountsTypeMap,
ValidationResult,
LazerState,
LazerFeed,
LazerPublisher,
LazerFeedMetadata,
LazerConfig,
} from "./programs/types";

export {
getProgramAddress,
isAvailableOnCluster,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
import {
AccountType,
PythCluster,
getPythProgramKeyForCluster,
parseBaseData,
parseMappingData,
parsePermissionData,
Expand All @@ -33,8 +32,7 @@ import {
DownloadableProduct,
PriceRawConfig,
RawConfig,
ValidationResult,
ProgramType,
CoreValidationResult,
} from "../types";
import { Program } from "@coral-xyz/anchor";
import { PythOracle } from "@pythnetwork/client/lib/anchor";
Expand Down Expand Up @@ -103,7 +101,9 @@ const mapValues = <T, U>(
/**
* Sort configuration data for consistent output
*/
function sortData(data: DownloadableConfig): DownloadableConfig {
function sortData(
data: Record<string, DownloadableProduct>,
): DownloadableConfig {
return mapValues(data, (productData: DownloadableProduct) => ({
address: productData.address,
metadata: Object.fromEntries(
Expand Down Expand Up @@ -306,10 +306,10 @@ export function getDownloadableConfig(
* Validate an uploaded configuration against the current configuration
*/
export function validateUploadedConfig(
existingConfig: DownloadableConfig,
uploadedConfig: DownloadableConfig,
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