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
address: string | null
}
-interface LedgerSignResponse extends LedgerResponse {
+export interface LedgerSignResponse extends LedgerResponse {
signature: string | null,
hash?: string
}
}
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 })
}
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 })
- }
- })
- }
}
--- /dev/null
+//! 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 })
+ }
+ })
+}