diff --git a/packages/launchpad-ledger/src/launchpadledger.ts b/packages/launchpad-ledger/src/launchpadledger.ts index fd773ec5..0d296a0f 100644 --- a/packages/launchpad-ledger/src/launchpadledger.ts +++ b/packages/launchpad-ledger/src/launchpadledger.ts @@ -2,7 +2,7 @@ import { HdPath, Secp256k1Signature } from "@cosmjs/crypto"; import { fromUtf8 } from "@cosmjs/encoding"; import { makeCosmoshubPath } from "@cosmjs/launchpad"; import { assert } from "@cosmjs/utils"; -import Transport from "@ledgerhq/hw-transport"; +import LedgerTransport from "@ledgerhq/hw-transport"; import CosmosApp, { AppInfoResponse, PublicKeyResponse, @@ -18,37 +18,13 @@ export interface LedgerAppErrorResponse { } /* eslint-enable */ -interface ConnectedApp { - /** The transport used by the app */ - readonly transport: Transport; - readonly app: CosmosApp; -} - -/** Time to establish a connection in milliseconds */ -const defaultOpenTimeout = 120_000; -const requiredCosmosAppVersion = "1.5.3"; - -function isWindows(platform: string): boolean { - return platform.indexOf("Win") > -1; -} - -function verifyBrowserIsSupported(platform: string, userAgent: string | null): void { - if (isWindows(platform)) { - throw new Error("Windows is not currently supported."); - } - - 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."); - } -} - function unharden(hdPath: HdPath): number[] { return hdPath.map((n) => (n.isHardened() ? n.toNumber() - 2 ** 31 : n.toNumber())); } const cosmosHdPath = makeCosmoshubPath(0); const cosmosBech32Prefix = "cosmos"; +const requiredCosmosAppVersion = "1.5.3"; export interface LaunchpadLedgerOptions { readonly hdPaths?: readonly HdPath[]; @@ -60,11 +36,10 @@ export class LaunchpadLedger { private readonly testModeAllowed: boolean; private readonly hdPaths: readonly HdPath[]; private readonly prefix: string; - private connectedApp: ConnectedApp | null; - public readonly platform: string; - public readonly userAgent: string | null; + private readonly transport: LedgerTransport; + private app: CosmosApp | null; - public constructor(options: LaunchpadLedgerOptions = {}) { + public constructor(transport: LedgerTransport, options: LaunchpadLedgerOptions = {}) { const defaultOptions = { hdPaths: [cosmosHdPath], prefix: cosmosBech32Prefix, @@ -77,22 +52,15 @@ export class LaunchpadLedger { this.testModeAllowed = testModeAllowed; this.hdPaths = hdPaths; this.prefix = prefix; - this.connectedApp = null; - - try { - this.platform = navigator.platform; - this.userAgent = navigator.userAgent; - } catch (error) { - this.platform = "node"; - this.userAgent = null; - } + this.transport = transport; + this.app = new CosmosApp(transport); } public async getCosmosAppVersion(): Promise { - await this.ensureConnected(); - assert(this.connectedApp, "Cosmos Ledger App is not connected"); + await this.verifyCosmosAppIsOpen(); + assert(this.app, "Cosmos Ledger App is not connected"); - const response = await this.connectedApp.app.getVersion(); + const response = await this.app.getVersion(); this.handleLedgerErrors(response); // eslint-disable-next-line @typescript-eslint/naming-convention const { major, minor, patch, test_mode: testMode } = response as VersionResponse; @@ -101,12 +69,12 @@ export class LaunchpadLedger { } public async getPubkey(hdPath?: HdPath): Promise { - await this.ensureConnected(); - assert(this.connectedApp, "Cosmos Ledger App is not connected"); + await this.verifyDeviceIsReady(); + assert(this.app, "Cosmos Ledger App is not connected"); const hdPathToUse = hdPath || this.hdPaths[0]; // ledger-cosmos-js hardens the first three indices - const response = await this.connectedApp.app.publicKey(unharden(hdPathToUse)); + const response = await this.app.publicKey(unharden(hdPathToUse)); this.handleLedgerErrors(response); return Uint8Array.from((response as PublicKeyResponse).compressed_pk); } @@ -125,82 +93,20 @@ export class LaunchpadLedger { } public async sign(message: Uint8Array, hdPath?: HdPath): Promise { - await this.ensureConnected(); - assert(this.connectedApp, "Cosmos Ledger App is not connected"); + await this.verifyDeviceIsReady(); + assert(this.app, "Cosmos Ledger App is not connected"); const hdPathToUse = hdPath || this.hdPaths[0]; // ledger-cosmos-js hardens the first three indices - const response = await this.connectedApp.app.sign(unharden(hdPathToUse), fromUtf8(message)); + const response = await this.app.sign(unharden(hdPathToUse), fromUtf8(message)); this.handleLedgerErrors(response, "Transaction signing request was rejected by the user"); return Secp256k1Signature.fromDer((response as SignResponse).signature).toFixedLength(); } public async disconnect(): Promise { - if (this.connectedApp) { - await this.connectedApp.transport.close(); - this.connectedApp = null; - } - } - - private async ensureConnected(): Promise { - // assume good connection if connected once - if (this.connectedApp) { - return; - } - - if (this.platform !== "node") { - verifyBrowserIsSupported(this.platform, this.userAgent); - } - - const transport = await this.createTransport(defaultOpenTimeout); - this.connectedApp = { - transport: transport, - app: new CosmosApp(transport), - }; - - await this.verifyDeviceIsReady(); - } - - /** - * @param openTimeout The time to establish a connection in milliseconds. This is - * [passed into as the second argument into Transport.open](https://github.com/LedgerHQ/ledgerjs/blob/v5.25.2/packages/hw-transport/src/Transport.js#L235), - * which is ignored by both [TransportWebUSB.open](https://github.com/LedgerHQ/ledgerjs/blob/v5.25.2/packages/hw-transport-webusb/src/TransportWebUSB.js#L116) - * and [TransportNodeHid.open](https://github.com/LedgerHQ/ledgerjs/blob/v5.25.2/packages/hw-transport-node-hid/src/TransportNodeHid.js#L115). - */ - private async createTransport(openTimeout: 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(openTimeout); - 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; + if (this.app) { + await this.transport.close(); + this.app = null; } } @@ -211,10 +117,9 @@ export class LaunchpadLedger { } private async getOpenAppName(): Promise { - await this.ensureConnected(); - assert(this.connectedApp, "Cosmos Ledger App is not connected"); + assert(this.app, "Cosmos Ledger App is not connected"); - const response = await this.connectedApp.app.appInfo(); + const response = await this.app.appInfo(); this.handleLedgerErrors(response); return (response as AppInfoResponse).appName; } diff --git a/packages/launchpad-ledger/src/ledgersigner.ts b/packages/launchpad-ledger/src/ledgersigner.ts index 8c863165..6e548c07 100644 --- a/packages/launchpad-ledger/src/ledgersigner.ts +++ b/packages/launchpad-ledger/src/ledgersigner.ts @@ -7,6 +7,7 @@ import { StdSignDoc, } from "@cosmjs/launchpad"; import { serializeSignDoc, SignResponse } from "@cosmjs/launchpad"; +import LedgerTransport from "@ledgerhq/hw-transport"; import { LaunchpadLedger, LaunchpadLedgerOptions } from "./launchpadledger"; @@ -15,9 +16,9 @@ export class LedgerSigner implements OfflineSigner { private readonly hdPaths: readonly HdPath[]; private accounts?: readonly AccountData[]; - public constructor(options: LaunchpadLedgerOptions = {}) { + public constructor(transport: LedgerTransport, options: LaunchpadLedgerOptions = {}) { this.hdPaths = options.hdPaths || [makeCosmoshubPath(0)]; - this.ledger = new LaunchpadLedger(options); + this.ledger = new LaunchpadLedger(transport, options); } public async getAccounts(): Promise { diff --git a/packages/launchpad-ledger/types/launchpadledger.d.ts b/packages/launchpad-ledger/types/launchpadledger.d.ts index 25443745..e2bb37d5 100644 --- a/packages/launchpad-ledger/types/launchpadledger.d.ts +++ b/packages/launchpad-ledger/types/launchpadledger.d.ts @@ -1,4 +1,6 @@ +/// import { HdPath } from "@cosmjs/crypto"; +import LedgerTransport from "@ledgerhq/hw-transport"; export interface LedgerAppErrorResponse { readonly error_message?: string; readonly device_locked?: boolean; @@ -12,24 +14,15 @@ export declare class LaunchpadLedger { private readonly testModeAllowed; private readonly hdPaths; private readonly prefix; - private connectedApp; - readonly platform: string; - readonly userAgent: string | null; - constructor(options?: LaunchpadLedgerOptions); + private readonly transport; + private app; + constructor(transport: LedgerTransport, options?: LaunchpadLedgerOptions); getCosmosAppVersion(): Promise; getPubkey(hdPath?: HdPath): Promise; getPubkeys(): Promise; getCosmosAddress(pubkey?: Uint8Array): Promise; sign(message: Uint8Array, hdPath?: HdPath): Promise; disconnect(): Promise; - private ensureConnected; - /** - * @param openTimeout The time to establish a connection in milliseconds. This is - * [passed into as the second argument into Transport.open](https://github.com/LedgerHQ/ledgerjs/blob/v5.25.2/packages/hw-transport/src/Transport.js#L235), - * which is ignored by both [TransportWebUSB.open](https://github.com/LedgerHQ/ledgerjs/blob/v5.25.2/packages/hw-transport-webusb/src/TransportWebUSB.js#L116) - * and [TransportNodeHid.open](https://github.com/LedgerHQ/ledgerjs/blob/v5.25.2/packages/hw-transport-node-hid/src/TransportNodeHid.js#L115). - */ - private createTransport; private verifyAppMode; private getOpenAppName; private verifyAppVersion; diff --git a/packages/launchpad-ledger/types/ledgersigner.d.ts b/packages/launchpad-ledger/types/ledgersigner.d.ts index 8a81c649..63792bea 100644 --- a/packages/launchpad-ledger/types/ledgersigner.d.ts +++ b/packages/launchpad-ledger/types/ledgersigner.d.ts @@ -1,11 +1,13 @@ +/// import { AccountData, OfflineSigner, StdSignDoc } from "@cosmjs/launchpad"; import { SignResponse } from "@cosmjs/launchpad"; +import LedgerTransport from "@ledgerhq/hw-transport"; import { LaunchpadLedgerOptions } from "./launchpadledger"; export declare class LedgerSigner implements OfflineSigner { private readonly ledger; private readonly hdPaths; private accounts?; - constructor(options?: LaunchpadLedgerOptions); + constructor(transport: LedgerTransport, options?: LaunchpadLedgerOptions); getAccounts(): Promise; sign(signerAddress: string, signDoc: StdSignDoc): Promise; disconnect(): Promise;