diff --git a/packages/launchpad-ledger/src/launchpadledger.ts b/packages/launchpad-ledger/src/launchpadledger.ts index d781ea95..efecf95b 100644 --- a/packages/launchpad-ledger/src/launchpadledger.ts +++ b/packages/launchpad-ledger/src/launchpadledger.ts @@ -3,7 +3,6 @@ import { fromUtf8 } from "@cosmjs/encoding"; import { makeCosmoshubPath } from "@cosmjs/launchpad"; import { assert } from "@cosmjs/utils"; import Transport from "@ledgerhq/hw-transport"; -import TransportWebUsb from "@ledgerhq/hw-transport-webusb"; import CosmosApp, { AppInfoResponse, PublicKeyResponse, @@ -26,46 +25,17 @@ function isWindows(platform: string): boolean { return platform.indexOf("Win") > -1; } -function verifyBrowserIsSupported(platform: string, userAgent: string): void { +function verifyBrowserIsSupported(platform: string, userAgent: string | null): void { if (isWindows(platform)) { throw new Error("Windows is not currently supported."); } - const isChromeOrBrave = /chrome|crios/i.test(userAgent) && !/edge|opr\//i.test(userAgent); + const isChromeOrBrave = userAgent && /chrome|crios/i.test(userAgent) && !/edge|opr\//i.test(userAgent); if (!isChromeOrBrave) { throw new Error("Your browser does not support Ledger devices."); } } -async function createTransport(timeout: number): Promise { - try { - const transport = await TransportWebUsb.create(timeout * 1000); - return transport; - } catch (error) { - const trimmedErrorMessage = error.message.trim(); - if (trimmedErrorMessage.startsWith("No WebUSB interface found for your Ledger device")) { - throw new Error( - "Could not connect to a Ledger device. Please use Ledger Live to upgrade the Ledger firmware to version 1.5.5 or later.", - ); - } - if (trimmedErrorMessage.startsWith("Unable to claim interface")) { - throw new Error("Could not access Ledger device. Is it being used in another tab?"); - } - if (trimmedErrorMessage.startsWith("Not supported")) { - throw new Error( - "Your browser does not seem to support WebUSB yet. Try updating it to the latest version.", - ); - } - if (trimmedErrorMessage.startsWith("No device selected")) { - throw new Error( - "You did not select a Ledger device. If you did not see your Ledger, check if the Ledger is plugged in and unlocked.", - ); - } - - throw error; - } -} - function unharden(hdPath: HdPath): number[] { return hdPath.map((n) => (n.isHardened() ? n.toNumber() - 2 ** 31 : n.toNumber())); } @@ -85,7 +55,7 @@ export class LaunchpadLedger { private readonly prefix: string; private cosmosApp: CosmosApp | null; public readonly platform: string; - public readonly userAgent: string; + public readonly userAgent: string | null; constructor(options: LaunchpadLedgerOptions = {}) { const defaultOptions = { @@ -101,8 +71,14 @@ export class LaunchpadLedger { this.hdPaths = hdPaths; this.prefix = prefix; this.cosmosApp = null; - this.platform = navigator.platform; - this.userAgent = navigator.userAgent; + + try { + this.platform = navigator.platform; + this.userAgent = navigator.userAgent; + } catch (error) { + this.platform = "node"; + this.userAgent = null; + } } async connect(timeout = defaultInteractionTimeout): Promise { @@ -111,9 +87,11 @@ export class LaunchpadLedger { return this; } - verifyBrowserIsSupported(this.platform, this.userAgent); + if (this.platform !== "node") { + verifyBrowserIsSupported(this.platform, this.userAgent); + } - const transport = await createTransport(timeout * 1000); + const transport = await this.createTransport(timeout * 1000); this.cosmosApp = new CosmosApp(transport); await this.verifyDeviceIsReady(); @@ -163,6 +141,43 @@ export class LaunchpadLedger { return Secp256k1Signature.fromDer((response as SignResponse).signature).toFixedLength(); } + private async createTransport(timeout: number): Promise { + // HACK: Use a variable to get webpack to ignore this + const nodeJsTransportPackageName = "@ledgerhq/hw-transport-node-hid"; + /* eslint-disable-next-line @typescript-eslint/naming-convention */ + const { default: TransportClass } = + this.platform === "node" + ? await import(nodeJsTransportPackageName) + : await import("@ledgerhq/hw-transport-webusb"); + + try { + const transport = await TransportClass.create(timeout * 1000); + return transport; + } catch (error) { + const trimmedErrorMessage = error.message.trim(); + if (trimmedErrorMessage.startsWith("No WebUSB interface found for your Ledger device")) { + throw new Error( + "Could not connect to a Ledger device. Please use Ledger Live to upgrade the Ledger firmware to version 1.5.5 or later.", + ); + } + if (trimmedErrorMessage.startsWith("Unable to claim interface")) { + throw new Error("Could not access Ledger device. Is it being used in another tab?"); + } + if (trimmedErrorMessage.startsWith("Not supported")) { + throw new Error( + "Your browser does not seem to support WebUSB yet. Try updating it to the latest version.", + ); + } + if (trimmedErrorMessage.startsWith("No device selected")) { + throw new Error( + "You did not select a Ledger device. If you did not see your Ledger, check if the Ledger is plugged in and unlocked.", + ); + } + + throw error; + } + } + private verifyAppMode(testMode: boolean): void { if (testMode && !this.testModeAllowed) { throw new Error(`DANGER: The Cosmos Ledger app is in test mode and should not be used on mainnet!`); diff --git a/packages/launchpad-ledger/types/launchpadledger.d.ts b/packages/launchpad-ledger/types/launchpadledger.d.ts index 6eda113d..2cc42f18 100644 --- a/packages/launchpad-ledger/types/launchpadledger.d.ts +++ b/packages/launchpad-ledger/types/launchpadledger.d.ts @@ -14,7 +14,7 @@ export declare class LaunchpadLedger { private readonly prefix; private cosmosApp; readonly platform: string; - readonly userAgent: string; + readonly userAgent: string | null; constructor(options?: LaunchpadLedgerOptions); connect(timeout?: number): Promise; getCosmosAppVersion(): Promise; @@ -22,6 +22,7 @@ export declare class LaunchpadLedger { getPubkeys(): Promise; getCosmosAddress(pubkey?: Uint8Array): Promise; sign(message: Uint8Array, hdPath?: HdPath): Promise; + private createTransport; private verifyAppMode; private getOpenAppName; private verifyAppVersion;