Selaa lähdekoodia

[Xc-admin] ledger (#561)

* Add ledger support

* Checkpoint

* Checkpoint

* Package locK

* Console err
guibescos 2 vuotta sitten
vanhempi
sitoutus
fc08ec277e

+ 14 - 7
governance/xc_admin/packages/crank_executor/src/index.ts

@@ -53,13 +53,16 @@ async function run() {
     multisigProgramId: DEFAULT_MULTISIG_PROGRAM_ID,
   });
   const multisigParser = MultisigParser.fromCluster(CLUSTER as PythCluster);
-  const wormholeFee = (
-    await getWormholeBridgeData(
-      squad.connection,
-      multisigParser.wormholeBridgeAddress!,
-      COMMITMENT
-    )
-  ).config.fee;
+
+  const wormholeFee = multisigParser.wormholeBridgeAddress
+    ? (
+        await getWormholeBridgeData(
+          squad.connection,
+          multisigParser.wormholeBridgeAddress!,
+          COMMITMENT
+        )
+      ).config.fee
+    : 0;
 
   const proposals = await getProposals(squad, VAULT, undefined, "executeReady");
   for (const proposal of proposals) {
@@ -114,11 +117,15 @@ async function run() {
         } catch (error) {
           // Mark the transaction as cancelled if we failed to run it
           if (error instanceof SendTransactionError) {
+            console.error(error);
             await squad.cancelTransaction(proposal.publicKey);
+            console.log("Cancelled: ", proposal.publicKey.toBase58());
           }
           break;
         }
       }
+    } else {
+      console.log("Skipping: ", proposal.publicKey.toBase58());
     }
   }
 }

+ 2 - 0
governance/xc_admin/packages/xc_admin_cli/package.json

@@ -19,6 +19,8 @@
   },
   "dependencies": {
     "@coral-xyz/anchor": "^0.26.0",
+    "@ledgerhq/hw-transport": "^6.27.10",
+    "@ledgerhq/hw-transport-node-hid": "^6.27.10",
     "@pythnetwork/client": "^2.9.0",
     "@solana/web3.js": "^1.73.0",
     "@sqds/mesh": "^1.0.6",

+ 71 - 17
governance/xc_admin/packages/xc_admin_cli/src/index.ts

@@ -28,14 +28,43 @@ import {
   WORMHOLE_ADDRESS,
 } from "xc_admin_common";
 import { pythOracleProgram } from "@pythnetwork/client";
+import { Wallet } from "@coral-xyz/anchor/dist/cjs/provider";
+import { LedgerNodeWallet } from "./ledger";
+
+export async function loadHotWalletOrLedger(
+  wallet: string,
+  lda: number,
+  ldc: number
+): Promise<Wallet> {
+  if (wallet === "ledger") {
+    return await LedgerNodeWallet.createWallet(lda, ldc);
+  } else {
+    return new NodeWallet(
+      Keypair.fromSecretKey(
+        Uint8Array.from(JSON.parse(fs.readFileSync(wallet, "ascii")))
+      )
+    );
+  }
+}
 
 const mutlisigCommand = (name: string, description: string) =>
   program
     .command(name)
     .description(description)
     .requiredOption("-c, --cluster <network>", "solana cluster to use")
-    .requiredOption("-w, --wallet <filepath>", "path to the operations key")
-    .requiredOption("-v, --vault <pubkey>", "multisig address");
+    .requiredOption(
+      "-w, --wallet <filepath>",
+      'path to the operations key or "ledger"'
+    )
+    .requiredOption("-v, --vault <pubkey>", "multisig address")
+    .option(
+      "-lda, --ledger-derivation-account <number>",
+      "ledger derivation account to use"
+    )
+    .option(
+      "-ldc, --ledger-derivation-change <number>",
+      "ledger derivation change to use"
+    );
 
 program
   .name("xc_admin_cli")
@@ -56,10 +85,10 @@ mutlisigCommand(
   )
 
   .action(async (options: any) => {
-    const wallet = new NodeWallet(
-      Keypair.fromSecretKey(
-        Uint8Array.from(JSON.parse(fs.readFileSync(options.wallet, "ascii")))
-      )
+    const wallet = await loadHotWalletOrLedger(
+      options.wallet,
+      options.ledgerDerivationAccount,
+      options.ledgerDerivationChange
     );
     const cluster: PythCluster = options.cluster;
     const programId: PublicKey = new PublicKey(options.programId);
@@ -104,7 +133,7 @@ mutlisigCommand(
       .accept()
       .accounts({
         currentAuthority: current,
-        newAuthority: mapKey(vaultAuthority),
+        newAuthority: isRemote ? mapKey(vaultAuthority) : vaultAuthority,
         programAccount: programId,
         programDataAccount,
         bpfUpgradableLoader: BPF_UPGRADABLE_LOADER,
@@ -128,10 +157,10 @@ mutlisigCommand("upgrade-program", "Upgrade a program from a buffer")
   .requiredOption("-b, --buffer <pubkey>", "buffer account")
 
   .action(async (options: any) => {
-    const wallet = new NodeWallet(
-      Keypair.fromSecretKey(
-        Uint8Array.from(JSON.parse(fs.readFileSync(options.wallet, "ascii")))
-      )
+    const wallet = await loadHotWalletOrLedger(
+      options.wallet,
+      options.ledgerDerivationAccount,
+      options.ledgerDerivationChange
     );
     const cluster: PythCluster = options.cluster;
     const programId: PublicKey = new PublicKey(options.programId);
@@ -166,7 +195,11 @@ mutlisigCommand("upgrade-program", "Upgrade a program from a buffer")
         { pubkey: wallet.publicKey, isSigner: false, isWritable: true },
         { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false },
         { pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false },
-        { pubkey: mapKey(vaultAuthority), isSigner: true, isWritable: false },
+        {
+          pubkey: isRemote ? mapKey(vaultAuthority) : vaultAuthority,
+          isSigner: true,
+          isWritable: false,
+        },
       ],
     };
 
@@ -186,10 +219,10 @@ mutlisigCommand(
   .requiredOption("-p, --price <pubkey>", "Price account to modify")
   .requiredOption("-e, --exponent <number>", "New exponent")
   .action(async (options: any) => {
-    const wallet = new NodeWallet(
-      Keypair.fromSecretKey(
-        Uint8Array.from(JSON.parse(fs.readFileSync(options.wallet, "ascii")))
-      )
+    const wallet = await loadHotWalletOrLedger(
+      options.wallet,
+      options.ledgerDerivationAccount,
+      options.ledgerDerivationChange
     );
     const cluster: PythCluster = options.cluster;
     const vault: PublicKey = new PublicKey(options.vault);
@@ -222,7 +255,10 @@ program
   .command("parse-transaction")
   .description("Parse a transaction sitting in the multisig")
   .requiredOption("-c, --cluster <network>", "solana cluster to use")
-  .requiredOption("-t, --transaction <pubkey>", "path to the operations key")
+  .requiredOption(
+    "-t, --transaction <pubkey>",
+    "address of the outstanding transaction"
+  )
   .action(async (options: any) => {
     const cluster = options.cluster;
     const transaction: PublicKey = new PublicKey(options.transaction);
@@ -245,4 +281,22 @@ program
     console.log(JSON.stringify(parsed, null, 2));
   });
 
+mutlisigCommand("approve", "Approve a transaction sitting in the multisig")
+  .requiredOption(
+    "-t, --transaction <pubkey>",
+    "address of the outstanding transaction"
+  )
+  .action(async (options: any) => {
+    const wallet = await loadHotWalletOrLedger(
+      options.wallet,
+      options.ledgerDerivationAccount,
+      options.ledgerDerivationChange
+    );
+    const transaction: PublicKey = new PublicKey(options.transaction);
+    const cluster: PythCluster = options.cluster;
+
+    const squad = SquadsMesh.endpoint(getPythClusterApiUrl(cluster), wallet);
+    await squad.approveTransaction(transaction);
+  });
+
 program.parse();

+ 163 - 0
governance/xc_admin/packages/xc_admin_cli/src/ledger.ts

@@ -0,0 +1,163 @@
+import { Wallet } from "@coral-xyz/anchor/dist/cjs/provider";
+import Transport, {
+  StatusCodes,
+  TransportStatusError,
+} from "@ledgerhq/hw-transport";
+import TransportNodeHid from "@ledgerhq/hw-transport-node-hid";
+import { PublicKey, Transaction } from "@solana/web3.js";
+
+export class LedgerNodeWallet implements Wallet {
+  private _derivationPath: Buffer;
+  private _transport: Transport;
+  publicKey: PublicKey;
+
+  constructor(
+    derivationPath: Buffer,
+    transport: Transport,
+    publicKey: PublicKey
+  ) {
+    this._derivationPath = derivationPath;
+    this._transport = transport;
+    this.publicKey = publicKey;
+  }
+
+  static async createWallet(
+    derivationAccount?: number,
+    derivationChange?: number
+  ): Promise<LedgerNodeWallet> {
+    const transport = await TransportNodeHid.create();
+    const derivationPath = getDerivationPath(
+      derivationAccount,
+      derivationChange
+    );
+    const publicKey = await getPublicKey(transport, derivationPath);
+    console.log(`Loaded ledger: ${publicKey.toBase58()}}`);
+    return new LedgerNodeWallet(derivationPath, transport, publicKey);
+  }
+
+  async signTransaction(transaction: Transaction): Promise<Transaction> {
+    console.log("Please approve the transaction on your ledger device...");
+    const transport = this._transport;
+    const publicKey = this.publicKey;
+
+    const signature = await signTransaction(
+      transport,
+      transaction,
+      this._derivationPath
+    );
+    transaction.addSignature(publicKey, signature);
+    return transaction;
+  }
+
+  async signAllTransactions(txs: Transaction[]): Promise<Transaction[]> {
+    return await Promise.all(txs.map((tx) => this.signTransaction(tx)));
+  }
+}
+
+/** @internal */
+function getDerivationPath(account?: number, change?: number): Buffer {
+  const length = account !== undefined ? (change === undefined ? 3 : 4) : 2;
+  const derivationPath = Buffer.alloc(1 + length * 4);
+
+  let offset = derivationPath.writeUInt8(length, 0);
+  offset = derivationPath.writeUInt32BE(harden(44), offset); // Using BIP44
+  offset = derivationPath.writeUInt32BE(harden(501), offset); // Solana's BIP44 path
+
+  if (account !== undefined) {
+    offset = derivationPath.writeUInt32BE(harden(account), offset);
+    if (change !== undefined) {
+      derivationPath.writeUInt32BE(harden(change), offset);
+    }
+  }
+
+  return derivationPath;
+}
+
+const BIP32_HARDENED_BIT = (1 << 31) >>> 0;
+
+/** @internal */
+function harden(n: number): number {
+  return (n | BIP32_HARDENED_BIT) >>> 0;
+}
+
+const INS_GET_PUBKEY = 0x05;
+const INS_SIGN_MESSAGE = 0x06;
+
+const P1_NON_CONFIRM = 0x00;
+const P1_CONFIRM = 0x01;
+
+const P2_EXTEND = 0x01;
+const P2_MORE = 0x02;
+
+const MAX_PAYLOAD = 255;
+
+const LEDGER_CLA = 0xe0;
+
+/** @internal */
+export async function getPublicKey(
+  transport: Transport,
+  derivationPath: Buffer
+): Promise<PublicKey> {
+  const bytes = await send(
+    transport,
+    INS_GET_PUBKEY,
+    P1_NON_CONFIRM,
+    derivationPath
+  );
+  return new PublicKey(bytes);
+}
+
+/** @internal */
+export async function signTransaction(
+  transport: Transport,
+  transaction: Transaction,
+  derivationPath: Buffer
+): Promise<Buffer> {
+  const paths = Buffer.alloc(1);
+  paths.writeUInt8(1, 0);
+
+  const message = transaction.serializeMessage();
+  const data = Buffer.concat([paths, derivationPath, message]);
+
+  return await send(transport, INS_SIGN_MESSAGE, P1_CONFIRM, data);
+}
+
+/** @internal */
+async function send(
+  transport: Transport,
+  instruction: number,
+  p1: number,
+  data: Buffer
+): Promise<Buffer> {
+  let p2 = 0;
+  let offset = 0;
+
+  if (data.length > MAX_PAYLOAD) {
+    while (data.length - offset > MAX_PAYLOAD) {
+      const buffer = data.subarray(offset, offset + MAX_PAYLOAD);
+      const response = await transport.send(
+        LEDGER_CLA,
+        instruction,
+        p1,
+        p2 | P2_MORE,
+        buffer
+      );
+      if (response.length !== 2)
+        throw TransportStatusError(StatusCodes.INCORRECT_DATA);
+
+      p2 |= P2_EXTEND;
+      offset += MAX_PAYLOAD;
+    }
+  }
+
+  const buffer = data.subarray(offset);
+  const response = await transport.send(
+    LEDGER_CLA,
+    instruction,
+    p1,
+    p2,
+    buffer
+  );
+
+  return response.subarray(0, response.length - 2);
+}

+ 4 - 0
package-lock.json

@@ -1356,6 +1356,8 @@
       "license": "ISC",
       "dependencies": {
         "@coral-xyz/anchor": "^0.26.0",
+        "@ledgerhq/hw-transport": "^6.27.10",
+        "@ledgerhq/hw-transport-node-hid": "^6.27.10",
         "@pythnetwork/client": "^2.9.0",
         "@solana/web3.js": "^1.73.0",
         "@sqds/mesh": "^1.0.6",
@@ -85114,6 +85116,8 @@
       "version": "file:governance/xc_admin/packages/xc_admin_cli",
       "requires": {
         "@coral-xyz/anchor": "^0.26.0",
+        "@ledgerhq/hw-transport": "^6.27.10",
+        "@ledgerhq/hw-transport-node-hid": "^6.27.10",
         "@pythnetwork/client": "^2.9.0",
         "@solana/web3.js": "^1.73.0",
         "@sqds/mesh": "^1.0.6",