// Copyright 2017-2023 @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 } 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; } // A very basic wrapper for a ledger app - // - it connects automatically, creating an app as required // - Promises return errors (instead of wrapper errors) export class Ledger { #app: SubstrateApp | null = null; #ledgerName: string; #transportDef: TransportDef; 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 transcation on the Ledger device */ public async sign (message: Uint8Array, 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 buffer = u8aToBuffer(message); const { signature } = await wrapError(app.sign(account + accountOffset, change, addressIndex + addressOffset, buffer)); return { signature: hexAddPrefix(signature.toString('hex')) }; }); } /** * @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(); this.#app = newSubstrateApp(transport, this.#ledgerName); } return await fn(this.#app); } catch (error) { this.#app = null; throw error; } } }