launchpad-ledger: Pass transport as argument
This commit is contained in:
parent
d1c6ba4494
commit
db88528921
@ -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<string> {
|
||||
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<Uint8Array> {
|
||||
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<Uint8Array> {
|
||||
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<void> {
|
||||
if (this.connectedApp) {
|
||||
await this.connectedApp.transport.close();
|
||||
this.connectedApp = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureConnected(): Promise<void> {
|
||||
// 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<Transport> {
|
||||
// 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<string> {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -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<readonly AccountData[]> {
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
/// <reference types="ledgerhq__hw-transport" />
|
||||
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<string>;
|
||||
getPubkey(hdPath?: HdPath): Promise<Uint8Array>;
|
||||
getPubkeys(): Promise<readonly Uint8Array[]>;
|
||||
getCosmosAddress(pubkey?: Uint8Array): Promise<string>;
|
||||
sign(message: Uint8Array, hdPath?: HdPath): Promise<Uint8Array>;
|
||||
disconnect(): Promise<void>;
|
||||
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;
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
/// <reference types="ledgerhq__hw-transport" />
|
||||
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<readonly AccountData[]>;
|
||||
sign(signerAddress: string, signDoc: StdSignDoc): Promise<SignResponse>;
|
||||
disconnect(): Promise<void>;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user