async sign (input?: number | string, block?: { [key: string]: string }): Promise<void> {
if (typeof input === 'number') {
const index = input
- const { Ledger } = await import('./ledger')
+ const { Ledger } = await import('./wallets')
const ledger = await Ledger.init()
await ledger.open()
if (block) {
+++ /dev/null
-// SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-// Ledger ADPU commands: https://github.com/roosmaa/ledger-app-nano/blob/master/doc/nano.md
-
-import Transport from '@ledgerhq/hw-transport'
-import { default as TransportBLE } from '@ledgerhq/hw-transport-web-ble'
-import { default as TransportUSB } from '@ledgerhq/hw-transport-webusb'
-import { default as TransportHID } from '@ledgerhq/hw-transport-webhid'
-import { ChangeBlock, ReceiveBlock, SendBlock } from './block'
-import { BIP44_COIN_NANO, BIP44_PURPOSE, HARDENED_OFFSET, LEDGER_ADPU_CODES, LEDGER_STATUS_CODES } from './constants'
-import { bytes, dec, hex, utf8 } from './convert'
-import { Rpc } from './rpc'
-
-interface LedgerResponse {
- status: string
-}
-
-interface LedgerVersionResponse extends LedgerResponse {
- name: string | null,
- version: string | null
-}
-
-interface LedgerAccountResponse extends LedgerResponse {
- publicKey: string | null,
- address: string | null
-}
-
-interface LedgerSignResponse extends LedgerResponse {
- signature: string | null,
- hash?: string
-}
-
-export class Ledger {
- static #isInternal: boolean = false
- #status: 'DISCONNECTED' | 'LOCKED' | 'BUSY' | 'CONNECTED' = 'DISCONNECTED'
- get status () { return this.#status }
- openTimeout = 3000
- listenTimeout = 30000;
- transport: Transport | null = null
- DynamicTransport: typeof TransportBLE | typeof TransportUSB | typeof TransportHID = TransportHID
-
- constructor () {
- if (!Ledger.#isInternal) {
- throw new Error('Ledger cannot be instantiated directly. Use Ledger.init()')
- }
- Ledger.#isInternal = false
- }
-
- static async init (): Promise<Ledger> {
- Ledger.#isInternal = true
- const self = new this()
- await self.checkBrowserSupport()
- await self.listen()
- return self
- }
-
- /**
- * Check which transport protocols are supported by the browser and set the
- * transport type according to the following priorities: Bluetooth, USB, HID.
- */
- async checkBrowserSupport (): Promise<void> {
- console.log('Checking browser Ledger support...')
- const supports = {
- ble: await TransportBLE.isSupported(),
- usb: await TransportUSB.isSupported(),
- hid: await TransportHID.isSupported()
- }
- console.log(`ble: ${supports.ble}; usb: ${supports.usb}; hid: ${supports.hid}`)
- if (supports.ble) {
- this.DynamicTransport = TransportBLE
- } else if (supports.usb) {
- this.DynamicTransport = TransportUSB
- } else if (supports.hid) {
- this.DynamicTransport = TransportHID
- } else {
- throw new Error('Unsupported browser')
- }
- }
-
- async listen (): Promise<void> {
- const { usb } = globalThis.navigator
- if (usb) {
- usb.addEventListener('connect', console.log.bind(console))
- usb.addEventListener('disconnect', console.log.bind(console))
- }
- }
-
- async connect (): Promise<string> {
- const { usb } = globalThis.navigator
- if (usb) {
- usb.removeEventListener('disconnect', this.onDisconnectUsb.bind(this))
- usb.addEventListener('disconnect', this.onDisconnectUsb.bind(this))
- }
- const version = await this.version()
- if (version.status === 'OK') {
- if (version.name === 'Nano') {
- const account = await this.account()
- if (account.status === 'OK') {
- this.#status = 'CONNECTED'
- } else if (account.status === 'SECURITY_STATUS_NOT_SATISFIED') {
- this.#status = 'LOCKED'
- } else {
- this.#status = 'DISCONNECTED'
- }
- } else if (version.name === 'BOLOS') {
- const open = await this.open()
- this.#status = (open.status === 'OK')
- ? 'CONNECTED'
- : 'DISCONNECTED'
- } else {
- this.#status = 'BUSY'
- }
- } else {
- this.#status = 'DISCONNECTED'
- }
- return this.status
- }
-
- async onDisconnectUsb (e: USBConnectionEvent): Promise<void> {
- if (e.device?.manufacturerName === 'Ledger') {
- const { usb } = globalThis.navigator
- usb.removeEventListener('disconnect', this.onDisconnectUsb)
- this.#status = 'DISCONNECTED'
- }
- }
-
- /**
- * Open Nano app by launching 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
- */
- async open (): Promise<LedgerResponse> {
- const name = new TextEncoder().encode('Nano')
- const transport = await this.DynamicTransport.create(this.openTimeout, this.listenTimeout)
- const response = await transport.send(0xe0, 0xd8, LEDGER_ADPU_CODES.paramUnused, LEDGER_ADPU_CODES.paramUnused, name as Buffer)
- .then(res => bytes.toDec(res))
- .catch(err => err.statusCode) as number
- return new Promise(resolve => setTimeout(resolve, 1000, { status: LEDGER_STATUS_CODES[response] }))
- }
-
- /**
- * Close the currently running app.
- *
- * 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
- */
- async close (): Promise<LedgerResponse> {
- const transport = await this.DynamicTransport.create(this.openTimeout, this.listenTimeout)
- const response = await transport.send(0xb0, 0xa7, LEDGER_ADPU_CODES.paramUnused, LEDGER_ADPU_CODES.paramUnused)
- .then(res => bytes.toDec(res))
- .catch(err => err.statusCode) as number
- return new Promise(resolve => setTimeout(resolve, 1000, { status: LEDGER_STATUS_CODES[response] }))
- }
-
- /**
- * Get the version of the current process. If a specific app is running, get
- * the app version. Otherwise, get the Ledger BOLOS version instead.
- *
- * https://developers.ledger.com/docs/connectivity/ledgerJS/open-close-info-on-apps#get-information
- *
- * @returns Status, process name, and version
- */
- async version (): Promise<LedgerVersionResponse> {
- const transport = await this.DynamicTransport.create(this.openTimeout, this.listenTimeout)
- const response = await transport.send(0xb0, LEDGER_ADPU_CODES.version, LEDGER_ADPU_CODES.paramUnused, LEDGER_ADPU_CODES.paramUnused)
- .catch(err => dec.toBytes(err.statusCode)) as Uint8Array
- await transport.close()
-
- if (response.length === 2) {
- const statusCode = bytes.toDec(response) as number
- const status = LEDGER_STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR'
- return { status, name: null, version: null }
- }
-
- const nameLength = response[1]
- const name = response.slice(2, 2 + nameLength).toString()
- const versionLength = response[2 + nameLength]
- const version = response.slice(2 + nameLength + 1, 2 + nameLength + 1 + versionLength).toString()
- const statusCode = bytes.toDec(response.slice(-2)) as number
-
- const status = LEDGER_STATUS_CODES[statusCode]
- return { status, name, version }
- }
-
- /**
- * Get an account at a specific BIP-44 index.
- *
- * @returns Response object containing command status, public key, and address
- */
- async account (index: number = 0, show: boolean = false): Promise<LedgerAccountResponse> {
- if (typeof index !== 'number' || index < 0 || index >= HARDENED_OFFSET) {
- throw new TypeError('Invalid account index')
- }
- const purpose = dec.toBytes(BIP44_PURPOSE + HARDENED_OFFSET, 4)
- const coin = dec.toBytes(BIP44_COIN_NANO + HARDENED_OFFSET, 4)
- const account = dec.toBytes(index + HARDENED_OFFSET, 4)
- const data = new Uint8Array([LEDGER_ADPU_CODES.bip32DerivationLevel, ...purpose, ...coin, ...account])
-
- const transport = await this.DynamicTransport.create(this.openTimeout, this.listenTimeout)
- const response = await transport.send(LEDGER_ADPU_CODES.class, LEDGER_ADPU_CODES.account, show ? 0x01 : 0x00, LEDGER_ADPU_CODES.paramUnused, data as Buffer)
- .catch(err => dec.toBytes(err.statusCode)) as Uint8Array
- await transport.close()
-
- if (response.length === 2) {
- const statusCode = bytes.toDec(response) as number
- const status = LEDGER_STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR'
- return { status, publicKey: null, address: null }
- }
-
- try {
- const publicKey = bytes.toHex(response.slice(0, 32))
- const addressLength = response[32]
- const address = response.slice(33, 33 + addressLength).toString()
- const statusCode = bytes.toDec(response.slice(33 + addressLength)) as number
- const status = LEDGER_STATUS_CODES[statusCode]
- return { status, publicKey, address }
- } catch (err) {
- return { status: 'ERROR_PARSING_ACCOUNT', publicKey: null, address: null }
- }
- }
-
- /**
- * Cache frontier block in device memory.
- *
- * @param {number} index - Account number
- * @param {any} block - Block data to cache
- * @returns Status of command
- */
- async cacheBlock (index: number = 0, block: SendBlock | ReceiveBlock | ChangeBlock): Promise<LedgerResponse> {
- if (typeof index !== 'number' || index < 0 || index >= HARDENED_OFFSET) {
- throw new TypeError('Invalid account index')
- }
- if (!(block instanceof SendBlock) && !(block instanceof ReceiveBlock) && !(block instanceof ChangeBlock)) {
- throw new TypeError('Invalid block format')
- }
- if (!block.signature) {
- throw new ReferenceError('Cannot cache unsigned block')
- }
-
- const purpose = dec.toBytes(BIP44_PURPOSE + HARDENED_OFFSET, 4)
- const coin = dec.toBytes(BIP44_COIN_NANO + HARDENED_OFFSET, 4)
- const account = dec.toBytes(index + HARDENED_OFFSET, 4)
- const previous = hex.toBytes(block.previous)
- const link = hex.toBytes(block.link)
- const representative = hex.toBytes(block.representative.publicKey)
- const balance = hex.toBytes(BigInt(block.balance).toString(16), 16)
- const signature = hex.toBytes(block.signature)
- const data = new Uint8Array([LEDGER_ADPU_CODES.bip32DerivationLevel, ...purpose, ...coin, ...account, ...previous, ...link, ...representative, ...balance, ...signature])
-
- const transport = await this.DynamicTransport.create(this.openTimeout, this.listenTimeout)
- const response = await transport.send(LEDGER_ADPU_CODES.class, LEDGER_ADPU_CODES.cacheBlock, LEDGER_ADPU_CODES.paramUnused, LEDGER_ADPU_CODES.paramUnused, data as Buffer)
- .then(res => bytes.toDec(res))
- .catch(err => err.statusCode) as number
- await transport.close()
-
- return { status: LEDGER_STATUS_CODES[response] }
- }
-
- /**
- * Sign a block with the Ledger device.
- *
- * @param {number} index - Account number
- * @param {object} block - Block data to sign
- * @returns {Promise} Status, signature, and block hash
- */
- async sign (index: number, block: SendBlock | ReceiveBlock | ChangeBlock): Promise<LedgerSignResponse>
- /**
- * Sign a nonce with the Ledger device.
- *
- * @param {number} index - Account number
- * @param {string} nonce - 128-bit string to sign
- * @returns {Promise} Status and signature
- */
- async sign (index: number, nonce: string): Promise<LedgerSignResponse>
- async sign (index: number = 0, input: string | SendBlock | ReceiveBlock | ChangeBlock): Promise<LedgerSignResponse> {
- if (typeof index !== 'number' || index < 0 || index >= HARDENED_OFFSET) {
- throw new TypeError('Invalid account index')
- }
- const purpose = dec.toBytes(BIP44_PURPOSE + HARDENED_OFFSET, 4)
- const coin = dec.toBytes(BIP44_COIN_NANO + HARDENED_OFFSET, 4)
- const account = dec.toBytes(index + HARDENED_OFFSET, 4)
-
- const transport = await this.DynamicTransport.create(this.openTimeout, this.listenTimeout)
- if (typeof input === 'string') {
- // input is a nonce
- const nonce = utf8.toBytes(input)
- if (nonce.length !== 16) {
- throw new RangeError('Nonce must be 16-byte string')
- }
- const data = new Uint8Array([LEDGER_ADPU_CODES.bip32DerivationLevel, ...purpose, ...coin, ...account, ...nonce])
- const response = await transport.send(LEDGER_ADPU_CODES.class, LEDGER_ADPU_CODES.signNonce, LEDGER_ADPU_CODES.paramUnused, LEDGER_ADPU_CODES.paramUnused, data as Buffer)
- .catch(err => dec.toBytes(err.statusCode)) as Uint8Array
- await transport.close()
-
- if (response.length === 2) {
- const statusCode = bytes.toDec(response) as number
- const status = LEDGER_STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR'
- return { status, signature: null }
- }
-
- const signature = bytes.toHex(response.slice(0, 64))
- const statusCode = bytes.toDec(response.slice(-2)) as number
- const status = LEDGER_STATUS_CODES[statusCode]
- return { status, signature }
- } else {
- // input is a block
- const previous = hex.toBytes(input.previous)
- const link = hex.toBytes(input.link)
- const representative = hex.toBytes(input.representative.publicKey)
- const balance = hex.toBytes(BigInt(input.balance).toString(16), 16)
- const data = new Uint8Array([LEDGER_ADPU_CODES.bip32DerivationLevel, ...purpose, ...coin, ...account, ...previous, ...link, ...representative, ...balance])
- const response = await transport.send(LEDGER_ADPU_CODES.class, LEDGER_ADPU_CODES.signBlock, LEDGER_ADPU_CODES.paramUnused, LEDGER_ADPU_CODES.paramUnused, data as Buffer)
- .catch(err => dec.toBytes(err.statusCode)) as Uint8Array
- await transport.close()
-
- if (response.length === 2) {
- const statusCode = bytes.toDec(response) as number
- const status = LEDGER_STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR'
- return { status, signature: null }
- }
-
- const hash = bytes.toHex(response.slice(0, 32))
- const signature = bytes.toHex(response.slice(32, 96))
- const statusCode = bytes.toDec(response.slice(-2)) as number
- const status = LEDGER_STATUS_CODES[statusCode]
- return { status, signature, hash }
- }
- }
-
- /**
- * Update cache from raw block data. Suitable for offline use.
- *
- * @param {number} index - Account number
- * @param {object} block - JSON-formatted block data
- */
- async updateCache (index: number, block: { [key: string]: string }): Promise<LedgerResponse>
- /**
- * Update cache from a block hash by calling out to a node. Suitable for online
- * use only.
- *
- * @param {number} index - Account number
- * @param {string} hash - Hexadecimal block hash
- * @param {Rpc} rpc - Rpc class object with a node URL
- */
- async updateCache (index: number, hash: string, rpc: Rpc): Promise<LedgerResponse>
- async updateCache (index: number, input: any, node?: Rpc): Promise<LedgerResponse> {
- if (typeof input === 'string' && node?.constructor === Rpc) {
- const data = {
- 'json_block': 'true',
- 'hash': input
- }
- const res = await node.call('block_info', data)
- if (!res || res.ok === false) {
- throw new Error(`Unable to fetch block info`, res)
- }
- input = res.contents
- }
- return this.cacheBlock(index, input)
- }
-}
-
\r
export { Bip44Wallet } from './bip44-wallet'\r
export { Blake2bWallet } from './blake2b-wallet'\r
-export { LedgerWallet } from './ledger-wallet'\r
+export { Ledger, LedgerWallet } from './ledger-wallet'\r
import { BIP44_COIN_NANO, BIP44_PURPOSE, HARDENED_OFFSET, LEDGER_ADPU_CODES, LEDGER_STATUS_CODES } from '#src/lib/constants.js'\r
import { bytes, dec, hex, utf8 } from '#src/lib/convert.js'\r
import { Entropy } from '#src/lib/entropy.js'\r
-import { Ledger } from '#src/lib/ledger.js'\r
import { Rpc } from '#src/lib/rpc.js'\r
import { KeyPair, Wallet } from './wallet'\r
\r
* @returns {LedgerWallet} A wallet containing accounts and a Ledger device communication object\r
*/\r
static async create (): Promise<LedgerWallet> {\r
- const { Ledger } = await import('../ledger')\r
const l = await Ledger.init()\r
const id = await Entropy.create(16)\r
LedgerWallet.#isInternal = true\r
if (typeof id !== 'string' || id === '') {\r
throw new TypeError('Wallet ID is required to restore')\r
}\r
- const { Ledger } = await import('../ledger')\r
const l = await Ledger.init()\r
LedgerWallet.#isInternal = true\r
return new this(await Entropy.import(id), l)\r
return result === 'OK'\r
}\r
}\r
+\r
+export class Ledger {\r
+ static #isInternal: boolean = false\r
+ #status: 'DISCONNECTED' | 'LOCKED' | 'BUSY' | 'CONNECTED' = 'DISCONNECTED'\r
+ get status () { return this.#status }\r
+ openTimeout = 3000\r
+ listenTimeout = 30000;\r
+ transport: Transport | null = null\r
+ DynamicTransport: typeof TransportBLE | typeof TransportUSB | typeof TransportHID = TransportHID\r
+\r
+ constructor () {\r
+ if (!Ledger.#isInternal) {\r
+ throw new Error('Ledger cannot be instantiated directly. Use Ledger.init()')\r
+ }\r
+ Ledger.#isInternal = false\r
+ Buffer\r
+ }\r
+\r
+ static async init (): Promise<Ledger> {\r
+ Ledger.#isInternal = true\r
+ const self = new this()\r
+ await self.checkBrowserSupport()\r
+ await self.listen()\r
+ return self\r
+ }\r
+\r
+ /**\r
+ * Check which transport protocols are supported by the browser and set the\r
+ * transport type according to the following priorities: Bluetooth, USB, HID.\r
+ */\r
+ async checkBrowserSupport (): Promise<void> {\r
+ console.log('Checking browser Ledger support...')\r
+ const supports = {\r
+ ble: await TransportBLE.isSupported(),\r
+ usb: await TransportUSB.isSupported(),\r
+ hid: await TransportHID.isSupported()\r
+ }\r
+ console.log(`ble: ${supports.ble}; usb: ${supports.usb}; hid: ${supports.hid}`)\r
+ if (supports.ble) {\r
+ this.DynamicTransport = TransportBLE\r
+ } else if (supports.usb) {\r
+ this.DynamicTransport = TransportUSB\r
+ } else if (supports.hid) {\r
+ this.DynamicTransport = TransportHID\r
+ } else {\r
+ throw new Error('Unsupported browser')\r
+ }\r
+ }\r
+\r
+ async listen (): Promise<void> {\r
+ const { usb } = globalThis.navigator\r
+ if (usb) {\r
+ usb.addEventListener('connect', console.log.bind(console))\r
+ usb.addEventListener('disconnect', console.log.bind(console))\r
+ }\r
+ }\r
+\r
+ async connect (): Promise<string> {\r
+ const { usb } = globalThis.navigator\r
+ if (usb) {\r
+ usb.removeEventListener('disconnect', this.onDisconnectUsb.bind(this))\r
+ usb.addEventListener('disconnect', this.onDisconnectUsb.bind(this))\r
+ }\r
+ const version = await this.version()\r
+ if (version.status === 'OK') {\r
+ if (version.name === 'Nano') {\r
+ const account = await this.account()\r
+ if (account.status === 'OK') {\r
+ this.#status = 'CONNECTED'\r
+ } else if (account.status === 'SECURITY_STATUS_NOT_SATISFIED') {\r
+ this.#status = 'LOCKED'\r
+ } else {\r
+ this.#status = 'DISCONNECTED'\r
+ }\r
+ } else if (version.name === 'BOLOS') {\r
+ const open = await this.open()\r
+ this.#status = (open.status === 'OK')\r
+ ? 'CONNECTED'\r
+ : 'DISCONNECTED'\r
+ } else {\r
+ this.#status = 'BUSY'\r
+ }\r
+ } else {\r
+ this.#status = 'DISCONNECTED'\r
+ }\r
+ return this.status\r
+ }\r
+\r
+ async onDisconnectUsb (e: USBConnectionEvent): Promise<void> {\r
+ if (e.device?.manufacturerName === 'Ledger') {\r
+ const { usb } = globalThis.navigator\r
+ usb.removeEventListener('disconnect', this.onDisconnectUsb)\r
+ this.#status = 'DISCONNECTED'\r
+ }\r
+ }\r
+\r
+ /**\r
+ * Open Nano app by launching user flow.\r
+ *\r
+ * https://developers.ledger.com/docs/connectivity/ledgerJS/open-close-info-on-apps#open-application\r
+ *\r
+ * This command resets the internal USB connection of the device which can\r
+ * cause subsequent commands to fail if called too quickly. A one-second delay\r
+ * is implemented in this method to mitigate the issue.\r
+ *\r
+ * https://github.com/LedgerHQ/ledger-live/issues/4964#issuecomment-1878361157\r
+ *\r
+ * @returns Status of command\r
+ */\r
+ async open (): Promise<LedgerResponse> {\r
+ const name = new TextEncoder().encode('Nano')\r
+ const transport = await this.DynamicTransport.create(this.openTimeout, this.listenTimeout)\r
+ const response = await transport.send(0xe0, 0xd8, LEDGER_ADPU_CODES.paramUnused, LEDGER_ADPU_CODES.paramUnused, name as Buffer)\r
+ .then(res => bytes.toDec(res))\r
+ .catch(err => err.statusCode) as number\r
+ return new Promise(resolve => setTimeout(resolve, 1000, { status: LEDGER_STATUS_CODES[response] }))\r
+ }\r
+\r
+ /**\r
+ * Close the currently running app.\r
+ *\r
+ * https://developers.ledger.com/docs/connectivity/ledgerJS/open-close-info-on-apps#quit-application\r
+ *\r
+ * This command resets the internal USB connection of the device which can\r
+ * cause subsequent commands to fail if called too quickly. A one-second delay\r
+ * is implemented in this method to mitigate the issue.\r
+ *\r
+ * https://github.com/LedgerHQ/ledger-live/issues/4964#issuecomment-1878361157\r
+ *\r
+ * @returns Status of command\r
+ */\r
+ async close (): Promise<LedgerResponse> {\r
+ const transport = await this.DynamicTransport.create(this.openTimeout, this.listenTimeout)\r
+ const response = await transport.send(0xb0, 0xa7, LEDGER_ADPU_CODES.paramUnused, LEDGER_ADPU_CODES.paramUnused)\r
+ .then(res => bytes.toDec(res))\r
+ .catch(err => err.statusCode) as number\r
+ return new Promise(resolve => setTimeout(resolve, 1000, { status: LEDGER_STATUS_CODES[response] }))\r
+ }\r
+\r
+ /**\r
+ * Get the version of the current process. If a specific app is running, get\r
+ * the app version. Otherwise, get the Ledger BOLOS version instead.\r
+ *\r
+ * https://developers.ledger.com/docs/connectivity/ledgerJS/open-close-info-on-apps#get-information\r
+ *\r
+ * @returns Status, process name, and version\r
+ */\r
+ async version (): Promise<LedgerVersionResponse> {\r
+ const transport = await this.DynamicTransport.create(this.openTimeout, this.listenTimeout)\r
+ const response = await transport.send(0xb0, LEDGER_ADPU_CODES.version, LEDGER_ADPU_CODES.paramUnused, LEDGER_ADPU_CODES.paramUnused)\r
+ .catch(err => dec.toBytes(err.statusCode)) as Uint8Array\r
+ await transport.close()\r
+\r
+ if (response.length === 2) {\r
+ const statusCode = bytes.toDec(response) as number\r
+ const status = LEDGER_STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR'\r
+ return { status, name: null, version: null }\r
+ }\r
+\r
+ const nameLength = response[1]\r
+ const name = response.slice(2, 2 + nameLength).toString()\r
+ const versionLength = response[2 + nameLength]\r
+ const version = response.slice(2 + nameLength + 1, 2 + nameLength + 1 + versionLength).toString()\r
+ const statusCode = bytes.toDec(response.slice(-2)) as number\r
+\r
+ const status = LEDGER_STATUS_CODES[statusCode]\r
+ return { status, name, version }\r
+ }\r
+\r
+ /**\r
+ * Get an account at a specific BIP-44 index.\r
+ *\r
+ * @returns Response object containing command status, public key, and address\r
+ */\r
+ async account (index: number = 0, show: boolean = false): Promise<LedgerAccountResponse> {\r
+ if (typeof index !== 'number' || index < 0 || index >= HARDENED_OFFSET) {\r
+ throw new TypeError('Invalid account index')\r
+ }\r
+ const purpose = dec.toBytes(BIP44_PURPOSE + HARDENED_OFFSET, 4)\r
+ const coin = dec.toBytes(BIP44_COIN_NANO + HARDENED_OFFSET, 4)\r
+ const account = dec.toBytes(index + HARDENED_OFFSET, 4)\r
+ const data = new Uint8Array([LEDGER_ADPU_CODES.bip32DerivationLevel, ...purpose, ...coin, ...account])\r
+\r
+ const transport = await this.DynamicTransport.create(this.openTimeout, this.listenTimeout)\r
+ const response = await transport.send(LEDGER_ADPU_CODES.class, LEDGER_ADPU_CODES.account, show ? 0x01 : 0x00, LEDGER_ADPU_CODES.paramUnused, data as Buffer)\r
+ .catch(err => dec.toBytes(err.statusCode)) as Uint8Array\r
+ await transport.close()\r
+\r
+ if (response.length === 2) {\r
+ const statusCode = bytes.toDec(response) as number\r
+ const status = LEDGER_STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR'\r
+ return { status, publicKey: null, address: null }\r
+ }\r
+\r
+ try {\r
+ const publicKey = bytes.toHex(response.slice(0, 32))\r
+ const addressLength = response[32]\r
+ const address = response.slice(33, 33 + addressLength).toString()\r
+ const statusCode = bytes.toDec(response.slice(33 + addressLength)) as number\r
+ const status = LEDGER_STATUS_CODES[statusCode]\r
+ return { status, publicKey, address }\r
+ } catch (err) {\r
+ return { status: 'ERROR_PARSING_ACCOUNT', publicKey: null, address: null }\r
+ }\r
+ }\r
+\r
+ /**\r
+ * Cache frontier block in device memory.\r
+ *\r
+ * @param {number} index - Account number\r
+ * @param {any} block - Block data to cache\r
+ * @returns Status of command\r
+ */\r
+ async cacheBlock (index: number = 0, block: SendBlock | ReceiveBlock | ChangeBlock): Promise<LedgerResponse> {\r
+ if (typeof index !== 'number' || index < 0 || index >= HARDENED_OFFSET) {\r
+ throw new TypeError('Invalid account index')\r
+ }\r
+ if (!(block instanceof SendBlock) && !(block instanceof ReceiveBlock) && !(block instanceof ChangeBlock)) {\r
+ throw new TypeError('Invalid block format')\r
+ }\r
+ if (!block.signature) {\r
+ throw new ReferenceError('Cannot cache unsigned block')\r
+ }\r
+\r
+ const purpose = dec.toBytes(BIP44_PURPOSE + HARDENED_OFFSET, 4)\r
+ const coin = dec.toBytes(BIP44_COIN_NANO + HARDENED_OFFSET, 4)\r
+ const account = dec.toBytes(index + HARDENED_OFFSET, 4)\r
+ const previous = hex.toBytes(block.previous)\r
+ const link = hex.toBytes(block.link)\r
+ const representative = hex.toBytes(block.representative.publicKey)\r
+ const balance = hex.toBytes(BigInt(block.balance).toString(16), 16)\r
+ const signature = hex.toBytes(block.signature)\r
+ const data = new Uint8Array([LEDGER_ADPU_CODES.bip32DerivationLevel, ...purpose, ...coin, ...account, ...previous, ...link, ...representative, ...balance, ...signature])\r
+\r
+ const transport = await this.DynamicTransport.create(this.openTimeout, this.listenTimeout)\r
+ const response = await transport.send(LEDGER_ADPU_CODES.class, LEDGER_ADPU_CODES.cacheBlock, LEDGER_ADPU_CODES.paramUnused, LEDGER_ADPU_CODES.paramUnused, data as Buffer)\r
+ .then(res => bytes.toDec(res))\r
+ .catch(err => err.statusCode) as number\r
+ await transport.close()\r
+\r
+ return { status: LEDGER_STATUS_CODES[response] }\r
+ }\r
+\r
+ /**\r
+ * Sign a block with the Ledger device.\r
+ *\r
+ * @param {number} index - Account number\r
+ * @param {object} block - Block data to sign\r
+ * @returns {Promise} Status, signature, and block hash\r
+ */\r
+ async sign (index: number, block: SendBlock | ReceiveBlock | ChangeBlock): Promise<LedgerSignResponse>\r
+ /**\r
+ * Sign a nonce with the Ledger device.\r
+ *\r
+ * @param {number} index - Account number\r
+ * @param {string} nonce - 128-bit string to sign\r
+ * @returns {Promise} Status and signature\r
+ */\r
+ async sign (index: number, nonce: string): Promise<LedgerSignResponse>\r
+ async sign (index: number = 0, input: string | SendBlock | ReceiveBlock | ChangeBlock): Promise<LedgerSignResponse> {\r
+ if (typeof index !== 'number' || index < 0 || index >= HARDENED_OFFSET) {\r
+ throw new TypeError('Invalid account index')\r
+ }\r
+ const purpose = dec.toBytes(BIP44_PURPOSE + HARDENED_OFFSET, 4)\r
+ const coin = dec.toBytes(BIP44_COIN_NANO + HARDENED_OFFSET, 4)\r
+ const account = dec.toBytes(index + HARDENED_OFFSET, 4)\r
+\r
+ const transport = await this.DynamicTransport.create(this.openTimeout, this.listenTimeout)\r
+ if (typeof input === 'string') {\r
+ // input is a nonce\r
+ const nonce = utf8.toBytes(input)\r
+ if (nonce.length !== 16) {\r
+ throw new RangeError('Nonce must be 16-byte string')\r
+ }\r
+ const data = new Uint8Array([LEDGER_ADPU_CODES.bip32DerivationLevel, ...purpose, ...coin, ...account, ...nonce])\r
+ const response = await transport.send(LEDGER_ADPU_CODES.class, LEDGER_ADPU_CODES.signNonce, LEDGER_ADPU_CODES.paramUnused, LEDGER_ADPU_CODES.paramUnused, data as Buffer)\r
+ .catch(err => dec.toBytes(err.statusCode)) as Uint8Array\r
+ await transport.close()\r
+\r
+ if (response.length === 2) {\r
+ const statusCode = bytes.toDec(response) as number\r
+ const status = LEDGER_STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR'\r
+ return { status, signature: null }\r
+ }\r
+\r
+ const signature = bytes.toHex(response.slice(0, 64))\r
+ const statusCode = bytes.toDec(response.slice(-2)) as number\r
+ const status = LEDGER_STATUS_CODES[statusCode]\r
+ return { status, signature }\r
+ } else {\r
+ // input is a block\r
+ const previous = hex.toBytes(input.previous)\r
+ const link = hex.toBytes(input.link)\r
+ const representative = hex.toBytes(input.representative.publicKey)\r
+ const balance = hex.toBytes(BigInt(input.balance).toString(16), 16)\r
+ const data = new Uint8Array([LEDGER_ADPU_CODES.bip32DerivationLevel, ...purpose, ...coin, ...account, ...previous, ...link, ...representative, ...balance])\r
+ const response = await transport.send(LEDGER_ADPU_CODES.class, LEDGER_ADPU_CODES.signBlock, LEDGER_ADPU_CODES.paramUnused, LEDGER_ADPU_CODES.paramUnused, data as Buffer)\r
+ .catch(err => dec.toBytes(err.statusCode)) as Uint8Array\r
+ await transport.close()\r
+\r
+ if (response.length === 2) {\r
+ const statusCode = bytes.toDec(response) as number\r
+ const status = LEDGER_STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR'\r
+ return { status, signature: null }\r
+ }\r
+\r
+ const hash = bytes.toHex(response.slice(0, 32))\r
+ const signature = bytes.toHex(response.slice(32, 96))\r
+ const statusCode = bytes.toDec(response.slice(-2)) as number\r
+ const status = LEDGER_STATUS_CODES[statusCode]\r
+ return { status, signature, hash }\r
+ }\r
+ }\r
+\r
+ /**\r
+ * Update cache from raw block data. Suitable for offline use.\r
+ *\r
+ * @param {number} index - Account number\r
+ * @param {object} block - JSON-formatted block data\r
+ */\r
+ async updateCache (index: number, block: { [key: string]: string }): Promise<LedgerResponse>\r
+ /**\r
+ * Update cache from a block hash by calling out to a node. Suitable for online\r
+ * use only.\r
+ *\r
+ * @param {number} index - Account number\r
+ * @param {string} hash - Hexadecimal block hash\r
+ * @param {Rpc} rpc - Rpc class object with a node URL\r
+ */\r
+ async updateCache (index: number, hash: string, rpc: Rpc): Promise<LedgerResponse>\r
+ async updateCache (index: number, input: any, node?: Rpc): Promise<LedgerResponse> {\r
+ if (typeof input === 'string' && node?.constructor === Rpc) {\r
+ const data = {\r
+ 'json_block': 'true',\r
+ 'hash': input\r
+ }\r
+ const res = await node.call('block_info', data)\r
+ if (!res || res.ok === false) {\r
+ throw new Error(`Unable to fetch block info`, res)\r
+ }\r
+ input = res.contents\r
+ }\r
+ return this.cacheBlock(index, input)\r
+ }\r
+}\r