From b36b5c79b41dc3451a7de6ae1219f97b2983dba1 Mon Sep 17 00:00:00 2001 From: Ondra Chaloupka Date: Fri, 21 Jul 2023 19:27:09 +0200 Subject: [PATCH] ledger parsing to implement Wallet interface --- src/context.ts | 9 +++---- src/utils/cliParser.ts | 10 ++++---- src/utils/ledger.ts | 53 +++++++++++++++++++++++++++++++++------ src/utils/transactions.ts | 15 ++++------- 4 files changed, 59 insertions(+), 28 deletions(-) diff --git a/src/context.ts b/src/context.ts index 12c6fab..52f813c 100644 --- a/src/context.ts +++ b/src/context.ts @@ -1,12 +1,11 @@ import { Commitment, Connection, clusterApiUrl, Cluster } from '@solana/web3.js' import { Logger } from 'pino' -import { SolanaLedger } from './utils/ledger' -import { Wallet } from '@coral-xyz/anchor' +import { Wallet } from '@coral-xyz/anchor/dist/cjs/provider' import { MarinadeConfig } from '@marinade.finance/marinade-ts-sdk' export interface Context { connection: Connection - walletSigner: SolanaLedger | Wallet + walletSigner: Wallet logger: Logger skipPreflight: boolean simulate: boolean @@ -17,7 +16,7 @@ export interface Context { const context: { connection: Connection | null - walletSigner: SolanaLedger | Wallet | null + walletSigner: Wallet | null logger: Logger | null skipPreflight: boolean simulate: boolean @@ -92,7 +91,7 @@ export const setContext = ({ command, }: { url: string - walletSigner: SolanaLedger | Wallet + walletSigner: Wallet simulate: boolean printOnly: boolean skipPreflight: boolean diff --git a/src/utils/cliParser.ts b/src/utils/cliParser.ts index a3f0f41..d407fcb 100644 --- a/src/utils/cliParser.ts +++ b/src/utils/cliParser.ts @@ -1,9 +1,9 @@ -import { Wallet } from '@coral-xyz/anchor' +import { Wallet } from '@coral-xyz/anchor/dist/cjs/provider' import NodeWallet from '@coral-xyz/anchor/dist/cjs/nodewallet' import { Keypair, PublicKey } from '@solana/web3.js' import expandTilde from 'expand-tilde' import { readFile } from 'fs/promises' -import { CLI_LEDGER_URL_PREFIX, SolanaLedger } from './ledger' +import { CLI_LEDGER_URL_PREFIX, LedgerWallet } from './ledger' import { LockedDeviceError, TransportError, @@ -23,7 +23,7 @@ export async function parsePubkey(pubkeyOrPath: string): Promise { export async function parsePubkeyOrSigner( pubkeyOrPath: string, logger: Logger -): Promise { +): Promise { try { return new PublicKey(pubkeyOrPath) } catch (err) { @@ -37,13 +37,13 @@ export async function parsePubkeyOrSigner( export async function parseSigner( pathOrUrl: string, logger: Logger -): Promise { +): Promise { pathOrUrl = pathOrUrl.trim() // trying ledger (https://docs.solana.com/wallet-guide/hardware-wallets/ledger) if (pathOrUrl.startsWith(CLI_LEDGER_URL_PREFIX)) { try { - const solanaLedger = await SolanaLedger.instance(pathOrUrl) + const solanaLedger = await LedgerWallet.instance(pathOrUrl) logger.debug( 'Successfully connected to Ledger device of key %s', solanaLedger.publicKey.toBase58() diff --git a/src/utils/ledger.ts b/src/utils/ledger.ts index d34ac3e..884a5ce 100644 --- a/src/utils/ledger.ts +++ b/src/utils/ledger.ts @@ -1,27 +1,34 @@ +import { Wallet } from '@coral-xyz/anchor/dist/cjs/provider' import Solana from '@ledgerhq/hw-app-solana' import TransportNodeHid, { getDevices, } from '@ledgerhq/hw-transport-node-hid-noevents' -import { MessageV0, PublicKey, Message } from '@solana/web3.js' +import { + MessageV0, + PublicKey, + Message, + Transaction, + VersionedTransaction, +} from '@solana/web3.js' export const CLI_LEDGER_URL_PREFIX = 'usb://ledger' export const SOLANA_LEDGER_BIP44_BASE_PATH = "44'/501'/" export const SOLANA_LEDGER_BIP44_BASE_REGEXP = /^44[']{0,1}\/501[']{0,1}\// export const DEFAULT_DERIVATION_PATH = SOLANA_LEDGER_BIP44_BASE_PATH + "0'/0'" -export class SolanaLedger { +export class LedgerWallet implements Wallet { /** * "Constructor" of SolanaLedger class. * From ledger url in format of usb://ledger[/[?key=] * creates wrapper class around Solana ledger device from '@ledgerhq/hw-app-solana' package. */ - static async instance(ledgerUrl = '0'): Promise { + static async instance(ledgerUrl = '0'): Promise { const { pubkey, derivedPath } = parseLedgerUrl(ledgerUrl) - const solanaApi = await SolanaLedger.findByPubkey(pubkey, derivedPath) - const publicKey = await SolanaLedger.getPublicKey(solanaApi, derivedPath) + const solanaApi = await LedgerWallet.findByPubkey(pubkey, derivedPath) + const publicKey = await LedgerWallet.getPublicKey(solanaApi, derivedPath) - return new SolanaLedger(solanaApi, derivedPath, publicKey) + return new LedgerWallet(solanaApi, derivedPath, publicKey) } private constructor( @@ -30,6 +37,27 @@ export class SolanaLedger { public readonly publicKey: PublicKey ) {} + public async signTransaction( + tx: T + ): Promise { + let message: Message | MessageV0 + if (tx instanceof Transaction) { + message = tx.compileMessage() + } else { + message = tx.message + } + const signature = await this.signMessage(message) + tx.addSignature(this.publicKey, signature) + return tx + } + + public async signAllTransactions< + T extends Transaction | VersionedTransaction + >(txs: T[]): Promise { + const signedTxs = await Promise.all(txs.map(tx => this.signTransaction(tx))) + return signedTxs + } + private static async getPublicKey( solanaApi: Solana, derivedPath: string @@ -57,7 +85,7 @@ export class SolanaLedger { for (const device of ledgerDevices) { transport = await TransportNodeHid.open(device.path) const solanaApi = new Solana(transport) - const ledgerPubkey = await SolanaLedger.getPublicKey( + const ledgerPubkey = await LedgerWallet.getPublicKey( solanaApi, derivedPath ) @@ -85,7 +113,7 @@ export class SolanaLedger { * ) * ``` */ - public async signMessage(message: MessageV0 | Message): Promise { + private async signMessage(message: MessageV0 | Message): Promise { const { signature } = await this.solanaApi.signTransaction( this.derivedPath, Buffer.from(message.serialize()) @@ -166,3 +194,12 @@ export function parseLedgerUrl(ledgerUrl: string): { return { pubkey, derivedPath } } + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function instanceOfWallet(object: any): object is Wallet { + return ( + 'signTransaction' in object && + 'signAllTransactions' in object && + 'publicKey' in object + ) +} diff --git a/src/utils/transactions.ts b/src/utils/transactions.ts index de47155..7c9ffe7 100644 --- a/src/utils/transactions.ts +++ b/src/utils/transactions.ts @@ -1,6 +1,5 @@ import { serializeInstructionToBase64 } from '@solana/spl-governance' -import { Wallet } from '@coral-xyz/anchor' -import { SolanaLedger } from './ledger' +import { Wallet } from '@coral-xyz/anchor/dist/cjs/provider' import { Connection, Transaction, @@ -9,6 +8,7 @@ import { SendTransactionError, Keypair, } from '@solana/web3.js' +import { instanceOfWallet } from './ledger' import { Logger } from 'pino' import { CliCommandError } from './error' @@ -23,7 +23,7 @@ export async function executeTx({ }: { connection: Connection transaction: Transaction - signers: (SolanaLedger | Wallet | Keypair)[] + signers: (Wallet | Keypair)[] errMessage: string simulate?: boolean printOnly?: boolean @@ -49,13 +49,8 @@ export async function executeTx({ transaction.feePayer = transaction.feePayer ?? signers[0].publicKey for (const signer of signers) { - if (signer instanceof SolanaLedger) { - const message = transaction.compileMessage() - const ledgerSignature = await signer.signMessage(message) - transaction.addSignature(signer.publicKey, ledgerSignature) - } else if (signer instanceof Wallet) { - // NOTE: Anchor NodeWallet does partial signing by this call - await signer.signTransaction(transaction) + if (instanceOfWallet(signer)) { + await signer.signTransaction(transaction) // partial signing by this call } else { transaction.partialSign(signer) }