import { delay as diDelay , inject , singleton } from "tsyringe" ; import { TYPES } from "../types" ; import { Ledger , LedgerWebHIDIniter , LedgerWebUSBIniter } from "./ledger" ; import delay from "delay" ; import { APP_PORT , Env } from "@keplr-wallet/router" ; import { BIP44HDPath } from "../keyring" ; import { KVStore } from "@keplr-wallet/common" ; import { InteractionService } from "../interaction" ; import { LedgerOptions } from "./options" ; import { Buffer } from "buffer/" ; @ singleton ( ) export class LedgerService { private previousInitAborter : ( ( e : Error ) => void ) | undefined ; protected options : LedgerOptions ; constructor ( @ inject ( TYPES . LedgerStore ) protected readonly kvStore , @ inject ( diDelay ( ( ) => InteractionService ) ) protected readonly interactionService , @ inject ( TYPES . LedgerOptions ) options ) { this . options = { defaultMode : options . defaultMode || "webusb" , transportIniters : options . transportIniters ?? { } , } ; if ( ! this . options . transportIniters [ "webusb" ] ) { this . options . transportIniters [ "webusb" ] = LedgerWebUSBIniter ; } if ( ! this . options . transportIniters [ "webhid" ] ) { this . options . transportIniters [ "webhid" ] = LedgerWebHIDIniter ; } } async getPublicKey ( env , bip44HDPath ) { return await this . useLedger ( env , async ( ledger , retryCount ) => { try { return await ledger . getPublicKey ( [ 44 , 118 , bip44HDPath . account , bip44HDPath . change , bip44HDPath . addressIndex , ] ) ; } finally { if ( retryCount > 0 ) { this . interactionService . dispatchEvent ( APP_PORT , "ledger-init" , { event : "get-pubkey" , success : true , } ) ; } } } ) ; } async sign ( env , bip44HDPath , expectedPubKey , message ) { return await this . useLedger ( env , async ( ledger , retryCount ) => { try { const pubKey = await ledger . getPublicKey ( [ 44 , 118 , bip44HDPath . account , bip44HDPath . change , bip44HDPath . addressIndex , ] ) ; if ( Buffer . from ( expectedPubKey ) . toString ( "hex" ) !== Buffer . from ( pubKey ) . toString ( "hex" ) ) { throw new Error ( "Unmatched public key" ) ; } const signature = await ledger . sign ( [ 44 , 118 , bip44HDPath . account , bip44HDPath . change , bip44HDPath . addressIndex , ] , message ) ; if ( retryCount > 0 ) { this . interactionService . dispatchEvent ( APP_PORT , "ledger-init" , { event : "sign" , success : true , } ) ; } return signature ; } catch ( e ) { if ( retryCount > 0 ) { this . interactionService . dispatchEvent ( APP_PORT , "ledger-init" , { event : "sign" , success : false , } ) ; } throw e ; } } ) ; } async useLedger < T > ( env , fn ) { let ledger ; try { ledger = await this . initLedger ( env ) ; return await fn ( ledger . ledger , ledger . retryCount ) ; } finally { if ( ledger ) { await ledger . ledger . close ( ) ; } } } async initLedger ( env ) { if ( this . previousInitAborter ) { this . previousInitAborter ( new Error ( "New ledger request occurred before the ledger was initialized" ) ) ; } const aborter = ( ( ) => { let _reject ; return { wait : ( ) => { return new Promise ( ( _ , reject ) => { _reject = reject ; } ) ; } , abort : ( e ) => { if ( _reject ) { _reject ( e ) ; } } , } ; } ) ( ) ; this . previousInitAborter = aborter . abort ; let retryCount = 0 ; let initArgs = [ ] ; while ( true ) { const mode = await this . getMode ( ) ; try { const transportIniter = this . options . transportIniters [ mode ] ; if ( ! transportIniter ) { throw new Error ( `Unknown mode: ${ mode }` ) ; } const ledger = await Ledger . init ( transportIniter , initArgs ) ; this . previousInitAborter = undefined ; return { ledger , retryCount , } ; } catch ( e ) { console . log ( e ) ; const timeoutAbortController = new AbortController ( ) ; try { const promises = [ ( async ( ) => { const response = ( await this . interactionService . waitApprove ( env , "/ledger-grant" , "ledger-init" , { event : "init-failed" , mode , } , { forceOpenWindow : true , channel : "ledger" , } ) ) as { abort ? : boolean ; initArgs ? : any [ ] ; } | undefined ; if ( response ?. abort ) { throw new Error ( "Ledger init aborted" ) ; } if ( response ?. initArgs ) { initArgs = response . initArgs ; } } ) ( ) , ] ; promises . push ( ( async ( ) => { let timeoutAborted = false ; try { await delay ( 5 * 60 * 1000 , { signal : timeoutAbortController . signal , } ) ; } catch ( e ) { if ( e . name === "AbortError" ) { timeoutAborted = true ; } else { throw e ; } } if ( ! timeoutAborted ) { this . interactionService . dispatchEvent ( APP_PORT , "ledger-init" , { event : "init-aborted" , mode , } ) ; throw new Error ( "Ledger init timeout" ) ; } } ) ( ) ) ; promises . push ( aborter . wait ( ) ) ; if ( typeof browser !== "undefined" ) { promises . push ( this . testLedgerGrantUIOpened ( ) ) ; } await Promise . race ( promises ) ; } finally { timeoutAbortController . abort ( ) ; } } retryCount ++ ; } } async testLedgerGrantUIOpened ( ) { await delay ( 1000 ) ; while ( true ) { const views = browser . extension . getViews ( ) ; let find = false ; for ( const view of views ) { if ( view . location . href . includes ( browser . runtime . getURL ( "popup.html#/ledger-grant" ) ) ) { find = true ; break ; } } if ( ! find ) { throw new Error ( "Ledger init aborted" ) ; } await delay ( 1000 ) ; } } /**
     * Mode means that which transport should be used.
     * "webusb" and "webhid" are used in the extension environment (web).
     * Alternatively, custom mode can be supported by delivering the custom transport initer on the constructor.
     * Maybe, the "ble" (bluetooth) mode would be supported in the mobile environment (only with Ledger Nano X).
     */ async getMode ( ) { if ( await this . getWebHIDFlag ( ) ) { return "webhid" ; } return this . options . defaultMode ; } async getWebHIDFlag ( ) { const webHIDFlag = await this . kvStore . get < boolean > ( "webhid" ) ; return ! ! webHIDFlag ; } async setWebHIDFlag ( flag ) { await this . kvStore . set < boolean > ( "webhid" , flag ) ; } }