From 0d122537a8317b93f018d7b722bd762ae3682969 Mon Sep 17 00:00:00 2001 From: Martin Varmuza Date: Fri, 31 Jan 2025 20:41:40 +0100 Subject: [PATCH] fixup: rejections removed up to typedCall --- packages/connect-web/tsconfig.lib.json | 2 +- .../connect-webextension/tsconfig.lib.json | 2 +- packages/connect/src/core/index.ts | 2 +- packages/connect/src/device/Device.ts | 12 +- packages/connect/src/device/DeviceCommands.ts | 126 ++++++++++-------- packages/connect/src/device/prompts.ts | 52 +++++--- packages/transport/src/types/apiCall.ts | 2 +- 7 files changed, 118 insertions(+), 80 deletions(-) diff --git a/packages/connect-web/tsconfig.lib.json b/packages/connect-web/tsconfig.lib.json index 0120e049f8f..46c9ff7c5d9 100644 --- a/packages/connect-web/tsconfig.lib.json +++ b/packages/connect-web/tsconfig.lib.json @@ -2,7 +2,7 @@ "extends": "../../tsconfig.lib.json", "compilerOptions": { "outDir": "lib", - "target": "es2017", + "target": "es2019", "types": ["chrome", "w3c-web-usb"] }, "include": ["./src"], diff --git a/packages/connect-webextension/tsconfig.lib.json b/packages/connect-webextension/tsconfig.lib.json index b2f9e227332..33704e7e465 100644 --- a/packages/connect-webextension/tsconfig.lib.json +++ b/packages/connect-webextension/tsconfig.lib.json @@ -2,7 +2,7 @@ "extends": "../../tsconfig.lib.json", "compilerOptions": { "outDir": "lib", - "target": "es2017", + "target": "es2019", "types": ["chrome", "w3c-web-usb"] }, "include": ["./src"], diff --git a/packages/connect/src/core/index.ts b/packages/connect/src/core/index.ts index cf7b4fcfa26..02388863d37 100644 --- a/packages/connect/src/core/index.ts +++ b/packages/connect/src/core/index.ts @@ -922,7 +922,7 @@ const onPopupClosed = (context: CoreContext, customErrorMessage?: string) => { deviceList.getAllDevices().forEach(d => { d.releaseTransportSession(); // clear transportSession on release if (d.isUsedHere()) { - setOverridePromise(d.interruptionFromUser(error)); + setOverridePromise(d.interruptionFromUser(error.toString())); } else { const success = uiPromises.resolve({ type: DEVICE.DISCONNECT, payload: undefined }); if (!success) { diff --git a/packages/connect/src/device/Device.ts b/packages/connect/src/device/Device.ts index 51aadc75301..fe54dcdddbb 100644 --- a/packages/connect/src/device/Device.ts +++ b/packages/connect/src/device/Device.ts @@ -163,7 +163,7 @@ export class Device extends TypedEmitter { private keepTransportSession = false; public commands?: DeviceCommands; - private cancelableAction?: (err?: Error) => Promise; + private cancelableAction?: (err?: string) => Promise; private loaded = false; @@ -447,7 +447,7 @@ export class Device extends TypedEmitter { } if (this.runPromise) { - await this.interruptionFromUser(error); + await this.interruptionFromUser(error.toString()); } if (this.releasePromise) { await this.releasePromise; @@ -455,7 +455,7 @@ export class Device extends TypedEmitter { } setCancelableAction(callback: NonNullable) { - this.cancelableAction = (e?: Error) => + this.cancelableAction = (e?: string) => callback(e) .catch(e2 => { _log.debug('cancelableAction error', e2); @@ -469,7 +469,7 @@ export class Device extends TypedEmitter { this.cancelableAction = undefined; } - async interruptionFromUser(error: Error) { + async interruptionFromUser(error: string) { _log.debug('interruptionFromUser'); await this.cancelableAction?.(error); @@ -477,7 +477,7 @@ export class Device extends TypedEmitter { if (this.runPromise) { // reject inner defer - this.runPromise.reject(error); + this.runPromise.reject(new Error(error)); delete this.runPromise; } } @@ -1046,7 +1046,7 @@ export class Device extends TypedEmitter { this.emitLifecycle(DEVICE.DISCONNECT); - // return this.interruptionFromUser(ERRORS.TypedError('Device_Disconnected')); + // return this.ERRORS.TypedError('Device_Disconnected')); } isBootloader() { diff --git a/packages/connect/src/device/DeviceCommands.ts b/packages/connect/src/device/DeviceCommands.ts index aebf093ff50..5b2297288bd 100644 --- a/packages/connect/src/device/DeviceCommands.ts +++ b/packages/connect/src/device/DeviceCommands.ts @@ -1,10 +1,9 @@ // original file https://github.com/trezor/connect/blob/develop/src/js/device/DeviceCommands.js -import { ErrorGeneric } from '@trezor/transport/src/types'; -import { ReadWriteError } from '@trezor/transport/src/transports/abstract'; import { MessagesSchema as Messages } from '@trezor/protobuf'; import { Assert } from '@trezor/schema-utils'; import { Session, Transport } from '@trezor/transport'; +import { ReadWriteError } from '@trezor/transport/src/transports/abstract'; import { createTimeoutPromise, versionUtils } from '@trezor/utils'; import { ERRORS } from '../constants'; @@ -281,23 +280,6 @@ export class DeviceCommands { return this._getAddress(); } - // Sends an async message to the opened device. - private async call(type: MessageKey, msg: DefaultPayloadMessage['message'] = {}) { - logger.debug('Sending', type, filterForLog(type, msg)); - - this.callPromise = this.transport.call({ - session: this.transportSession, - name: type, - data: msg, - protocol: this.device.protocol, - }); - - const res = await this.callPromise; - this.callPromise = undefined; - - return res; - } - typedCall( type: T, resType: R, @@ -322,13 +304,17 @@ export class DeviceCommands { const response = await this._commonCall(type, msg); - if ('isTransportError' in response) { - throw new Error(response.error, { cause: 'transport-error' }); + if (!response.success) { + if (response.isTransportError) { + throw new Error(response.error, { cause: 'transport-error' }); + } else { + throw new Error(response.error); + } } const splitResTypes = Array.isArray(resType) ? resType : resType.split('|'); - if (splitResTypes.includes(response.type)) { - return response; + if (splitResTypes.includes(response.payload.type)) { + return response.payload; } // handle possible race condition // Bridge may have some unread message in buffer, read it @@ -351,20 +337,33 @@ export class DeviceCommands { throw ERRORS.TypedError( 'Runtime', - `assertType: Response of unexpected type: ${response.type}. Should be ${resType}`, + `assertType: Response of unexpected type: ${response.payload.type}. Should be ${resType}`, ); } async _commonCall( type: MessageKey, - msg?: DefaultPayloadMessage['message'], + msg: DefaultPayloadMessage['message'] = {}, ): Promise< - DefaultPayloadMessage | (ErrorGeneric & { isTransportError: true }) + | { success: true; payload: DefaultPayloadMessage } + | { success: false; error: any; isTransportError: false } + | { success: false; error: ReadWriteError; message?: string; isTransportError: true } > { if (this.disposed) { throw ERRORS.TypedError('Runtime', 'typedCall: DeviceCommands already disposed'); } - const res = await this.call(type, msg); + + logger.debug('Sending', type, filterForLog(type, msg)); + + this.callPromise = this.transport.call({ + session: this.transportSession, + name: type, + data: msg, + protocol: this.device.protocol, + }); + + const res = await this.callPromise; + this.callPromise = undefined; if (!res.success) { logger.warn( @@ -389,7 +388,13 @@ export class DeviceCommands { return this._filterCommonTypes(res.payload as DefaultPayloadMessage); } - _filterCommonTypes(res: DefaultPayloadMessage) { + _filterCommonTypes( + res: DefaultPayloadMessage, + ): Promise< + | { success: true; payload: DefaultPayloadMessage } + | { success: false; error: any; isTransportError: false } + | { success: false; error: ReadWriteError; message?: string; isTransportError: true } + > { this.device.clearCancelableAction(); if (res.type === 'Failure') { @@ -409,16 +414,17 @@ export class DeviceCommands { } // pass code and message from firmware error - return Promise.reject( - new ERRORS.TrezorError( - (code as any) || 'Failure_UnknownCode', - message || 'Failure_UnknownMessage', - ), - ); + return Promise.resolve({ + success: false, + // todo: check renaming error vs code + error: code || 'Failure_UnknownCode', + message: message || 'Failure_UnknownMessage', + isTransportError: false, + }); } if (res.type === 'Features') { - return Promise.resolve(res); + return Promise.resolve({ success: true, payload: res }); } if (res.type === 'ButtonRequest') { @@ -434,38 +440,52 @@ export class DeviceCommands { } if (res.type === 'PinMatrixRequest') { - return promptPin(this.device, res.message.type).then( - pin => - this._commonCall('PinMatrixAck', { pin }).then(response => { + return promptPin(this.device, res.message.type).then(promptRes => { + if (!promptRes.success) { + return promptRes; + } + + return this._commonCall('PinMatrixAck', { pin: promptRes.payload }).then( + response => { + if (!response.success) { + return response; + } if (!this.device.features.unlocked) { // reload features to after successful PIN return this.device.getFeatures().then(() => response); } return response; - }), - error => Promise.reject(error), - ); + }, + ); + }); } + // { value, passphraseOnDevice } if (res.type === 'PassphraseRequest') { - return promptPassphrase(this.device).then( - ({ value, passphraseOnDevice }) => - !passphraseOnDevice - ? this._commonCall('PassphraseAck', { passphrase: value.normalize('NFKD') }) - : this._commonCall('PassphraseAck', { on_device: true }), - error => Promise.reject(error), - ); + return promptPassphrase(this.device).then(promptRes => { + if (!promptRes.success) { + return promptRes; + } + const { value, passphraseOnDevice } = promptRes.payload; + + return !passphraseOnDevice + ? this._commonCall('PassphraseAck', { passphrase: value.normalize('NFKD') }) + : this._commonCall('PassphraseAck', { on_device: true }); + }); } if (res.type === 'WordRequest') { - return promptWord(this.device, res.message.type).then( - word => this._commonCall('WordAck', { word }), - error => Promise.reject(error), - ); + return promptWord(this.device, res.message.type).then(promptRes => { + if (!promptRes.success) { + return promptRes; + } + + return this._commonCall('WordAck', { word: promptRes.payload }); + }); } - return Promise.resolve(res); + return Promise.resolve({ success: true, payload: res }); } private async _getAddress() { diff --git a/packages/connect/src/device/prompts.ts b/packages/connect/src/device/prompts.ts index 741dae64c6b..5fb9cc7059e 100644 --- a/packages/connect/src/device/prompts.ts +++ b/packages/connect/src/device/prompts.ts @@ -1,10 +1,10 @@ import { Messages, TRANSPORT_ERROR } from '@trezor/transport'; +import { ReadWriteError } from '@trezor/transport/src/transports/abstract'; -import { ERRORS } from '../constants'; import { DEVICE } from '../events'; import type { Device, DeviceEvents } from './Device'; -export type PromptCallback = (response: T | null, error?: Error) => void; +export type PromptCallback = (response: T | null, error?: string) => void; type PromptEvents = typeof DEVICE.PIN | typeof DEVICE.PASSPHRASE | typeof DEVICE.WORD; // infer all args of Device.emit but one (callback) @@ -40,20 +40,38 @@ export const cancelPrompt = (device: Device, expectResponse = true) => { return expectResponse ? device.transport.call(cancelArgs) : device.transport.send(cancelArgs); }; -const prompt = (event: E, ...[device, ...args]: DeviceEventArgs) => +type PromptReturnType = Promise< + | { success: true; payload: NonNullable>[0]> } + | ({ success: false } & ( + | { error: string; isTransportError: false } + | { error: ReadWriteError; isTransportError: true } + )) +>; + +const prompt = ( + event: E, + ...[device, ...args]: DeviceEventArgs +): PromptReturnType => // return non nullable first arg of PromptCallback - new Promise>[0]>>((resolve, reject) => { - const cancelAndReject = (error?: Error) => - cancelPrompt(device).then(onCancel => - reject( - error || - new Error( - onCancel.success - ? (onCancel.payload?.message.message as string) - : onCancel.error, - ), - ), - ); + new Promise(resolve => { + const cancelAndReject = (error?: string) => + cancelPrompt(device).then(cancelResponse => { + if (cancelResponse.success) { + return resolve({ + success: false, + error: error || (cancelResponse.payload?.message.message as string), + isTransportError: false, + }); + } + + resolve({ + success: false, + error: error || cancelResponse.error, + // todo todo: + // @ts-expect-error + isTransportError: true, + }); + }); if (device.listenerCount(event) > 0) { device.setCancelableAction(cancelAndReject); @@ -63,7 +81,7 @@ const prompt = (event: E, ...[device, ...args]: DeviceEv if (error || response == null) { cancelAndReject(error); } else { - resolve(response); + resolve({ success: true, payload: response }); } }; @@ -73,7 +91,7 @@ const prompt = (event: E, ...[device, ...args]: DeviceEv device.emit(...emitArgs); } else { - cancelAndReject(ERRORS.TypedError('Runtime', `${event} callback not configured`)); + cancelAndReject(`${event} callback not configured`); } }); export const promptPassphrase = (device: Device) => prompt(DEVICE.PASSPHRASE, device); diff --git a/packages/transport/src/types/apiCall.ts b/packages/transport/src/types/apiCall.ts index 6f337594268..56b126a9574 100644 --- a/packages/transport/src/types/apiCall.ts +++ b/packages/transport/src/types/apiCall.ts @@ -9,7 +9,7 @@ export interface Success { payload: T; } -export type ErrorGeneric = { +type ErrorGeneric = { success: false; error: ErrorType; message?: string;