From 253e17836e853743803774e97bad3a332ed5455f Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Alapont Date: Thu, 2 Feb 2023 15:12:31 -0600 Subject: [PATCH 1/5] Add ledger support --- .../packages/xc_admin_cli/package.json | 2 + .../packages/xc_admin_cli/src/index.ts | 57 ++++++++++++++----- package-lock.json | 4 ++ 3 files changed, 49 insertions(+), 14 deletions(-) diff --git a/governance/xc_admin/packages/xc_admin_cli/package.json b/governance/xc_admin/packages/xc_admin_cli/package.json index c35df412f1..ab3e7eb629 100644 --- a/governance/xc_admin/packages/xc_admin_cli/package.json +++ b/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", diff --git a/governance/xc_admin/packages/xc_admin_cli/src/index.ts b/governance/xc_admin/packages/xc_admin_cli/src/index.ts index 03bcf15e2e..3636d8fb5f 100644 --- a/governance/xc_admin/packages/xc_admin_cli/src/index.ts +++ b/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 { + 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 ", "solana cluster to use") - .requiredOption("-w, --wallet ", "path to the operations key") - .requiredOption("-v, --vault ", "multisig address"); + .requiredOption( + "-w, --wallet ", + 'path to the operations key or "ledger"' + ) + .requiredOption("-v, --vault ", "multisig address") + .option( + "-lda, --ledger-derivation-account ", + "ledger derivation account to use" + ) + .option( + "-ldc, --ledger-derivation-change ", + "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); @@ -128,10 +157,10 @@ mutlisigCommand("upgrade-program", "Upgrade a program from a buffer") .requiredOption("-b, --buffer ", "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); @@ -186,10 +215,10 @@ mutlisigCommand( .requiredOption("-p, --price ", "Price account to modify") .requiredOption("-e, --exponent ", "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); diff --git a/package-lock.json b/package-lock.json index d32a7cdd6d..7bb8c76e8d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1353,6 +1353,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", @@ -85098,6 +85100,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": "*", "@pythnetwork/client": "^2.9.0", "@solana/web3.js": "^1.73.0", "@sqds/mesh": "^1.0.6", From 17fcc52ddeb75ff39aafcdf0a8b277953771e530 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Alapont Date: Thu, 2 Feb 2023 15:28:44 -0600 Subject: [PATCH 2/5] Checkpoint --- .../packages/xc_admin_cli/src/index.ts | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/governance/xc_admin/packages/xc_admin_cli/src/index.ts b/governance/xc_admin/packages/xc_admin_cli/src/index.ts index 3636d8fb5f..0b88de87c9 100644 --- a/governance/xc_admin/packages/xc_admin_cli/src/index.ts +++ b/governance/xc_admin/packages/xc_admin_cli/src/index.ts @@ -36,7 +36,7 @@ export async function loadHotWalletOrLedger( lda: number, ldc: number ): Promise { - if ((wallet = "ledger")) { + if (wallet === "ledger") { return await LedgerNodeWallet.createWallet(lda, ldc); } else { return new NodeWallet( @@ -251,7 +251,10 @@ program .command("parse-transaction") .description("Parse a transaction sitting in the multisig") .requiredOption("-c, --cluster ", "solana cluster to use") - .requiredOption("-t, --transaction ", "path to the operations key") + .requiredOption( + "-t, --transaction ", + "address of the outstanding transaction" + ) .action(async (options: any) => { const cluster = options.cluster; const transaction: PublicKey = new PublicKey(options.transaction); @@ -274,4 +277,22 @@ program console.log(JSON.stringify(parsed, null, 2)); }); +mutlisigCommand("approve", "Approve a transaction sitting in the multisig") + .requiredOption( + "-t, --transaction ", + "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(); From b6b75f50ab192a655aed580242103f4889b7b779 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Alapont Date: Thu, 2 Feb 2023 15:59:30 -0600 Subject: [PATCH 3/5] Checkpoint --- .../packages/crank_executor/src/index.ts | 21 ++- .../packages/xc_admin_cli/src/index.ts | 8 +- .../packages/xc_admin_cli/src/ledger.ts | 163 ++++++++++++++++++ 3 files changed, 183 insertions(+), 9 deletions(-) create mode 100644 governance/xc_admin/packages/xc_admin_cli/src/ledger.ts diff --git a/governance/xc_admin/packages/crank_executor/src/index.ts b/governance/xc_admin/packages/crank_executor/src/index.ts index 62c23c4455..98ab57a2f0 100644 --- a/governance/xc_admin/packages/crank_executor/src/index.ts +++ b/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.log(error); await squad.cancelTransaction(proposal.publicKey); + console.log("Cancelled: ", proposal.publicKey.toBase58()); } break; } } + } else { + console.log("Skipping: ", proposal.publicKey.toBase58()); } } } diff --git a/governance/xc_admin/packages/xc_admin_cli/src/index.ts b/governance/xc_admin/packages/xc_admin_cli/src/index.ts index 0b88de87c9..0a44195c0c 100644 --- a/governance/xc_admin/packages/xc_admin_cli/src/index.ts +++ b/governance/xc_admin/packages/xc_admin_cli/src/index.ts @@ -133,7 +133,7 @@ mutlisigCommand( .accept() .accounts({ currentAuthority: current, - newAuthority: mapKey(vaultAuthority), + newAuthority: isRemote ? mapKey(vaultAuthority) : vaultAuthority, programAccount: programId, programDataAccount, bpfUpgradableLoader: BPF_UPGRADABLE_LOADER, @@ -195,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, + }, ], }; diff --git a/governance/xc_admin/packages/xc_admin_cli/src/ledger.ts b/governance/xc_admin/packages/xc_admin_cli/src/ledger.ts new file mode 100644 index 0000000000..984977f1de --- /dev/null +++ b/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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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); +} From fdb50ab49ae6c4aa1d0d76c72bb8195dc5862765 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Alapont Date: Thu, 2 Feb 2023 16:40:12 -0600 Subject: [PATCH 4/5] Package locK --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 7bb8c76e8d..bace814448 100644 --- a/package-lock.json +++ b/package-lock.json @@ -85101,7 +85101,7 @@ "requires": { "@coral-xyz/anchor": "^0.26.0", "@ledgerhq/hw-transport": "^6.27.10", - "@ledgerhq/hw-transport-node-hid": "*", + "@ledgerhq/hw-transport-node-hid": "^6.27.10", "@pythnetwork/client": "^2.9.0", "@solana/web3.js": "^1.73.0", "@sqds/mesh": "^1.0.6", From 4b5e9f8aa6305f93f0b5237afe6920cf8956e941 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Alapont Date: Fri, 3 Feb 2023 12:06:49 -0600 Subject: [PATCH 5/5] Console err --- governance/xc_admin/packages/crank_executor/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/governance/xc_admin/packages/crank_executor/src/index.ts b/governance/xc_admin/packages/crank_executor/src/index.ts index 98ab57a2f0..4e02d0e251 100644 --- a/governance/xc_admin/packages/crank_executor/src/index.ts +++ b/governance/xc_admin/packages/crank_executor/src/index.ts @@ -117,7 +117,7 @@ async function run() { } catch (error) { // Mark the transaction as cancelled if we failed to run it if (error instanceof SendTransactionError) { - console.log(error); + console.error(error); await squad.cancelTransaction(proposal.publicKey); console.log("Cancelled: ", proposal.publicKey.toBase58()); }