diff --git a/governance/xc_admin/packages/xc_admin_common/src/__tests__/LazerMultisigInstruction.test.ts b/governance/xc_admin/packages/xc_admin_common/src/__tests__/LazerMultisigInstruction.test.ts new file mode 100644 index 0000000000..69e7eb5ec3 --- /dev/null +++ b/governance/xc_admin/packages/xc_admin_common/src/__tests__/LazerMultisigInstruction.test.ts @@ -0,0 +1,171 @@ +import { + PublicKey, + TransactionInstruction, + SystemProgram, +} from "@solana/web3.js"; +import { LazerMultisigInstruction } from "../multisig_transaction/LazerMultisigInstruction"; +import { + MultisigInstructionProgram, + UNRECOGNIZED_INSTRUCTION, +} from "../multisig_transaction"; + +describe("LazerMultisigInstruction", () => { + const mockProgramId = new PublicKey( + "pytd2yyk641x7ak7mkaasSJVXh6YYZnC7wTmtgAyxPt" + ); + const systemProgram = SystemProgram.programId; + + // Generate reusable keypairs for tests + const topAuthority = PublicKey.unique(); + const storage = PublicKey.unique(); + const payer = PublicKey.unique(); + + // Test recognized instruction + test("fromInstruction should decode update instruction", () => { + const instructionData = Buffer.from([ + // Anchor discriminator for update (from IDL) + 219, + 200, + 88, + 176, + 158, + 63, + 253, + 127, + // trusted_signer (pubkey - 32 bytes) + ...Array(32).fill(1), + // expires_at (i64 - 8 bytes) + 42, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ]); + + const keys = [ + { + pubkey: topAuthority, + isSigner: true, + isWritable: false, + }, + { + pubkey: storage, + isSigner: false, + isWritable: true, + }, + ]; + + const instruction = new TransactionInstruction({ + programId: mockProgramId, + keys, + data: instructionData, + }); + + const lazerInstruction = + LazerMultisigInstruction.fromInstruction(instruction); + + expect(lazerInstruction.name).toBe("update"); + expect(lazerInstruction.args).toBeDefined(); + expect(lazerInstruction.args.trustedSigner).toBeDefined(); + expect(lazerInstruction.args.expiresAt).toBeDefined(); + expect(lazerInstruction.accounts).toBeDefined(); + expect(lazerInstruction.accounts.named.topAuthority).toBeDefined(); + expect(lazerInstruction.accounts.named.storage).toBeDefined(); + }); + + // Test unrecognized instruction + test("fromInstruction should handle unrecognized instruction", () => { + const unrecognizedData = Buffer.from([1, 2, 3, 4]); + const keys = [ + { + pubkey: topAuthority, + isSigner: false, + isWritable: true, + }, + ]; + + const instruction = new TransactionInstruction({ + programId: mockProgramId, + keys, + data: unrecognizedData, + }); + + const lazerInstruction = + LazerMultisigInstruction.fromInstruction(instruction); + + expect(lazerInstruction.name).toBe(UNRECOGNIZED_INSTRUCTION); + expect(lazerInstruction.args).toEqual({ data: unrecognizedData }); + expect(lazerInstruction.accounts.remaining).toEqual(keys); + }); + + // Test initialize instruction + test("fromInstruction should decode initialize instruction", () => { + const instructionData = Buffer.from([ + // Anchor discriminator for initialize (from IDL) + 175, + 175, + 109, + 31, + 13, + 152, + 155, + 237, + // top_authority (pubkey - 32 bytes) + ...Array(32).fill(2), + // treasury (pubkey - 32 bytes) + ...Array(32).fill(3), + ]); + + const keys = [ + { + pubkey: payer, + isSigner: true, + isWritable: true, + }, + { + pubkey: storage, + isSigner: false, + isWritable: true, + }, + { + pubkey: systemProgram, + isSigner: false, + isWritable: false, + }, + ]; + + const instruction = new TransactionInstruction({ + programId: mockProgramId, + keys, + data: instructionData, + }); + + const lazerInstruction = + LazerMultisigInstruction.fromInstruction(instruction); + + expect(lazerInstruction.name).toBe("initialize"); + expect(lazerInstruction.args).toBeDefined(); + expect(lazerInstruction.args.topAuthority).toBeDefined(); + expect(lazerInstruction.args.treasury).toBeDefined(); + expect(lazerInstruction.accounts).toBeDefined(); + expect(lazerInstruction.accounts.named.payer).toBeDefined(); + expect(lazerInstruction.accounts.named.storage).toBeDefined(); + expect(lazerInstruction.accounts.named.systemProgram).toBeDefined(); + }); + + // Test program field + test("should have correct program type", () => { + const instruction = new TransactionInstruction({ + programId: mockProgramId, + keys: [], + data: Buffer.from([]), + }); + + const lazerInstruction = + LazerMultisigInstruction.fromInstruction(instruction); + expect(lazerInstruction.program).toBe(MultisigInstructionProgram.Lazer); + }); +}); diff --git a/governance/xc_admin/packages/xc_admin_common/src/multisig_transaction/LazerMultisigInstruction.ts b/governance/xc_admin/packages/xc_admin_common/src/multisig_transaction/LazerMultisigInstruction.ts new file mode 100644 index 0000000000..29a2758d42 --- /dev/null +++ b/governance/xc_admin/packages/xc_admin_common/src/multisig_transaction/LazerMultisigInstruction.ts @@ -0,0 +1,55 @@ +import { + MultisigInstruction, + MultisigInstructionProgram, + UNRECOGNIZED_INSTRUCTION, +} from "./index"; +import { AnchorAccounts, resolveAccountNames } from "./anchor"; +import { PublicKey, TransactionInstruction } from "@solana/web3.js"; +import { Idl, BorshInstructionCoder } from "@coral-xyz/anchor"; +import lazerIdl from "./idl/lazer.json"; + +export const LAZER_PROGRAM_ID = new PublicKey( + "pytd2yyk641x7ak7mkaasSJVXh6YYZnC7wTmtgAyxPt" +); + +export class LazerMultisigInstruction implements MultisigInstruction { + readonly program = MultisigInstructionProgram.Lazer; + readonly name: string; + readonly args: { [key: string]: any }; + readonly accounts: AnchorAccounts; + + constructor( + name: string, + args: { [key: string]: any }, + accounts: AnchorAccounts + ) { + this.name = name; + this.args = args; + this.accounts = accounts; + } + + static fromInstruction( + instruction: TransactionInstruction + ): LazerMultisigInstruction { + // TODO: This is a hack to transform the IDL to be compatible with the anchor version we are using, we can't upgrade anchor to 0.30.1 because then the existing idls will break + const idl = lazerIdl as Idl; + + const coder = new BorshInstructionCoder(idl); + + const deserializedData = coder.decode(instruction.data); + + if (deserializedData) { + return new LazerMultisigInstruction( + deserializedData.name, + deserializedData.data, + resolveAccountNames(idl, deserializedData.name, instruction) + ); + } else { + return new LazerMultisigInstruction( + UNRECOGNIZED_INSTRUCTION, + { data: instruction.data }, + { named: {}, remaining: instruction.keys } + ); + } + } +} diff --git a/governance/xc_admin/packages/xc_admin_common/src/multisig_transaction/idl/lazer.json b/governance/xc_admin/packages/xc_admin_common/src/multisig_transaction/idl/lazer.json new file mode 100644 index 0000000000..d2bb20dadd --- /dev/null +++ b/governance/xc_admin/packages/xc_admin_common/src/multisig_transaction/idl/lazer.json @@ -0,0 +1,307 @@ +{ + "version": "0.2.0", + "name": "pyth_lazer_solana_contract", + "instructions": [ + { + "name": "initialize", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "storage", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "topAuthority", + "type": "publicKey" + }, + { + "name": "treasury", + "type": "publicKey" + } + ] + }, + { + "name": "migrateFrom010", + "accounts": [ + { + "name": "topAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "storage", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "treasury", + "type": "publicKey" + } + ] + }, + { + "name": "update", + "accounts": [ + { + "name": "topAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "storage", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "trustedSigner", + "type": "publicKey" + }, + { + "name": "expiresAt", + "type": "i64" + } + ] + }, + { + "name": "verifyMessage", + "docs": [ + "Verifies a ed25519 signature on Solana by checking that the transaction contains", + "a correct call to the built-in `ed25519_program`.", + "", + "- `message_data` is the signed message that is being verified.", + "- `ed25519_instruction_index` is the index of the `ed25519_program` instruction", + "within the transaction. This instruction must precede the current instruction.", + "- `signature_index` is the index of the signature within the inputs to the `ed25519_program`.", + "- `message_offset` is the offset of the signed message within the", + "input data for the current instruction." + ], + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "storage", + "isMut": false, + "isSigner": false + }, + { + "name": "treasury", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "instructionsSysvar", + "isMut": false, + "isSigner": false, + "docs": [ + "(e.g. in `sysvar::instructions::load_instruction_at_checked`).", + "This account is not usable with anchor's `Program` account type because it's not executable." + ] + } + ], + "args": [ + { + "name": "messageData", + "type": "bytes" + }, + { + "name": "ed25519InstructionIndex", + "type": "u16" + }, + { + "name": "signatureIndex", + "type": "u8" + }, + { + "name": "messageOffset", + "type": "u16" + } + ], + "returns": { + "defined": "VerifiedMessage" + } + } + ], + "accounts": [ + { + "name": "Storage", + "type": { + "kind": "struct", + "fields": [ + { + "name": "topAuthority", + "type": "publicKey" + }, + { + "name": "treasury", + "type": "publicKey" + }, + { + "name": "singleUpdateFeeInLamports", + "type": "u64" + }, + { + "name": "numTrustedSigners", + "type": "u8" + }, + { + "name": "trustedSigners", + "type": { + "array": [ + { + "defined": "TrustedSignerInfo" + }, + 5 + ] + } + }, + { + "name": "extraSpace", + "type": { + "array": ["u8", 100] + } + } + ] + } + } + ], + "types": [ + { + "name": "VerifiedMessage", + "docs": ["A message with a verified ed25519 signature."], + "type": { + "kind": "struct", + "fields": [ + { + "name": "publicKey", + "docs": ["Public key that signed the message."], + "type": "publicKey" + }, + { + "name": "payload", + "docs": ["Signed message payload."], + "type": "bytes" + } + ] + } + }, + { + "name": "TrustedSignerInfo", + "type": { + "kind": "struct", + "fields": [ + { + "name": "pubkey", + "type": "publicKey" + }, + { + "name": "expiresAt", + "type": "i64" + } + ] + } + }, + { + "name": "SignatureVerificationError", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Ed25519InstructionMustPrecedeCurrentInstruction" + }, + { + "name": "LoadInstructionAtFailed", + "fields": [ + { + "defined": "ProgramError" + } + ] + }, + { + "name": "LoadCurrentIndexFailed", + "fields": [ + { + "defined": "ProgramError" + } + ] + }, + { + "name": "ClockGetFailed", + "fields": [ + { + "defined": "ProgramError" + } + ] + }, + { + "name": "InvalidEd25519InstructionProgramId" + }, + { + "name": "InvalidEd25519InstructionDataLength" + }, + { + "name": "InvalidSignatureIndex" + }, + { + "name": "InvalidSignatureOffset" + }, + { + "name": "InvalidPublicKeyOffset" + }, + { + "name": "InvalidMessageOffset" + }, + { + "name": "InvalidMessageDataSize" + }, + { + "name": "InvalidInstructionIndex" + }, + { + "name": "MessageOffsetOverflow" + }, + { + "name": "FormatMagicMismatch" + }, + { + "name": "InvalidStorageAccountId" + }, + { + "name": "InvalidStorageData" + }, + { + "name": "NotTrustedSigner" + } + ] + } + } + ] +} diff --git a/governance/xc_admin/packages/xc_admin_common/src/multisig_transaction/index.ts b/governance/xc_admin/packages/xc_admin_common/src/multisig_transaction/index.ts index e449684d79..52fc7287df 100644 --- a/governance/xc_admin/packages/xc_admin_common/src/multisig_transaction/index.ts +++ b/governance/xc_admin/packages/xc_admin_common/src/multisig_transaction/index.ts @@ -27,6 +27,10 @@ import { PRICE_STORE_PROGRAM_ID, PriceStoreMultisigInstruction, } from "../price_store"; +import { + LazerMultisigInstruction, + LAZER_PROGRAM_ID, +} from "./LazerMultisigInstruction"; export const UNRECOGNIZED_INSTRUCTION = "unrecognizedInstruction"; export enum MultisigInstructionProgram { @@ -41,6 +45,7 @@ export enum MultisigInstructionProgram { SolanaReceiver, UnrecognizedProgram, PythPriceStore, + Lazer, } export function getProgramName(program: MultisigInstructionProgram) { @@ -67,6 +72,8 @@ export function getProgramName(program: MultisigInstructionProgram) { return "Pyth Price Store"; case MultisigInstructionProgram.UnrecognizedProgram: return "Unknown"; + case MultisigInstructionProgram.Lazer: + return "Lazer"; } } @@ -161,6 +168,8 @@ export class MultisigParser { return SolanaStakingMultisigInstruction.fromTransactionInstruction( instruction ); + } else if (instruction.programId.equals(LAZER_PROGRAM_ID)) { + return LazerMultisigInstruction.fromInstruction(instruction); } else { return UnrecognizedProgram.fromTransactionInstruction(instruction); }