]> git.codecow.com Git - libnemo.git/commitdiff
Move Ledger command queue to its own file.
authorChris Duncan <chris@zoso.dev>
Fri, 15 May 2026 20:18:04 +0000 (13:18 -0700)
committerChris Duncan <chris@zoso.dev>
Fri, 15 May 2026 20:18:04 +0000 (13:18 -0700)
src/lib/ledger/index.ts
src/lib/ledger/queue.ts [new file with mode: 0644]

index 1a8ae463b136d1d85695ff5d2af1f392b0089681..6d2c8582943da4575243d54650e57d358c47284c 100644 (file)
@@ -11,6 +11,7 @@ import { BIP44_COIN_NANO, BIP44_PURPOSE, HARDENED_OFFSET } from '../constants'
 import { bytes, dec, hex, utf8 } from '../convert'
 import { Rpc } from '../rpc'
 import { Wallet } from '../wallet'
+import { enqueue } from './queue'
 
 type LedgerStatus = 'UNSUPPORTED' | 'DISCONNECTED' | 'BUSY' | 'LOCKED' | 'CONNECTED'
 
@@ -42,11 +43,9 @@ interface LedgerSignResponse extends LedgerResponse {
 * https://github.com/roosmaa/ledger-app-nano/blob/master/doc/nano.md
 */
 export class Ledger {
-       static #isIdle: boolean = true
        static #isPolling: boolean = false
        static #listenTimeout: 30000 = 30000
        static #openTimeout: 3000 = 3000
-       static #queue: { task: Function, resolve: Function, reject: Function }[] = []
        static #status: LedgerStatus = 'DISCONNECTED'
        static #transport: typeof TransportHID | typeof TransportBLE | typeof TransportUSB
        static #ADPU_CODES: Record<string, number> = Object.freeze({
@@ -129,7 +128,7 @@ export class Ledger {
        * @returns Response object containing command status, public key, and address
        */
        static async account (index: number = 0, show: boolean = false): Promise<LedgerAccountResponse> {
-               return this.#enqueue(async () => {
+               return enqueue(async () => {
                        try {
                                if (typeof index !== 'number' || index < 0 || index >= HARDENED_OFFSET) {
                                        throw new TypeError('Invalid account index')
@@ -207,7 +206,7 @@ export class Ledger {
        * connection updates.
        */
        static async disconnect (): Promise<void> {
-               this.#enqueue(async () => {
+               enqueue(async () => {
                        try {
                                this.#isPolling = false
                                const hidDevices = await navigator?.hid?.getDevices?.() ?? []
@@ -369,7 +368,7 @@ export class Ledger {
        * @returns Status of command
        */
        static async #cacheBlock (index: number = 0, block: Block): Promise<LedgerResponse> {
-               return this.#enqueue(async () => {
+               return enqueue(async () => {
                        try {
                                if (typeof index !== 'number' || index < 0 || index >= HARDENED_OFFSET) {
                                        throw new TypeError('Invalid account index')
@@ -426,7 +425,7 @@ export class Ledger {
        * @returns Status of command
        */
        static async #close (): Promise<LedgerResponse> {
-               return this.#enqueue(async () => {
+               return enqueue(async () => {
                        const transport = await this.#transport.create(this.#openTimeout, this.#listenTimeout)
                        const response = await transport
                                .send(0xb0, 0xa7, this.#ADPU_CODES.paramUnused, this.#ADPU_CODES.paramUnused)
@@ -473,24 +472,6 @@ export class Ledger {
                return this.#status
        }
 
-       /**
-       * Serially executes asynchronous functions.
-       */
-       static async #enqueue<T> (task: () => Promise<T>): Promise<T> {
-               const process = () => {
-                       const next = this.#queue.shift()
-                       if (next == null) return this.#isIdle = true
-                       const { task, resolve, reject } = next
-                       this.#isIdle = !task
-                       task?.().then(resolve).catch(reject).finally(process)
-               }
-               if (typeof task !== 'function') throw new TypeError('task is not a function')
-               return new Promise<T>((resolve, reject) => {
-                       this.#queue.push({ task, resolve, reject })
-                       if (this.#isIdle) process()
-               })
-       }
-
        /**
        * Open the Nano app by launching a user flow.
        *
@@ -505,7 +486,7 @@ export class Ledger {
        * @returns Status of command
        */
        static async #open (): Promise<LedgerResponse> {
-               return this.#enqueue(async () => {
+               return enqueue(async () => {
                        const name = new TextEncoder().encode('Nano')
                        const transport = await this.#transport.create(this.#openTimeout, this.#listenTimeout)
                        const response = await transport
@@ -570,7 +551,7 @@ export class Ledger {
                if (block.signature !== undefined) {
                        throw new TypeError('Block signature already exists', { cause: block.signature })
                }
-               return this.#enqueue(async () => {
+               return enqueue(async () => {
                        try {
                                if (typeof index !== 'number' || index < 0 || index >= HARDENED_OFFSET) {
                                        throw new TypeError('Invalid account index')
@@ -623,7 +604,7 @@ export class Ledger {
        * @returns {Promise} Status and signature
        */
        static async #signNonce (index: number, nonce: Uint8Array<ArrayBuffer>): Promise<LedgerSignResponse> {
-               return this.#enqueue(async () => {
+               return enqueue(async () => {
                        if (typeof index !== 'number' || index < 0 || index >= HARDENED_OFFSET) {
                                throw new TypeError('Invalid account index')
                        }
@@ -664,7 +645,7 @@ export class Ledger {
        * @returns Status, process name, and version
        */
        static async #version (): Promise<LedgerVersionResponse> {
-               return this.#enqueue(async () => {
+               return enqueue(async () => {
                        try {
                                const transport = await this.#transport.create(this.#openTimeout, this.#listenTimeout)
                                const response = await transport
diff --git a/src/lib/ledger/queue.ts b/src/lib/ledger/queue.ts
new file mode 100644 (file)
index 0000000..c1de428
--- /dev/null
@@ -0,0 +1,24 @@
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@codecow.com>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+const queue: { task: Function, resolve: Function, reject: Function }[] = []
+
+let isIdle: boolean = true
+
+/**
+* Serially executes asynchronous functions.
+*/
+export async function enqueue<T> (task: () => Promise<T>): Promise<T> {
+       const process = () => {
+               const next = queue.shift()
+               if (next == null) return isIdle = true
+               const { task, resolve, reject } = next
+               isIdle = !task
+               task?.().then(resolve).catch(reject).finally(process)
+       }
+       if (typeof task !== 'function') throw new TypeError('task is not a function')
+       return new Promise<T>((resolve, reject) => {
+               queue.push({ task, resolve, reject })
+               if (isIdle) process()
+       })
+}