Files
common/packages/hw-ledger/src/bundle.ts
T
Jaco 514570a5fb Additional hw-ledger cleanups (#1775)
* Additional hw-ledger cleanups

* Rename ledgerName
2023-03-24 10:29:45 +02:00

123 lines
4.2 KiB
TypeScript

// 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<ReturnType<SubstrateApp['getAddress' | 'getVersion' | 'sign']>>;
/** @internal Wraps a SubstrateApp call, checking the result for any errors which result in a rejection */
async function wrapError <T extends WrappedResult> (promise: Promise<T>): Promise<T> {
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<AccountOptions> = {}): Promise<LedgerAddress> {
return this.withApp(async (app: SubstrateApp): Promise<LedgerAddress> => {
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<LedgerVersion> {
return this.withApp(async (app: SubstrateApp): Promise<LedgerVersion> => {
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<AccountOptions> = {}): Promise<LedgerSignature> {
return this.withApp(async (app: SubstrateApp): Promise<LedgerSignature> => {
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 <T> (fn: (app: SubstrateApp) => Promise<T>): Promise<T> {
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;
}
}
}