From c763b2ac98cea87414a9d49ff5f7b6409687a36c Mon Sep 17 00:00:00 2001 From: HenryNguyen5 Date: Mon, 21 May 2018 16:47:25 -0400 Subject: [PATCH] Ledger types (#1690) * Do not truncate errors, pretty output * Introduce helpers for sagas * Update yarn lock * Initial types * Finish types * cleanup * Fix imports and filenames to cooperate with internal typings --- common/typescript/ledger/hw-app-eth.d.ts | 56 ++++ .../ledger/hw-transport-node-hid.d.ts | 97 +++++++ .../typescript/ledger/hw-transport-u2f.d.ts | 84 ++++++ common/typescript/ledger/hw-transport.d.ts | 274 ++++++++++++++++++ package.json | 3 + yarn.lock | 12 + 6 files changed, 526 insertions(+) create mode 100644 common/typescript/ledger/hw-app-eth.d.ts create mode 100644 common/typescript/ledger/hw-transport-node-hid.d.ts create mode 100644 common/typescript/ledger/hw-transport-u2f.d.ts create mode 100644 common/typescript/ledger/hw-transport.d.ts diff --git a/common/typescript/ledger/hw-app-eth.d.ts b/common/typescript/ledger/hw-app-eth.d.ts new file mode 100644 index 00000000..b22a4ef0 --- /dev/null +++ b/common/typescript/ledger/hw-app-eth.d.ts @@ -0,0 +1,56 @@ +declare module '@ledgerhq/hw-app-eth' { + import LedgerTransport from '@ledgerhq/hw-transport'; + + export default class Eth> { + constructor(transport: T); + + /** + * + * @description get Ethereum address for a given BIP 32 path. + * @param {string} path a path in BIP 32 format + * @param {boolean} [boolDisplay] enable or not the display + * @param {boolean} [boolChaincode] enable or not the chaincode request + * @returns {Promise<{ publicKey: string; address: string; chainCode?: string }>} + * @memberof Eth + */ + public getAddress( + path: string, + boolDisplay?: boolean, + boolChaincode?: boolean + ): Promise<{ publicKey: string; address: string; chainCode?: string }>; + + /** + * + * @description signs a raw transaction and returns v,r,s + * @param {string} path + * @param {string} rawTxHex + * @returns {Promise<{s: string, v: string, r: string}>} + * @memberof Eth + */ + public signTransaction( + path: string, + rawTxHex: string + ): Promise<{ s: string; v: string; r: string }>; + + /** + * + * + * @returns {Promise<{ arbitraryDataEnabled: number; version: string }>} + * @memberof Eth + */ + public getAppConfiguration(): Promise<{ arbitraryDataEnabled: number; version: string }>; + + /** + * + * @description sign a message according to eth_sign RPC call + * @param {string} path + * @param {string} messageHex + * @returns {Promise<{v: number, s: string, r: string}>} + * @memberof Eth + */ + public signPersonalMessage( + path: string, + messageHex: string + ): Promise<{ v: number; s: string; r: string }>; + } +} diff --git a/common/typescript/ledger/hw-transport-node-hid.d.ts b/common/typescript/ledger/hw-transport-node-hid.d.ts new file mode 100644 index 00000000..5db97fa6 --- /dev/null +++ b/common/typescript/ledger/hw-transport-node-hid.d.ts @@ -0,0 +1,97 @@ +declare module '@ledgerhq/hw-transport-node-hid' { + import LedgerTransport, { Observer, DescriptorEvent, Subscription } from '@ledgerhq/hw-transport'; + import { HID, Device } from 'node-hid'; + + export default class TransportNodeHid extends LedgerTransport { + /** + * @description Creates an instance of TransportNodeHid. + * @example + * + * ```ts + * import TransportNodeHid from "@ledgerhq/hw-transport-node-u2f"; + * TransportNodeHid.create().then(transport => ...); + * ``` + * + * @param {HID} device + * @param {boolean} [ledgerTransport] + * @param {number} [timeout] + * @param {boolean} [debug] + * @memberof TransportNodeHid + */ + constructor(device: HID, ledgerTransport?: boolean, timeout?: number, debug?: boolean); + + /** + * + * @description Check if an HID instance is active + * @static + * @returns {Promise} + * @memberof TransportNodeHid + */ + public static isSupported(): Promise; + + /** + * + * @description Lists all available HID device's paths + * @static + * @returns {Promise} + * @memberof TransportNodeHid + */ + public static list(): Promise; + + /** + * + * @description Listen all device events for a given Transport. + * The method takes an Observer of DescriptorEvent and returns a Subscription + * according to Observable paradigm https://github.com/tc39/proposal-observable + * a DescriptorEvent is a { descriptor, type } object. + * Type can be "add" or "remove" and descriptor is a value you can pass to open(descriptor). + * Each listen() call will first emit all potential device already connected and then will emit events can come over times, + * for instance if you plug a USB device after listen(). + * @static + * @template Descriptor + * @template Device + * @template Err + * @param {Observer, Err>} observer + * @returns {Subscription} + * @memberof TransportNodeHid + */ + public static listen( + observer: Observer, Err> + ): Subscription; + + /** + * + * @description Attempt to create a Transport instance with a descriptor. + * @static + * @template Descriptor + * @param {Descriptor} devicePath + * @returns {Promise} + * @memberof TransportNodeHid + */ + public static open(devicePath: Descriptor): Promise; + + /** + * + * @description Low level api to communicate with the device. + * @param {Buffer} apdu + * @returns {Promise} + * @memberof TransportNodeHid + */ + public exchange(apdu: Buffer): Promise; + + /** + * + * @description Does nothing + * @memberof TransportNodeHid + */ + public setScrambleKey(): void; + + /** + * + * @description Close the exchange with the device. + * @returns {Promise} + * @memberof TransportNodeHid + */ + public close(): Promise; + } +} diff --git a/common/typescript/ledger/hw-transport-u2f.d.ts b/common/typescript/ledger/hw-transport-u2f.d.ts new file mode 100644 index 00000000..88920b75 --- /dev/null +++ b/common/typescript/ledger/hw-transport-u2f.d.ts @@ -0,0 +1,84 @@ +declare module '@ledgerhq/hw-transport-u2f' { + import LedgerTransport, { + Observer, + DescriptorEvent, + Subscription, + TransportError + } from '@ledgerhq/hw-transport'; + import { isSupported, sign } from 'u2f-api'; + + export default class TransportU2F extends LedgerTransport { + public static isSupported: typeof isSupported; + + /** + * @description this transport is not discoverable but we are going to guess if it is here with isSupported() + * @static + * @template Descriptor An array with [null] if supported device + * @returns {Descriptor} + * @memberof TransportU2F + */ + public static list(): Descriptor; + + /** + * + * @description Listen all device events for a given Transport. + * The method takes an Observer of DescriptorEvent and returns a Subscription + * according to Observable paradigm https://github.com/tc39/proposal-observable + * a DescriptorEvent is a { descriptor, type } object. + * Type can be "add" or "remove" and descriptor is a value you can pass to open(descriptor). + * Each listen() call will first emit all potential device already connected and then will emit events can come over times, + * @static + * @template Descriptor + * @template Device + * @template Err + * @param {Observer< + * DescriptorEvent, + * ErrParam + * >} observer + * @returns {Subscription} + * @memberof TransportU2F + */ + public static listen( + observer: Observer, Err> + ): Subscription; + + /** + * + * @description static function to create a new Transport from a connected + * Ledger device discoverable via U2F (browser support) + * + * @static + * @param {*} _ + * @param {number} [_openTimeout] + * @returns {Promise} + * @memberof TransportU2F + */ + public static open(_?: any, __?: number): Promise; + + /** + * + * @description Low level api to communicate with the device. + * @param {Buffer} adpu + * @returns {Promise} + * @memberof TransportU2F + */ + public exchange(adpu: Buffer): Promise; + + /** + * + * @description Set the "scramble key" for the next exchange with the device. + * Each App can have a different scramble key and they internally will set it at instantiation. + * @param {string} scrambleKey + * @memberof TransportU2F + */ + public setScrambleKey(scrambleKey: string): void; + + /** + * + * @description Close the exchange with the device. + * @returns {Promise} + * @memberof TransportU2F + */ + public close(): Promise; + } +} diff --git a/common/typescript/ledger/hw-transport.d.ts b/common/typescript/ledger/hw-transport.d.ts new file mode 100644 index 00000000..7a1f9a4c --- /dev/null +++ b/common/typescript/ledger/hw-transport.d.ts @@ -0,0 +1,274 @@ +declare module '@ledgerhq/hw-transport' { + /** + * @description all possible status codes. + * @see https://github.com/LedgerHQ/blue-app-btc/blob/d8a03d10f77ca5ef8b22a5d062678eef788b824a/include/btchip_apdu_constants.h#L85-L115 + * @example + * import { StatusCodes } from "@ledgerhq/hw-transport"; + * @export + * @enum {number} + */ + export enum StatusCodes { + PIN_REMAINING_ATTEMPTS = 0x63c0, + INCORRECT_LENGTH = 0x6700, + COMMAND_INCOMPATIBLE_FILE_STRUCTURE = 0x6981, + SECURITY_STATUS_NOT_SATISFIED = 0x6982, + CONDITIONS_OF_USE_NOT_SATISFIED = 0x6985, + INCORRECT_DATA = 0x6a80, + NOT_ENOUGH_MEMORY_SPACE = 0x6a84, + REFERENCED_DATA_NOT_FOUND = 0x6a88, + FILE_ALREADY_EXISTS = 0x6a89, + INCORRECT_P1_P2 = 0x6b00, + INS_NOT_SUPPORTED = 0x6d00, + CLA_NOT_SUPPORTED = 0x6e00, + TECHNICAL_PROBLEM = 0x6f00, + OK = 0x9000, + MEMORY_PROBLEM = 0x9240, + NO_EF_SELECTED = 0x9400, + INVALID_OFFSET = 0x9402, + FILE_NOT_FOUND = 0x9404, + INCONSISTENT_FILE = 0x9408, + ALGORITHM_NOT_SUPPORTED = 0x9484, + INVALID_KCV = 0x9485, + CODE_NOT_INITIALIZED = 0x9802, + ACCESS_CONDITION_NOT_FULFILLED = 0x9804, + CONTRADICTION_SECRET_CODE_STATUS = 0x9808, + CONTRADICTION_INVALIDATION = 0x9810, + CODE_BLOCKED = 0x9840, + MAX_VALUE_REACHED = 0x9850, + GP_AUTH_FAILED = 0x6300, + LICENSING = 0x6f42, + HALTED = 0x6faa + } + + export enum AltStatusCodes { + 'Incorrect length' = 0x6700, + 'Security not satisfied (dongle locked or have invalid access rights)' = 0x6982, + 'Condition of use not satisfied (denied by the user?)' = 0x6985, + 'Invalid data received' = 0x6a80, + 'Invalid parameter received' = 0x6b00, + INTERNAL_ERROR = 'Internal error, please report' + } + + export interface Subscription { + unsubscribe: () => void; + } + + export interface ITransportError extends Error { + name: 'TransportError'; + message: string; + stack?: string; + id: string; + } + + /** + * TransportError is used for any generic transport errors. + * e.g. Error thrown when data received by exchanges are incorrect or if exchanged failed to communicate with the device for various reason. + */ + export class TransportError extends Error { + new(message: string, id: string): ITransportError; + } + + export interface ITransportStatusError extends Error { + name: 'TransportStatusError'; + message: string; + stack?: string; + statusCode: number; + statusText: keyof typeof StatusCodes | 'UNKNOWN_ERROR'; + } + + /** + * Error thrown when a device returned a non success status. + * the error.statusCode is one of the `StatusCodes` exported by this library. + */ + export class TransportStatusError extends Error { + new(statusCode: number): ITransportStatusError; + } + + export interface Observer { + next: (event: Ev) => void; + error: (e: Err) => void; + complete: () => void; + } + + export interface DescriptorEvent { + type: 'add' | 'remove'; + descriptor: Descriptor; + device?: Device; + } + + export type FunctionPropertyNames = { + [K in keyof T]: T[K] extends Function ? K : never + }[keyof T]; + + export type ExtractPromise = T extends Promise ? U : T; + + export default abstract class LedgerTransport { + /** + * + * @description Check if a transport is supported on the user's platform/browser. + * @static + * @returns {Promise} + * @memberof LedgerTransport + */ + public static isSupported(): Promise; + + /** + * + * @description List once all available descriptors. For a better granularity, checkout listen(). + * @static + * @template Descriptor + * @returns {Promise} + * @memberof LedgerTransport + */ + public static list(): Promise; + + /** + * + * @description Listen all device events for a given Transport. + * The method takes an Observer of DescriptorEvent and returns a Subscription + * according to Observable paradigm https://github.com/tc39/proposal-observable + * a DescriptorEvent is a { descriptor, type } object. + * Type can be "add" or "remove" and descriptor is a value you can pass to open(descriptor). + * Each listen() call will first emit all potential device already connected and then will emit events can come over times, + * for instance if you plug a USB device after listen() or a bluetooth device become discoverable. + * @static + * @template Descriptor + * @template Device + * @template Err + * @param {Observer, Err>} observer + * @returns {Subscription} + * @memberof LedgerTransport + */ + public static listen( + observer: Observer, Err> + ): Subscription; + + /** + * + * @description Attempt to create a Transport instance with potentially a descriptor. + * @static + * @template Descriptor + * @param {Descriptor} descriptor + * @param {number} [timeout] + * @returns {Promise>} + * @memberof LedgerTransport + */ + public static open( + descriptor: Descriptor, + timeout?: number + ): Promise>; + + /** + * + * @description create() attempts open the first descriptor available or throw if: + * - there is no descriptor + * - if either timeout is reached + * + * This is a light alternative to using listen() and open() that you may need for any advanced usecases + * @static + * @template Descriptor + * @param {number} [openTimeout] + * @param {number} [listenTimeout] + * @returns {Promise>} + * @memberof LedgerTransport + */ + public static create( + openTimeout?: number, + listenTimeout?: number + ): Promise>; + + /** + * + * @description Low level api to communicate with the device. + * This method is for implementations to implement but should not be directly called. + * Instead, the recommended way is to use send() method + * @param {Buffer} apdu + * @returns {Promise} + * @memberof LedgerTransport + */ + public abstract exchange(apdu: Buffer): Promise; + + /** + * + * @description Set the "scramble key" for the next exchange with the device. + * Each App can have a different scramble key and they internally will set it at instantiation. + * @param {string} scrambleKey + * @memberof LedgerTransport + */ + public setScrambleKey(scrambleKey: string): void; + + /** + * + * @description Close the exchange with the device. + * @returns {Promise} + * @memberof LedgerTransport + */ + public close(): Promise; + + /** + * + * @description Listen to an event on an instance of transport. + * Transport implementation can have specific events. Here are the common events: + * - "disconnect" : triggered if Transport is disconnected + * @param {string} eventName + * @param {Listener} cb + * @memberof LedgerTransport + */ + public on(eventName: string | 'listen', cb: (...args: any[]) => any): void; + + /** + * + * @description Stop listening to an event on an instance of transport. + * @param {string} eventName + * @param {Listener} cb + * @memberof LedgerTransport + */ + public off(eventName: string, cb: (...args: any[]) => any): void; + + /** + * + * @description Toggle logs of binary exchange + * @param {boolean} debug + * @memberof LedgerTransport + */ + public setDebugMode(debug: boolean): void; + + /** + * @description Set a timeout (in milliseconds) for the exchange call. + * Only some transport might implement it. (e.g. U2F) + * @param {number} exchangeTimeout + * @memberof LedgerTransport + */ + public setExchangeTimeout?(exchangeTimeout: number): void; + + /** + * @description Used to decorate all callable public methods of an app so that they + * are mutually exclusive. Scramble key is application specific, e.g hw-app-eth will set + * its own scramblekey + * @param self + * @param methods + * @param scrambleKey + */ + public decorateAppAPIMethods( + self: T, + methods: FunctionPropertyNames[], + scrambleKey: string + ): void; + + /** + * @description Decorates a function so that it uses a global mutex, if an + * exchange is already in process, then calling the function will throw an + * error about being locked + * @param methodName + * @param functionToDecorate + * @param thisContext + * @param scrambleKey + */ + public decorateAppAPIMethod( + methodName: FunctionPropertyNames, + functionToDecorate: (...args: FArgs[]) => FRet, + thisContext: T, + scrambleKey: string + ): (...args: FArgs[]) => Promise>; // make sure we dont wrap promises twice + } +} diff --git a/package.json b/package.json index ac0440df..b6bd0a5a 100644 --- a/package.json +++ b/package.json @@ -64,10 +64,12 @@ "@types/classnames": "2.2.3", "@types/enzyme": "3.1.8", "@types/enzyme-adapter-react-16": "1.0.1", + "@types/events": "1.2.0", "@types/history": "4.6.2", "@types/jest": "22.2.3", "@types/lodash": "4.14.107", "@types/moment-timezone": "0.5.4", + "@types/node-hid": "0.7.0", "@types/qrcode": "0.8.0", "@types/qrcode.react": "0.6.3", "@types/query-string": "5.1.0", @@ -131,6 +133,7 @@ "tslint-react": "3.5.1", "types-rlp": "0.0.1", "typescript": "2.8.1", + "u2f-api": "1.0.6", "uglifyjs-webpack-plugin": "1.2.4", "url-search-params-polyfill": "3.0.0", "webapp-webpack-plugin": "2.0.1", diff --git a/yarn.lock b/yarn.lock index 056c6cf2..07e0c0f7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -88,6 +88,10 @@ "@types/cheerio" "*" "@types/react" "*" +"@types/events@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86" + "@types/history@*", "@types/history@4.6.2": version "4.6.2" resolved "https://registry.yarnpkg.com/@types/history/-/history-4.6.2.tgz#12cfaba693ba20f114ed5765467ff25fdf67ddb0" @@ -106,6 +110,10 @@ dependencies: moment ">=2.14.0" +"@types/node-hid@0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@types/node-hid/-/node-hid-0.7.0.tgz#f696f39c528059116236e41df90c8fcba077d711" + "@types/node@*", "@types/node@^9.6.2": version "9.6.5" resolved "https://registry.yarnpkg.com/@types/node/-/node-9.6.5.tgz#ee700810fdf49ac1c399fc5980b7559b3e5a381d" @@ -11159,6 +11167,10 @@ u2f-api@0.2.7: version "0.2.7" resolved "https://registry.yarnpkg.com/u2f-api/-/u2f-api-0.2.7.tgz#17bf196b242f6bf72353d9858e6a7566cc192720" +u2f-api@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/u2f-api/-/u2f-api-1.0.6.tgz#fdde2a0788fcf7d8d273aa8688b217625961f866" + ua-parser-js@^0.7.9: version "0.7.17" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.17.tgz#e9ec5f9498b9ec910e7ae3ac626a805c4d09ecac"