]> git.codecow.com Git - libnemo.git/commitdiff
Move Ledger class into ledger wallet file and update import in block class.
authorChris Duncan <chris@zoso.dev>
Sat, 5 Jul 2025 08:41:27 +0000 (01:41 -0700)
committerChris Duncan <chris@zoso.dev>
Sat, 5 Jul 2025 08:41:27 +0000 (01:41 -0700)
src/lib/block.ts
src/lib/ledger.ts [deleted file]
src/lib/wallets/index.ts
src/lib/wallets/ledger-wallet.ts

index 563f78f870638d7a179dcf997267cd85d6ce3e29..2db2fe51e3c877f0880984353e176dc898d84482 100644 (file)
@@ -119,7 +119,7 @@ abstract class Block {
        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) {
diff --git a/src/lib/ledger.ts b/src/lib/ledger.ts
deleted file mode 100644 (file)
index 0e9dc04..0000000
+++ /dev/null
@@ -1,377 +0,0 @@
-// 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)
-       }
-}
-
index 574fc703ec1128884fb8f64e9b22ce7b0d9bee6e..a6569d0715fb3a218c325d6453086cd38bb96db8 100644 (file)
@@ -3,4 +3,4 @@
 \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
index e21298ea020accf38b1bb8ecff2eadacb19c58b4..4b4d28c45e2e12127d7cc24938153b940f5e4676 100644 (file)
@@ -9,7 +9,6 @@ import { ChangeBlock, ReceiveBlock, SendBlock } from '#src/lib/block.js'
 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
@@ -65,7 +64,6 @@ export class LedgerWallet extends Wallet {
        * @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
@@ -82,7 +80,6 @@ export class LedgerWallet extends Wallet {
                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
@@ -139,3 +136,348 @@ export class LedgerWallet extends Wallet {
                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