Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable CLI Support for Ledger Hardware Wallets #47

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions ts/sdk/cli/ledgerWallet.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import assert from 'assert';

import { parseKeypairUrl } from './ledgerWallet';

assert.deepStrictEqual(parseKeypairUrl(''), {
walletId: undefined,
account: undefined,
change: undefined,
});

assert.deepStrictEqual(parseKeypairUrl('usb://ledger'), {
walletId: undefined,
account: undefined,
change: undefined,
});

assert.deepStrictEqual(parseKeypairUrl('usb://ledger?key=1'), {
walletId: undefined,
account: 1,
change: undefined,
});

assert.deepStrictEqual(parseKeypairUrl('usb://ledger?key=1/2'), {
walletId: undefined,
account: 1,
change: 2,
});

assert.deepStrictEqual(
parseKeypairUrl(
'usb://ledger/BsNsvfXqQTtJnagwFWdBS7FBXgnsK8VZ5CmuznN85swK?key=0'
),
{
walletId: 'BsNsvfXqQTtJnagwFWdBS7FBXgnsK8VZ5CmuznN85swK',
account: 0,
change: undefined,
}
);

assert.deepStrictEqual(
parseKeypairUrl(
'usb://ledger/BsNsvfXqQTtJnagwFWdBS7FBXgnsK8VZ5CmuznN85swK?key=0/0'
),
{
walletId: 'BsNsvfXqQTtJnagwFWdBS7FBXgnsK8VZ5CmuznN85swK',
account: 0,
change: 0,
}
);
111 changes: 111 additions & 0 deletions ts/sdk/cli/ledgerWallet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import Solana from '@ledgerhq/hw-app-solana';
import type { default as Transport } from '@ledgerhq/hw-transport';
import TransportNodeHid from '@ledgerhq/hw-transport-node-hid';
import { getDevices } from '@ledgerhq/hw-transport-node-hid-noevents';
import {
LedgerWalletAdapter,
getDerivationPath,
} from '@solana/wallet-adapter-ledger';
import { PublicKey, Keypair } from '@solana/web3.js';
import type { Wallet } from '@drift-labs/sdk';

// Follows solana cli url format
// usb://<MANUFACTURER>[/<WALLET_ID>][?key=<ACCOUNT>[/<CHANGE>]]
// See: https://docs.solana.com/wallet-guide/hardware-wallets#specify-a-keypair-url
export const parseKeypairUrl = (
url = ''
): {
walletId?: string;
account?: number;
change?: number;
} => {
const walletId = url.match(/(?<=usb:\/\/ledger\/)(\w+)?/)?.[0];
const [account, change] = (url.split('?key=')[1]?.split('/') ?? []).map(
Number
);
return {
walletId,
account,
change,
};
};

async function getPublicKey(
transport: Transport,
account?: number,
change?: number
): Promise<PublicKey> {
const path =
"44'/501'" + // Following BIP44 standard
(account !== undefined ? `/${account}` : '') +
(change !== undefined ? `/${change}` : '');

let { address } = await new Solana(transport).getAddress(path);
return new PublicKey(new Uint8Array(address));
}

/*
* Returns a Drift compatible wallet backed by ledger hardware device
* This only works in an nodejs environment, based on the transport used
*
* Key derivation path is set based on:
* https://docs.solana.com/wallet-guide/hardware-wallets
*/
export async function getLedgerWallet(url = ''): Promise<Wallet> {
const { account, change, walletId } = parseKeypairUrl(url);

const derivationPath = getDerivationPath(account, change);

// Load the first device
let transport = await TransportNodeHid.open('');

// If walletId is specified, we need to loop and correct device.
if (walletId) {
const devices = getDevices();
let correctDeviceFound = false;

for (let device of devices) {
// Wallet id is the public key of the device (with no account or change)
const connectedWalletId = await getPublicKey(
transport,
undefined,
undefined
);

if (connectedWalletId.toString() === walletId) {
correctDeviceFound = true;
break;
}

transport.close();
transport = await TransportNodeHid.open(device.path);
}

if (!correctDeviceFound) {
throw new Error('Wallet not found');
}
}

const publicKey = await getPublicKey(transport, account, change);

// We can reuse the existing ledger wallet adapter
// But we need to inject/hack in our own transport (as we not a browser)
const wallet = new LedgerWalletAdapter({ derivationPath });

// Do some hacky things to get the wallet to work
// These are all done in the `connect` of the ledger wallet adapter
wallet['_transport'] = transport;
wallet['_publicKey'] = publicKey;
transport.on('disconnect', wallet['_disconnected']);
wallet.emit('connect', publicKey);

// Return a Drift compatible wallet
return {
payer: undefined as unknown as Keypair, // Doesn't appear to break things
publicKey: publicKey,
signTransaction: wallet.signTransaction.bind(wallet),
signVersionedTransaction: wallet.signTransaction.bind(wallet),
signAllTransactions: wallet.signAllTransactions.bind(wallet),
signAllVersionedTransactions: wallet.signAllTransactions.bind(wallet),
};
}
15 changes: 9 additions & 6 deletions ts/sdk/cli/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Connection, Keypair } from "@solana/web3.js";
import { AnchorProvider } from "@coral-xyz/anchor";
import * as anchor from '@coral-xyz/anchor';
import { IDL } from "../src/types/drift_vaults";
import { getLedgerWallet } from "./ledgerWallet";

export async function printVault(slot: number, driftClient: DriftClient, vault: Vault, vaultEquity: BN, spotMarket: SpotMarketAccount, spotOracle: OraclePriceData) {

Expand Down Expand Up @@ -117,20 +118,22 @@ export async function getCommandContext(program: Command, needToSign: boolean):

const opts = program.opts();

let keypair: Keypair;
if (needToSign) {
let wallet: Wallet;
if (!needToSign) {
wallet = new Wallet(Keypair.generate());
} else if (opts.keypair.startsWith('usb://ledger')) {
wallet = await getLedgerWallet(opts.keypair) as unknown as Wallet;
} else {
try {
console.log(opts.keypair);
keypair = loadKeypair(opts.keypair as string);
const keypair = loadKeypair(opts.keypair as string);
wallet = new Wallet(keypair);
} catch (e) {
console.error(`Need to provide a valid keypair: ${e}`);
process.exit(1);
}
} else {
keypair = Keypair.generate();
}

const wallet = new Wallet(keypair);
if (needToSign) {
console.log(`Signing wallet address: `, wallet.publicKey.toBase58());
}
Expand Down
6 changes: 6 additions & 0 deletions ts/sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@
"directories": {
"lib": "lib"
},
"devDependencies": {
"@ledgerhq/hw-app-solana": "^7.1.1",
"@ledgerhq/hw-transport": "^6.30.1",
"@ledgerhq/hw-transport-node-hid": "^6.28.1",
"@solana/wallet-adapter-ledger": "^0.9.25"
},
"dependencies": {
"@coral-xyz/anchor": "^0.26.0",
"@drift-labs/competitions-sdk": "0.2.325",
Expand Down
Loading