import { default as TransportUSB } from '@ledgerhq/hw-transport-webusb'
import { Block } from '../block'
import { BIP44_COIN_NANO, BIP44_PURPOSE, HARDENED_OFFSET } from '../constants'
-import { bytes, dec, utf8 } from '../convert'
+import { dec, utf8 } from '../convert'
import { Rpc } from '../rpc'
import { Wallet } from '../wallet'
import { _account } from './account'
import { _connect } from './connect'
import { queue } from './queue'
import { signBlock, signNonce } from './sign'
+import { _open } from './open'
+import { _close } from './close'
export type LedgerStatus = 'UNSUPPORTED' | 'DISCONNECTED' | 'BUSY' | 'LOCKED' | 'CONNECTED'
export type LedgerTransport = typeof TransportHID | typeof TransportBLE | typeof TransportUSB
signNonce: 0x05,
paramUnused: 0x00
})
+
export const DERIVATION_PATH: Uint8Array = new Uint8Array([
APDU_CODES.bip32DerivationLevel,
...dec.toBytes(BIP44_PURPOSE + HARDENED_OFFSET, 4),
...dec.toBytes(BIP44_COIN_NANO + HARDENED_OFFSET, 4)
])
+
export const STATUS_CODES: Readonly<Record<number, string>> = Object.freeze({
...Object.fromEntries(Object.entries(StatusCodes).map(([k, v]) => [+v, k])),
0x6807: 'APPLICATION_NOT_INSTALLED',
0x6a81: 'INVALID_SIGNATURE',
0x6a82: 'CACHE_MISS'
})
+
export const listenTimeout: 30000 = 30000
+
export const openTimeout: 3000 = 3000
/**
-* Ledger hardware wallet created by communicating with a Ledger device via ADPU
-* calls. This wallet does not feature any seed nor mnemonic phrase as all
-* private keys are held in the secure chip of the device. As such, the user
-* is responsible for using Ledger technology to back up these pieces of data.
-*
-* https://github.com/roosmaa/ledger-app-nano/blob/master/doc/nano.md
-*/
+ * Ledger hardware wallet created by communicating with a Ledger device via ADPU
+ * calls. This wallet does not feature any seed nor mnemonic phrase as all
+ * private keys are held in the secure chip of the device. As such, the user
+ * is responsible for using Ledger technology to back up these pieces of data.
+ *
+ * https://github.com/roosmaa/ledger-app-nano/blob/master/doc/nano.md
+ */
export class Ledger {
static #isPolling: boolean = false
static #status: LedgerStatus = 'DISCONNECTED'
static removeEventListener = this.#eventTarget.removeEventListener.bind(this.#eventTarget)
/**
- * Check which transport protocols are supported by the browser and return the
- * transport type according to the following priorities: HID, Bluetooth, USB.
- */
+ * Check which transport protocols are supported by the browser and return the
+ * transport type according to the following priorities: HID, Bluetooth, USB.
+ */
static get isUnsupported (): boolean {
if (this.#status === 'UNSUPPORTED') {
return true
}
/**
- * Vendor ID assigned to Ledger for HID and USB interfaces.
- * https://github.com/LedgerHQ/ledger-live/blob/develop/libs/ledgerjs/packages/devices/src/index.ts#L164
- */
+ * Vendor ID assigned to Ledger for HID and USB interfaces.
+ * https://github.com/LedgerHQ/ledger-live/blob/develop/libs/ledgerjs/packages/devices/src/index.ts#L164
+ */
static get ledgerVendorId (): 0x2c97 {
return 0x2c97
}
/**
- * Status of the Ledger device connection.
- *
- * UNSUPPORTED | DISCONNECTED | BUSY | LOCKED | CONNECTED
- */
+ * Status of the Ledger device connection.
+ *
+ * UNSUPPORTED | DISCONNECTED | BUSY | LOCKED | CONNECTED
+ */
static get status (): LedgerStatus {
return this.isUnsupported ? 'UNSUPPORTED' : this.#status
}
}
/**
- * Clears Ledger connections from HID and USB interfaces and stops polling for
- * connection updates.
- */
+ * Clears Ledger connections from HID and USB interfaces and stops polling for
+ * connection updates.
+ */
static async disconnect (): Promise<void> {
queue(async () => {
try {
}
/**
- * Sign a nonce with the Ledger device. The nonce must be a string that encodes
- * to 16 bytes using UTF-8. For this reason, although any Unicode characters
- * can be used, it is recommended to only pass printable ASCII which encodes to
- * one byte per character.
- *
- * The actual message signed is a string which can be expressed as the
- * following template literal:
- *
- * `Nano Signed Nonce:\n${nonce_bytes_as_hex}`
- *
- * IMPORTANT: The current version of the Nano app for Ledger devices will NOT
- * prompt users to confirm the signature. If valid, the nonce will immediately
- * be signed and the signature returned without user interaction, similar to
- * how receive blocks are automatically signed when auto-receive is configured.
- * Plan for this eventuality if you implement this method to sign nonces.
- *
- * @param {number} index - Account number
- * @param {string} nonce - 128-bit value to sign
- */
+ * Sign a nonce with the Ledger device. The nonce must be a string that encodes
+ * to 16 bytes using UTF-8. For this reason, although any Unicode characters
+ * can be used, it is recommended to only pass printable ASCII which encodes to
+ * one byte per character.
+ *
+ * The actual message signed is a string which can be expressed as the
+ * following template literal:
+ *
+ * `Nano Signed Nonce:\n${nonce_bytes_as_hex}`
+ *
+ * IMPORTANT: The current version of the Nano app for Ledger devices will NOT
+ * prompt users to confirm the signature. If valid, the nonce will immediately
+ * be signed and the signature returned without user interaction, similar to
+ * how receive blocks are automatically signed when auto-receive is configured.
+ * Plan for this eventuality if you implement this method to sign nonces.
+ *
+ * @param {number} index - Account number
+ * @param {string} nonce - 128-bit value to sign
+ */
static async sign (index: number, nonce: string): Promise<string>
/**
- * Sign a block with the Ledger device.
- *
- * @param {number} index - Account number
- * @param {Block} block - Block data to sign
- * @param {Block} [frontier] - Previous block data to cache in the device
- */
+ * Sign a block with the Ledger device.
+ *
+ * @param {number} index - Account number
+ * @param {Block} block - Block data to sign
+ * @param {Block} [frontier] - Previous block data to cache in the device
+ */
static async sign (index: number, block: Block, frontier?: Block): Promise<string>
static async sign (index: number, data: string | Block, frontier?: Block): Promise<string> {
try {
}
/**
- * Checks whether a given seed matches the wallet seed. The wallet must be
- * unlocked prior to verification.
- *
- * @param {string} seed - Hexadecimal seed to be matched against the wallet data
- * @returns True if input matches wallet seed
- */
+ * Checks whether a given seed matches the wallet seed. The wallet must be
+ * unlocked prior to verification.
+ *
+ * @param {string} seed - Hexadecimal seed to be matched against the wallet data
+ * @returns True if input matches wallet seed
+ */
static async verify (seed: string): Promise<boolean>
/**
- * Checks whether a given mnemonic phrase matches the wallet mnemonic. If a
- * personal salt was used when generating the mnemonic, it cannot be verified.
- * The wallet must be unlocked prior to verification.
- *
- * @param {string} mnemonic - Phrase to be matched against the wallet data
- * @returns True if input matches wallet mnemonic
- */
+ * Checks whether a given mnemonic phrase matches the wallet mnemonic. If a
+ * personal salt was used when generating the mnemonic, it cannot be verified.
+ * The wallet must be unlocked prior to verification.
+ *
+ * @param {string} mnemonic - Phrase to be matched against the wallet data
+ * @returns True if input matches wallet mnemonic
+ */
static async verify (mnemonic: string): Promise<boolean>
static async verify (secret: string): Promise<boolean> {
const testWallet = await Wallet.load('BIP-44', '', secret)
}
/**
- * Close the currently running app and return to the device dashboard.
- *
- * https://developers.ledger.com/docs/connectivity/ledgerJS/open-close-info-on-apps#quit-application
- *
- * This command resets the internal USB connection of the device which can
- * cause subsequent commands to fail if called too quickly. A one-second delay
- * is implemented in this method to mitigate the issue.
- *
- * https://github.com/LedgerHQ/ledger-live/issues/4964#issuecomment-1878361157
- *
- * @returns Status of command
- */
+ * Close the currently running app and return to the device dashboard.
+ *
+ * https://developers.ledger.com/docs/connectivity/ledgerJS/open-close-info-on-apps#quit-application
+ *
+ * This command resets the internal USB connection of the device which can
+ * cause subsequent commands to fail if called too quickly. A one-second delay
+ * is implemented in this method to mitigate the issue.
+ *
+ * https://github.com/LedgerHQ/ledger-live/issues/4964#issuecomment-1878361157
+ *
+ * @returns Status of command
+ */
static async #close (): Promise<LedgerResponse> {
- return queue(async () => {
- const transport = await this.#transport.create(openTimeout, listenTimeout)
- const response = await transport
- .send(0xb0, 0xa7, APDU_CODES.paramUnused, APDU_CODES.paramUnused)
- .then((res: Buffer) => bytes.toDec(res))
- .catch((err: any) => err.statusCode)
- .finally(async () => await transport.close()) as number
- return new Promise(r => setTimeout(r, 1000, { status: STATUS_CODES[response] }))
- })
+ return queue(async () => _close(this.#transport))
}
/**
- * Open the Nano app by launching a user flow.
- *
- * https://developers.ledger.com/docs/connectivity/ledgerJS/open-close-info-on-apps#open-application
- *
- * This command resets the internal USB connection of the device which can
- * cause subsequent commands to fail if called too quickly. A one-second delay
- * is implemented in this method to mitigate the issue.
- *
- * https://github.com/LedgerHQ/ledger-live/issues/4964#issuecomment-1878361157
- *
- * @returns Status of command
- */
+ * Open the Nano app by launching a user flow.
+ *
+ * https://developers.ledger.com/docs/connectivity/ledgerJS/open-close-info-on-apps#open-application
+ *
+ * This command resets the internal USB connection of the device which can
+ * cause subsequent commands to fail if called too quickly. A one-second delay
+ * is implemented in this method to mitigate the issue.
+ *
+ * https://github.com/LedgerHQ/ledger-live/issues/4964#issuecomment-1878361157
+ *
+ * @returns Status of command
+ */
static async #open (): Promise<LedgerResponse> {
- return queue(async () => {
- const name = new TextEncoder().encode('Nano')
- const transport = await this.#transport.create(openTimeout, listenTimeout)
- const response = await transport
- .send(0xe0, 0xd8, APDU_CODES.paramUnused, APDU_CODES.paramUnused, name as Buffer)
- .then((res: Buffer) => bytes.toDec(res))
- .catch((err: any) => err.statusCode)
- .finally(async () => await transport.close()) as number
- return new Promise(r => setTimeout(r, 1000, { status: STATUS_CODES[response] }))
- })
+ return queue(async () => _open(this.#transport))
}
/**
- * Checks for connected HID and USB Ledger devices to which access has been
- * previously granted in response to a `requestDevice()` call. It does not work
- * for Bluetooth interfaces.
- *
- * If no devices have been granted access, or if none with access are currently
- * connected, the poll will set the 'DISCONNECTED' status accordingly and
- * return. Otherwise, it will attempt to determine the actual status of the
- * device.
- */
+ * Checks for connected HID and USB Ledger devices to which access has been
+ * previously granted in response to a `requestDevice()` call. It does not work
+ * for Bluetooth interfaces.
+ *
+ * If no devices have been granted access, or if none with access are currently
+ * connected, the poll will set the 'DISCONNECTED' status accordingly and
+ * return. Otherwise, it will attempt to determine the actual status of the
+ * device.
+ */
static async #poll (): Promise<void> {
try {
const isHidPaired = (await navigator.hid?.getDevices?.() ?? [])
}
/**
- * Sets the Ledger status and emits an event.
- */
+ * Sets the Ledger status and emits an event.
+ */
static #setStatus (value: LedgerStatus) {
if (this.#status !== value) {
this.#status = value