Преглед на файлове

feat(xc-admin): add support for deserializing lazer instructions in xc-admin (#2246)

* feat: add support for deserializing lazer instructions in xc-admin

* fix error

* add test
Daniel Chew преди 10 месеца
родител
ревизия
cbfc885840

+ 171 - 0
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);
+  });
+});

+ 55 - 0
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 }
+      );
+    }
+  }
+}

+ 307 - 0
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"
+          }
+        ]
+      }
+    }
+  ]
+}

+ 9 - 0
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);
     }