From 8d9d8ec2ad6ed575b91d4f67b129769ea24e8ffe Mon Sep 17 00:00:00 2001 From: tarikgul Date: Tue, 9 Jul 2024 20:28:00 -0400 Subject: [PATCH] Ensure no breaking changes by keeping legacy Ledger class --- packages/hw-ledger/src/Ledger.ts | 143 ++++++++++++++++++++ packages/hw-ledger/src/LedgerGeneric.ts | 166 ++++++++++++++++++++++++ packages/hw-ledger/src/bundle.ts | 165 +---------------------- 3 files changed, 311 insertions(+), 163 deletions(-) create mode 100644 packages/hw-ledger/src/Ledger.ts create mode 100644 packages/hw-ledger/src/LedgerGeneric.ts diff --git a/packages/hw-ledger/src/Ledger.ts b/packages/hw-ledger/src/Ledger.ts new file mode 100644 index 0000000000..25d79f3e2e --- /dev/null +++ b/packages/hw-ledger/src/Ledger.ts @@ -0,0 +1,143 @@ +// Copyright 2017-2024 @polkadot/hw-ledger authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { SubstrateApp } from '@zondax/ledger-substrate'; +import type { TransportDef, TransportType } from '@polkadot/hw-ledger-transports/types'; +import type { AccountOptions, LedgerAddress, LedgerSignature, LedgerVersion } from './types.js'; + +import { newSubstrateApp } from '@zondax/ledger-substrate'; + +import { transports } from '@polkadot/hw-ledger-transports'; +import { hexAddPrefix, u8aToBuffer, u8aWrapBytes } from '@polkadot/util'; + +import { LEDGER_DEFAULT_ACCOUNT, LEDGER_DEFAULT_CHANGE, LEDGER_DEFAULT_INDEX, LEDGER_SUCCESS_CODE } from './constants.js'; +import { ledgerApps } from './defaults.js'; + +export { packageInfo } from './packageInfo.js'; + +type Chain = keyof typeof ledgerApps; + +type WrappedResult = Awaited>; + +/** @internal Wraps a SubstrateApp call, checking the result for any errors which result in a rejection */ +async function wrapError (promise: Promise): Promise { + const result = await promise; + + if (result.return_code !== LEDGER_SUCCESS_CODE) { + throw new Error(result.error_message); + } + + return result; +} + +/** @internal Wraps a sign/signRaw call and returns the associated signature */ +function sign (method: 'sign' | 'signRaw', message: Uint8Array, accountOffset = 0, addressOffset = 0, { account = LEDGER_DEFAULT_ACCOUNT, addressIndex = LEDGER_DEFAULT_INDEX, change = LEDGER_DEFAULT_CHANGE }: Partial = {}): (app: SubstrateApp) => Promise { + return async (app: SubstrateApp): Promise => { + const { signature } = await wrapError(app[method](account + accountOffset, change, addressIndex + addressOffset, u8aToBuffer(message))); + + return { + signature: hexAddPrefix(signature.toString('hex')) + }; + }; +} + +/** + * @name Ledger + * + * @description + * Legacy wrapper for a ledger app - + * - it connects automatically on use, creating an underlying interface as required + * - Promises reject with errors (unwrapped errors from @zondax/ledger-substrate) + */ +export class Ledger { + readonly #ledgerName: string; + readonly #transportDef: TransportDef; + + #app: SubstrateApp | null = null; + + constructor (transport: TransportType, chain: Chain) { + const ledgerName = ledgerApps[chain]; + const transportDef = transports.find(({ type }) => type === transport); + + if (!ledgerName) { + throw new Error(`Unsupported Ledger chain ${chain}`); + } else if (!transportDef) { + throw new Error(`Unsupported Ledger transport ${transport}`); + } + + this.#ledgerName = ledgerName; + this.#transportDef = transportDef; + } + + /** + * Returns the address associated with a specific account & address offset. Optionally + * asks for on-device confirmation + */ + public async getAddress (confirm = false, accountOffset = 0, addressOffset = 0, { account = LEDGER_DEFAULT_ACCOUNT, addressIndex = LEDGER_DEFAULT_INDEX, change = LEDGER_DEFAULT_CHANGE }: Partial = {}): Promise { + return this.withApp(async (app: SubstrateApp): Promise => { + const { address, pubKey } = await wrapError(app.getAddress(account + accountOffset, change, addressIndex + addressOffset, confirm)); + + return { + address, + publicKey: hexAddPrefix(pubKey) + }; + }); + } + + /** + * Returns the version of the Ledger application on the device + */ + public async getVersion (): Promise { + return this.withApp(async (app: SubstrateApp): Promise => { + const { device_locked: isLocked, major, minor, patch, test_mode: isTestMode } = await wrapError(app.getVersion()); + + return { + isLocked, + isTestMode, + version: [major, minor, patch] + }; + }); + } + + /** + * Signs a transaction on the Ledger device + */ + public async sign (message: Uint8Array, accountOffset?: number, addressOffset?: number, options?: Partial): Promise { + return this.withApp(sign('sign', message, accountOffset, addressOffset, options)); + } + + /** + * Signs a message (non-transactional) on the Ledger device + */ + public async signRaw (message: Uint8Array, accountOffset?: number, addressOffset?: number, options?: Partial): Promise { + return this.withApp(sign('signRaw', u8aWrapBytes(message), accountOffset, addressOffset, options)); + } + + /** + * @internal + * + * Returns a created SubstrateApp to perform operations against. Generally + * this is only used internally, to ensure consistent bahavior. + */ + async withApp (fn: (app: SubstrateApp) => Promise): Promise { + try { + if (!this.#app) { + const transport = await this.#transportDef.create(); + + // We need this override for the actual type passing - the Deno environment + // is quite a bit stricter and it yields invalids between the two (specifically + // since we mangle the imports from .default in the types for CJS/ESM and between + // esm.sh versions this yields problematic outputs) + // + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any + this.#app = newSubstrateApp(transport as any, this.#ledgerName); + } + + return await fn(this.#app); + } catch (error) { + this.#app = null; + + throw error; + } + } +} diff --git a/packages/hw-ledger/src/LedgerGeneric.ts b/packages/hw-ledger/src/LedgerGeneric.ts new file mode 100644 index 0000000000..77b5b627a9 --- /dev/null +++ b/packages/hw-ledger/src/LedgerGeneric.ts @@ -0,0 +1,166 @@ +// Copyright 2017-2024 @polkadot/hw-ledger authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { TransportDef, TransportType } from '@polkadot/hw-ledger-transports/types'; +import type { AccountOptions, LedgerAddress, LedgerSignature, LedgerVersion } from './types.js'; + +import { PolkadotGenericApp } from '@zondax/ledger-substrate'; + +import { transports } from '@polkadot/hw-ledger-transports'; +import { hexAddPrefix, u8aToBuffer, u8aWrapBytes } from '@polkadot/util'; + +import { ledgerApps } from './defaults.js'; + +export { packageInfo } from './packageInfo.js'; + +type Chain = keyof typeof ledgerApps; + +type WrappedResult = Awaited>; + +// This type is a copy of the `class ResponseError` +// imported from `@zondax/ledger-js`. This is a hack to avoid versioning issues +// with Deno. +interface ResponseError { + errorMessage: string + returnCode: number +} + +/** @internal Wraps a PolkadotGenericApp call, checking the result for any errors which result in a rejection */ +async function wrapError (promise: Promise): Promise { + let result: T; + + try { + result = await promise; + } catch (e: unknown) { + // We check to see if the propogated error is the newer ResponseError type. + // The response code use to be part of the result, but with the latest breaking changes from 0.42.x + // the interface and it's types have completely changed. + if ((e as ResponseError).returnCode) { + throw new Error((e as ResponseError).errorMessage); + } + + throw new Error((e as Error).message); + } + + return result; +} + +/** @internal Wraps a sign/signRaw call and returns the associated signature */ +function sign (method: 'sign' | 'signRaw', message: Uint8Array, slip44: number, addressOffset = 0, { addressIndex = 0 }: Partial = {}): (app: PolkadotGenericApp) => Promise { + const bip42Path = `m/44'/${slip44}'/${addressIndex}'/${0}'/${addressOffset}'`; + + return async (app: PolkadotGenericApp): Promise => { + const { signature } = await wrapError(app[method](bip42Path, u8aToBuffer(message))); + + return { + signature: hexAddPrefix(signature.toString('hex')) + }; + }; +} + +/** + * @name Ledger + * + * @description + * A very basic wrapper for a ledger app - + * - it connects automatically on use, creating an underlying interface as required + * - Promises reject with errors (unwrapped errors from @zondax/ledger-substrate) + */ +export class LedgerGeneric { + readonly #transportDef: TransportDef; + readonly #slip44: number; + readonly #chainId?: string; + readonly #metaUrl?: string; + + #app: PolkadotGenericApp | null = null; + + constructor (transport: TransportType, chain: Chain, slip44: number, chainId?: string, metaUrl?: string) { + const ledgerName = ledgerApps[chain]; + const transportDef = transports.find(({ type }) => type === transport); + + if (!ledgerName) { + throw new Error(`Unsupported Ledger chain ${chain}`); + } else if (!transportDef) { + throw new Error(`Unsupported Ledger transport ${transport}`); + } + + this.#metaUrl = metaUrl; + this.#chainId = chainId; + this.#slip44 = slip44; + this.#transportDef = transportDef; + } + + /** + * Returns the address associated with a specific account & address offset. Optionally + * asks for on-device confirmation + */ + public async getAddress (confirm = false, addressOffset = 0, ss58Prefix: number, { addressIndex = 0 }: Partial = {}): Promise { + const bip42Path = `m/44'/${this.#slip44}'/${addressIndex}'/${0}'/${addressOffset}'`; + + return this.withApp(async (app: PolkadotGenericApp): Promise => { + const { address, pubKey } = await wrapError(app.getAddress(bip42Path, ss58Prefix, confirm)); + + return { + address, + publicKey: hexAddPrefix(pubKey) + }; + }); + } + + /** + * Returns the version of the Ledger application on the device + */ + public async getVersion (): Promise { + return this.withApp(async (app: PolkadotGenericApp): Promise => { + const { deviceLocked: isLocked, major, minor, patch, testMode: isTestMode } = await wrapError(app.getVersion()); + + return { + isLocked: !!isLocked, + isTestMode: !!isTestMode, + version: [major || 0, minor || 0, patch || 0] + }; + }); + } + + /** + * Signs a transaction on the Ledger device + */ + public async sign (message: Uint8Array, addressOffset?: number, options?: Partial): Promise { + return this.withApp(sign('sign', message, this.#slip44, addressOffset, options)); + } + + /** + * Signs a message (non-transactional) on the Ledger device + */ + public async signRaw (message: Uint8Array, addressOffset?: number, options?: Partial): Promise { + return this.withApp(sign('signRaw', u8aWrapBytes(message), this.#slip44, addressOffset, options)); + } + + /** + * @internal + * + * Returns a created PolkadotGenericApp to perform operations against. Generally + * this is only used internally, to ensure consistent bahavior. + */ + async withApp (fn: (app: PolkadotGenericApp) => Promise): Promise { + try { + if (!this.#app) { + const transport = await this.#transportDef.create(); + + // We need this override for the actual type passing - the Deno environment + // is quite a bit stricter and it yields invalids between the two (specifically + // since we mangle the imports from .default in the types for CJS/ESM and between + // esm.sh versions this yields problematic outputs) + // + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any + this.#app = new PolkadotGenericApp(transport as any, this.#chainId, this.#metaUrl); + } + + return await fn(this.#app); + } catch (error) { + this.#app = null; + + throw error; + } + } +} diff --git a/packages/hw-ledger/src/bundle.ts b/packages/hw-ledger/src/bundle.ts index 2416c2fe4b..e67dc3323d 100644 --- a/packages/hw-ledger/src/bundle.ts +++ b/packages/hw-ledger/src/bundle.ts @@ -1,166 +1,5 @@ // Copyright 2017-2024 @polkadot/hw-ledger authors & contributors // SPDX-License-Identifier: Apache-2.0 -import type { TransportDef, TransportType } from '@polkadot/hw-ledger-transports/types'; -import type { AccountOptions, LedgerAddress, LedgerSignature, LedgerVersion } from './types.js'; - -import { PolkadotGenericApp } from '@zondax/ledger-substrate'; - -import { transports } from '@polkadot/hw-ledger-transports'; -import { hexAddPrefix, u8aToBuffer, u8aWrapBytes } from '@polkadot/util'; - -import { ledgerApps } from './defaults.js'; - -export { packageInfo } from './packageInfo.js'; - -type Chain = keyof typeof ledgerApps; - -type WrappedResult = Awaited>; - -// This type is a copy of the `class Response` error -// imported from `@zondax/ledger-js`. This is a hack to avoid versioning issues -// with Deno. -interface ResponseError { - errorMessage: string - returnCode: number -} - -/** @internal Wraps a PolkadotGenericApp call, checking the result for any errors which result in a rejection */ -async function wrapError (promise: Promise): Promise { - let result: T; - - try { - result = await promise; - } catch (e: unknown) { - // We check to see if the propogated error is the newer ResponseError type. - // The response code use to be part of the result, but with the latest breaking changes from 0.42.x - // the interface and it's types have completely changed. - if ((e as ResponseError).returnCode) { - throw new Error((e as ResponseError).errorMessage); - } - - throw new Error((e as Error).message); - } - - return result; -} - -/** @internal Wraps a sign/signRaw call and returns the associated signature */ -function sign (method: 'sign' | 'signRaw', message: Uint8Array, slip44: number, addressOffset = 0, { addressIndex = 0 }: Partial = {}): (app: PolkadotGenericApp) => Promise { - const bip42Path = `m/44'/${slip44}'/${addressIndex}'/${0}'/${addressOffset}'`; - - return async (app: PolkadotGenericApp): Promise => { - const { signature } = await wrapError(app[method](bip42Path, u8aToBuffer(message))); - - return { - signature: hexAddPrefix(signature.toString('hex')) - }; - }; -} - -/** - * @name Ledger - * - * @description - * A very basic wrapper for a ledger app - - * - it connects automatically on use, creating an underlying interface as required - * - Promises reject with errors (unwrapped errors from @zondax/ledger-substrate) - */ -export class Ledger { - readonly #transportDef: TransportDef; - readonly #slip44: number; - readonly #chainId?: string; - readonly #metaUrl?: string; - - #app: PolkadotGenericApp | null = null; - - constructor (transport: TransportType, chain: Chain, slip44: number, chainId?: string, metaUrl?: string) { - const ledgerName = ledgerApps[chain]; - const transportDef = transports.find(({ type }) => type === transport); - - if (!ledgerName) { - throw new Error(`Unsupported Ledger chain ${chain}`); - } else if (!transportDef) { - throw new Error(`Unsupported Ledger transport ${transport}`); - } - - this.#metaUrl = metaUrl; - this.#chainId = chainId; - this.#slip44 = slip44; - this.#transportDef = transportDef; - } - - /** - * Returns the address associated with a specific account & address offset. Optionally - * asks for on-device confirmation - */ - public async getAddress (confirm = false, addressOffset = 0, ss58Prefix: number, { addressIndex = 0 }: Partial = {}): Promise { - const bip42Path = `m/44'/${this.#slip44}'/${addressIndex}'/${0}'/${addressOffset}'`; - - return this.withApp(async (app: PolkadotGenericApp): Promise => { - const { address, pubKey } = await wrapError(app.getAddress(bip42Path, ss58Prefix, confirm)); - - return { - address, - publicKey: hexAddPrefix(pubKey) - }; - }); - } - - /** - * Returns the version of the Ledger application on the device - */ - public async getVersion (): Promise { - return this.withApp(async (app: PolkadotGenericApp): Promise => { - const { deviceLocked: isLocked, major, minor, patch, testMode: isTestMode } = await wrapError(app.getVersion()); - - return { - isLocked: !!isLocked, - isTestMode: !!isTestMode, - version: [major || 0, minor || 0, patch || 0] - }; - }); - } - - /** - * Signs a transaction on the Ledger device - */ - public async sign (message: Uint8Array, addressOffset?: number, options?: Partial): Promise { - return this.withApp(sign('sign', message, this.#slip44, addressOffset, options)); - } - - /** - * Signs a message (non-transactional) on the Ledger device - */ - public async signRaw (message: Uint8Array, addressOffset?: number, options?: Partial): Promise { - return this.withApp(sign('signRaw', u8aWrapBytes(message), this.#slip44, addressOffset, options)); - } - - /** - * @internal - * - * Returns a created PolkadotGenericApp to perform operations against. Generally - * this is only used internally, to ensure consistent bahavior. - */ - async withApp (fn: (app: PolkadotGenericApp) => Promise): Promise { - try { - if (!this.#app) { - const transport = await this.#transportDef.create(); - - // We need this override for the actual type passing - the Deno environment - // is quite a bit stricter and it yields invalids between the two (specifically - // since we mangle the imports from .default in the types for CJS/ESM and between - // esm.sh versions this yields problematic outputs) - // - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any - this.#app = new PolkadotGenericApp(transport as any, this.#chainId, this.#metaUrl); - } - - return await fn(this.#app); - } catch (error) { - this.#app = null; - - throw error; - } - } -} +export { Ledger } from './Ledger.js'; +export { LedgerGeneric } from './LedgerGeneric.js';