@@ -480,6 +480,7 @@ function makePairingBitBox(state: PairingState, close: () => void): PairingBitBo
480480 */
481481export class PairedBitBox {
482482 #state: PairedStateUnion = { kind : 'uninitialized' } ;
483+ #queue: Promise < void > = Promise . resolve ( ) ;
483484
484485 /** @internal */
485486 constructor ( init ?: Omit < PairedOpen , 'kind' > ) {
@@ -504,6 +505,43 @@ export class PairedBitBox {
504505 return state ;
505506 }
506507
508+ /**
509+ * Serializes all device-touching public methods on this paired connection.
510+ *
511+ * The BitBox protocol and Noise channel are ordered streams, not multiplexed
512+ * request/response transports. Some public methods also perform multi-step
513+ * conversations (for example ETH streaming or anti-klepto signing), so the
514+ * lock must cover the whole public method, not only one encrypted query.
515+ *
516+ * Calls made after close fail before joining the queue, so they do not wait
517+ * behind a stuck transport read. The open state is checked again when the
518+ * queued operation starts, so calls queued before close still fail if they
519+ * did not already enter the device conversation.
520+ *
521+ * Each call chains onto the previous queue tail and then replaces the tail
522+ * with a settled `void` promise, so a rejected call does not poison later
523+ * queued operations.
524+ */
525+ #runExclusive< T > (
526+ method : string ,
527+ fn : ( open : PairedOpen ) => Promise < T > ,
528+ ) : Promise < T > {
529+ this . #requireOpen( method ) ;
530+ const run = this . #queue. catch ( ( ) => undefined ) . then ( async ( ) => {
531+ const open = this . #requireOpen( method ) ;
532+ try {
533+ return await fn ( open ) ;
534+ } catch ( err ) {
535+ throw toPublicError ( err ) ;
536+ }
537+ } ) ;
538+ this . #queue = run . then (
539+ ( ) => undefined ,
540+ ( ) => undefined ,
541+ ) ;
542+ return run ;
543+ }
544+
507545 /** No-op; retained for ABI compatibility with the wasm-bindgen output. */
508546 free ( ) : void { }
509547
@@ -524,12 +562,7 @@ export class PairedBitBox {
524562
525563 /** Query device metadata. */
526564 async deviceInfo ( ) : Promise < DeviceInfo > {
527- const open = this . #requireOpen( 'deviceInfo' ) ;
528- try {
529- return await deviceInfoImpl ( open . channel ) ;
530- } catch ( err ) {
531- throw toPublicError ( err ) ;
532- }
565+ return this . #runExclusive( 'deviceInfo' , open => deviceInfoImpl ( open . channel ) ) ;
533566 }
534567
535568 /** Returns which product we are connected to. */
@@ -544,12 +577,7 @@ export class PairedBitBox {
544577
545578 /** Returns the hex-encoded 4-byte root fingerprint. */
546579 async rootFingerprint ( ) : Promise < string > {
547- const open = this . #requireOpen( 'rootFingerprint' ) ;
548- try {
549- return await rootFingerprintImpl ( open . channel ) ;
550- } catch ( err ) {
551- throw toPublicError ( err ) ;
552- }
580+ return this . #runExclusive( 'rootFingerprint' , open => rootFingerprintImpl ( open . channel ) ) ;
553581 }
554582
555583 /** Not implemented in this TypeScript iteration. */
@@ -647,12 +675,7 @@ export class PairedBitBox {
647675
648676 /** Query the device for an Ethereum account xpub. */
649677 async ethXpub ( keypath : Keypath ) : Promise < string > {
650- const open = this . #requireOpen( 'ethXpub' ) ;
651- try {
652- return await ethXpubImpl ( open . channel , keypath ) ;
653- } catch ( err ) {
654- throw toPublicError ( err ) ;
655- }
678+ return this . #runExclusive( 'ethXpub' , open => ethXpubImpl ( open . channel , keypath ) ) ;
656679 }
657680
658681 /**
@@ -661,12 +684,9 @@ export class PairedBitBox {
661684 * Set `display` to `true` to require on-device confirmation.
662685 */
663686 async ethAddress ( chain_id : bigint , keypath : Keypath , display : boolean ) : Promise < string > {
664- const open = this . #requireOpen( 'ethAddress' ) ;
665- try {
666- return await ethAddressImpl ( open . channel , chain_id , keypath , display ) ;
667- } catch ( err ) {
668- throw toPublicError ( err ) ;
669- }
687+ return this . #runExclusive( 'ethAddress' , open =>
688+ ethAddressImpl ( open . channel , chain_id , keypath , display ) ,
689+ ) ;
670690 }
671691
672692 /**
@@ -681,19 +701,16 @@ export class PairedBitBox {
681701 tx : EthTransaction ,
682702 address_case ?: EthAddressCase ,
683703 ) : Promise < EthSignature > {
684- const open = this . #requireOpen( 'ethSignTransaction' ) ;
685- try {
686- return await ethSignTransactionImpl (
704+ return this . #runExclusive( 'ethSignTransaction' , open =>
705+ ethSignTransactionImpl (
687706 open . channel ,
688707 open . info ,
689708 chain_id ,
690709 keypath ,
691710 tx ,
692711 address_case ,
693- ) ;
694- } catch ( err ) {
695- throw toPublicError ( err ) ;
696- }
712+ ) ,
713+ ) ;
697714 }
698715
699716 /**
@@ -706,18 +723,15 @@ export class PairedBitBox {
706723 tx : Eth1559Transaction ,
707724 address_case ?: EthAddressCase ,
708725 ) : Promise < EthSignature > {
709- const open = this . #requireOpen( 'ethSign1559Transaction' ) ;
710- try {
711- return await ethSign1559TransactionImpl (
726+ return this . #runExclusive( 'ethSign1559Transaction' , open =>
727+ ethSign1559TransactionImpl (
712728 open . channel ,
713729 open . info ,
714730 keypath ,
715731 tx ,
716732 address_case ,
717- ) ;
718- } catch ( err ) {
719- throw toPublicError ( err ) ;
720- }
733+ ) ,
734+ ) ;
721735 }
722736
723737 /**
@@ -731,12 +745,9 @@ export class PairedBitBox {
731745 keypath : Keypath ,
732746 msg : Uint8Array ,
733747 ) : Promise < EthSignature > {
734- const open = this . #requireOpen( 'ethSignMessage' ) ;
735- try {
736- return await ethSignMessageImpl ( open . channel , open . info , chain_id , keypath , msg ) ;
737- } catch ( err ) {
738- throw toPublicError ( err ) ;
739- }
748+ return this . #runExclusive( 'ethSignMessage' , open =>
749+ ethSignMessageImpl ( open . channel , open . info , chain_id , keypath , msg ) ,
750+ ) ;
740751 }
741752
742753 /**
@@ -750,19 +761,16 @@ export class PairedBitBox {
750761 msg : any ,
751762 use_antiklepto ?: boolean ,
752763 ) : Promise < EthSignature > {
753- const open = this . #requireOpen( 'ethSignTypedMessage' ) ;
754- try {
755- return await ethSignTypedMessageImpl (
764+ return this . #runExclusive( 'ethSignTypedMessage' , open =>
765+ ethSignTypedMessageImpl (
756766 open . channel ,
757767 open . info ,
758768 chain_id ,
759769 keypath ,
760770 msg ,
761771 use_antiklepto ,
762- ) ;
763- } catch ( err ) {
764- throw toPublicError ( err ) ;
765- }
772+ ) ,
773+ ) ;
766774 }
767775
768776 /** Cardano support is not implemented in this TypeScript iteration. */
0 commit comments