]> git.codecow.com Git - libnemo.git/commitdiff
Extract specific signing algorithms.
authorChris Duncan <chris@zoso.dev>
Sat, 16 May 2026 07:35:57 +0000 (00:35 -0700)
committerChris Duncan <chris@zoso.dev>
Sat, 16 May 2026 07:35:57 +0000 (00:35 -0700)
src/lib/ledger/index.ts
src/lib/ledger/sign.ts [new file with mode: 0644]

index 0dd995015a032294deb0f30ee05a80748b27e9ad..12e8779cd760610297b2c8aed328cce891db9323 100644 (file)
@@ -5,16 +5,16 @@ import { StatusCodes } from '@ledgerhq/errors'
 import { default as TransportBLE } from '@ledgerhq/hw-transport-web-ble'
 import { default as TransportHID } from '@ledgerhq/hw-transport-webhid'
 import { default as TransportUSB } from '@ledgerhq/hw-transport-webusb'
-import { Account } from '../account'
 import { Block } from '../block'
 import { BIP44_COIN_NANO, BIP44_PURPOSE, HARDENED_OFFSET } from '../constants'
-import { bytes, dec, hex, utf8 } from '../convert'
+import { bytes, dec, utf8 } from '../convert'
 import { Rpc } from '../rpc'
 import { Wallet } from '../wallet'
 import { _account } from './account'
 import { _cache } from './cache'
 import { _connect } from './connect'
 import { queue } from './queue'
+import { signBlock, signNonce } from './sign'
 
 export type LedgerStatus = 'UNSUPPORTED' | 'DISCONNECTED' | 'BUSY' | 'LOCKED' | 'CONNECTED'
 export type LedgerTransport = typeof TransportHID | typeof TransportBLE | typeof TransportUSB
@@ -28,7 +28,7 @@ export interface LedgerAccountResponse extends LedgerResponse {
        address: string | null
 }
 
-interface LedgerSignResponse extends LedgerResponse {
+export interface LedgerSignResponse extends LedgerResponse {
        signature: string | null,
        hash?: string
 }
@@ -238,8 +238,8 @@ export class Ledger {
                        }
                        console.log('Waiting for signature confirmation on Ledger device...')
                        const { status, signature, hash } = data instanceof Block
-                               ? await this.#signBlock(index, data)
-                               : await this.#signNonce(index, utf8.toBytes(data))
+                               ? await signBlock(this.#transport, index, data)
+                               : await signNonce(this.#transport, index, utf8.toBytes(data))
                        if (status !== 'OK') {
                                throw new Error('Signing with ledger failed', { cause: status })
                        }
@@ -419,100 +419,4 @@ export class Ledger {
                        console.log(event)
                }
        }
-
-       /**
-       * 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
-       */
-       static async #signBlock (index: number, block: Block): Promise<LedgerSignResponse> {
-               if (block.signature !== undefined) {
-                       throw new TypeError('Block signature already exists', { cause: block.signature })
-               }
-               return queue(async () => {
-                       try {
-                               if (typeof index !== 'number' || index < 0 || index >= HARDENED_OFFSET) {
-                                       throw new TypeError('Invalid account index')
-                               }
-                               if (!(block.link instanceof Uint8Array)) {
-                                       throw new TypeError('Invalid block link')
-                               }
-                               if (!(block.representative instanceof Account)) {
-                                       throw new TypeError('Invalid block representative')
-                               }
-
-                               const account = dec.toBytes(index + HARDENED_OFFSET, 4)
-                               const previous = block.previous
-                               const link = block.link
-                               const representative = hex.toBytes(block.representative.publicKey, 32)
-                               const balance = hex.toBytes(BigInt(block.balance).toString(16), 16)
-                               const data = new Uint8Array([...DERIVATION_PATH, ...account, ...previous, ...link, ...representative, ...balance])
-
-                               const transport = await this.#transport.create(openTimeout, listenTimeout)
-                               const response = await transport
-                                       .send(APDU_CODES.class, APDU_CODES.signBlock, APDU_CODES.paramUnused, APDU_CODES.paramUnused, data as Buffer)
-                                       .catch((err: any) => dec.toBytes(err.statusCode))
-                                       .finally(async () => await transport.close()) as Uint8Array
-
-                               const statusCode = bytes.toDec(response.slice(-2)) as number
-                               const status = STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR'
-
-                               if (response.byteLength === 2) {
-                                       return { status, signature: null }
-                               }
-                               if (response.byteLength === 98) {
-                                       const hash = bytes.toHex(response.slice(0, 32))
-                                       const signature = bytes.toHex(response.slice(32, 96))
-                                       return { status, signature, hash }
-                               } else {
-                                       throw new Error('Unexpected byte length from device signature', { cause: response })
-                               }
-                       } catch (err: any) {
-                               console.error('Ledger.#signBlock()', err)
-                               return { status: err.message, signature: null }
-                       }
-               })
-       }
-
-       /**
-       * Sign a nonce with the Ledger device.
-       *
-       * @param {number} index - Account number
-       * @param {Uint8Array} nonce - 128-bit value to sign
-       * @returns {Promise} Status and signature
-       */
-       static async #signNonce (index: number, nonce: Uint8Array<ArrayBuffer>): Promise<LedgerSignResponse> {
-               return queue(async () => {
-                       if (typeof index !== 'number' || index < 0 || index >= HARDENED_OFFSET) {
-                               throw new TypeError('Invalid account index')
-                       }
-                       if (nonce.byteLength !== 16) {
-                               throw new RangeError('Nonce must be 16-byte string')
-                       }
-
-                       const derivationAccount = dec.toBytes(index + HARDENED_OFFSET, 4)
-                       const data = new Uint8Array([...DERIVATION_PATH, ...derivationAccount, ...nonce])
-
-                       const transport = await this.#transport.create(openTimeout, listenTimeout)
-                       const response = await transport
-                               .send(APDU_CODES.class, APDU_CODES.signNonce, APDU_CODES.paramUnused, APDU_CODES.paramUnused, data as Buffer)
-                               .catch((err: any) => dec.toBytes(err.statusCode))
-                               .finally(async () => await transport.close()) as Uint8Array
-
-                       const statusCode = bytes.toDec(response.slice(-2)) as number
-                       const status = STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR'
-
-                       if (response.byteLength === 2) {
-                               return { status, signature: null }
-                       }
-                       if (response.byteLength === 66) {
-                               const signature = bytes.toHex(response.slice(0, 64))
-                               return { status, signature }
-                       } else {
-                               throw new Error('Unexpected byte length from device signature', { cause: response })
-                       }
-               })
-       }
 }
diff --git a/src/lib/ledger/sign.ts b/src/lib/ledger/sign.ts
new file mode 100644 (file)
index 0000000..884e387
--- /dev/null
@@ -0,0 +1,105 @@
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@codecow.com>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+import { APDU_CODES, DERIVATION_PATH, LedgerSignResponse, LedgerTransport, STATUS_CODES, listenTimeout, openTimeout } from '.'
+import { Account } from '../account'
+import { Block } from '../block'
+import { HARDENED_OFFSET } from '../constants'
+import { bytes, dec, hex } from '../convert'
+import { queue } from './queue'
+
+/**
+ * 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
+ */
+export async function signBlock (transport: LedgerTransport, index: number, block: Block): Promise<LedgerSignResponse> {
+       if (block.signature !== undefined) {
+               throw new TypeError('Block signature already exists', { cause: block.signature })
+       }
+       return queue(async () => {
+               try {
+                       if (typeof index !== 'number' || index < 0 || index >= HARDENED_OFFSET) {
+                               throw new TypeError('Invalid account index')
+                       }
+                       if (!(block.link instanceof Uint8Array)) {
+                               throw new TypeError('Invalid block link')
+                       }
+                       if (!(block.representative instanceof Account)) {
+                               throw new TypeError('Invalid block representative')
+                       }
+
+                       const account = dec.toBytes(index + HARDENED_OFFSET, 4)
+                       const previous = block.previous
+                       const link = block.link
+                       const representative = hex.toBytes(block.representative.publicKey, 32)
+                       const balance = hex.toBytes(BigInt(block.balance).toString(16), 16)
+                       const data = new Uint8Array([...DERIVATION_PATH, ...account, ...previous, ...link, ...representative, ...balance])
+
+                       const t = await transport.create(openTimeout, listenTimeout)
+                       const response = await t
+                               .send(APDU_CODES.class, APDU_CODES.signBlock, APDU_CODES.paramUnused, APDU_CODES.paramUnused, data as Buffer)
+                               .catch((err: any) => dec.toBytes(err.statusCode))
+                               .finally(async () => await t.close()) as Uint8Array
+
+                       const statusCode = bytes.toDec(response.slice(-2)) as number
+                       const status = STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR'
+
+                       if (response.byteLength === 2) {
+                               return { status, signature: null }
+                       }
+                       if (response.byteLength === 98) {
+                               const hash = bytes.toHex(response.slice(0, 32))
+                               const signature = bytes.toHex(response.slice(32, 96))
+                               return { status, signature, hash }
+                       } else {
+                               throw new Error('Unexpected byte length from device signature', { cause: response })
+                       }
+               } catch (err: any) {
+                       console.error('Ledger.#signBlock()', err)
+                       return { status: err.message, signature: null }
+               }
+       })
+}
+
+/**
+ * Sign a nonce with the Ledger device.
+ *
+ * @param {number} index - Account number
+ * @param {Uint8Array} nonce - 128-bit value to sign
+ * @returns {Promise} Status and signature
+ */
+export async function signNonce (transport: LedgerTransport, index: number, nonce: Uint8Array<ArrayBuffer>): Promise<LedgerSignResponse> {
+       return queue(async () => {
+               if (typeof index !== 'number' || index < 0 || index >= HARDENED_OFFSET) {
+                       throw new TypeError('Invalid account index')
+               }
+               if (nonce.byteLength !== 16) {
+                       throw new RangeError('Nonce must be 16-byte string')
+               }
+
+               const derivationAccount = dec.toBytes(index + HARDENED_OFFSET, 4)
+               const data = new Uint8Array([...DERIVATION_PATH, ...derivationAccount, ...nonce])
+
+               const t = await transport.create(openTimeout, listenTimeout)
+               const response = await t
+                       .send(APDU_CODES.class, APDU_CODES.signNonce, APDU_CODES.paramUnused, APDU_CODES.paramUnused, data as Buffer)
+                       .catch((err: any) => dec.toBytes(err.statusCode))
+                       .finally(async () => await t.close()) as Uint8Array
+
+               const statusCode = bytes.toDec(response.slice(-2)) as number
+               const status = STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR'
+
+               if (response.byteLength === 2) {
+                       return { status, signature: null }
+               }
+               if (response.byteLength === 66) {
+                       const signature = bytes.toHex(response.slice(0, 64))
+                       return { status, signature }
+               } else {
+                       throw new Error('Unexpected byte length from device signature', { cause: response })
+               }
+       })
+}