Skip to content

Commit

Permalink
ledger parsing to implement Wallet interface
Browse files Browse the repository at this point in the history
  • Loading branch information
ochaloup committed Jul 21, 2023
1 parent 0cfe5b1 commit b36b5c7
Show file tree
Hide file tree
Showing 4 changed files with 59 additions and 28 deletions.
9 changes: 4 additions & 5 deletions src/context.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -92,7 +91,7 @@ export const setContext = ({
command,
}: {
url: string
walletSigner: SolanaLedger | Wallet
walletSigner: Wallet
simulate: boolean
printOnly: boolean
skipPreflight: boolean
Expand Down
10 changes: 5 additions & 5 deletions src/utils/cliParser.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -23,7 +23,7 @@ export async function parsePubkey(pubkeyOrPath: string): Promise<PublicKey> {
export async function parsePubkeyOrSigner(
pubkeyOrPath: string,
logger: Logger
): Promise<PublicKey | Wallet | SolanaLedger> {
): Promise<PublicKey | Wallet> {
try {
return new PublicKey(pubkeyOrPath)
} catch (err) {
Expand All @@ -37,13 +37,13 @@ export async function parsePubkeyOrSigner(
export async function parseSigner(
pathOrUrl: string,
logger: Logger
): Promise<Wallet | SolanaLedger> {
): Promise<Wallet> {
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()
Expand Down
53 changes: 45 additions & 8 deletions src/utils/ledger.ts
Original file line number Diff line number Diff line change
@@ -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[/<pubkey>[?key=<number>]
* creates wrapper class around Solana ledger device from '@ledgerhq/hw-app-solana' package.
*/
static async instance(ledgerUrl = '0'): Promise<SolanaLedger> {
static async instance(ledgerUrl = '0'): Promise<LedgerWallet> {
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(
Expand All @@ -30,6 +37,27 @@ export class SolanaLedger {
public readonly publicKey: PublicKey
) {}

public async signTransaction<T extends Transaction | VersionedTransaction>(
tx: T
): Promise<T> {
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<T[]> {
const signedTxs = await Promise.all(txs.map(tx => this.signTransaction(tx)))
return signedTxs
}

private static async getPublicKey(
solanaApi: Solana,
derivedPath: string
Expand Down Expand Up @@ -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
)
Expand Down Expand Up @@ -85,7 +113,7 @@ export class SolanaLedger {
* )
* ```
*/
public async signMessage(message: MessageV0 | Message): Promise<Buffer> {
private async signMessage(message: MessageV0 | Message): Promise<Buffer> {
const { signature } = await this.solanaApi.signTransaction(
this.derivedPath,
Buffer.from(message.serialize())
Expand Down Expand Up @@ -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
)
}
15 changes: 5 additions & 10 deletions src/utils/transactions.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -9,6 +8,7 @@ import {
SendTransactionError,
Keypair,
} from '@solana/web3.js'
import { instanceOfWallet } from './ledger'
import { Logger } from 'pino'
import { CliCommandError } from './error'

Expand All @@ -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
Expand All @@ -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)
}
Expand Down

0 comments on commit b36b5c7

Please sign in to comment.