From 0f6ec45600d4f103441b7b4f238f9e6017dda89a Mon Sep 17 00:00:00 2001 From: Martin Varmuza Date: Tue, 29 Oct 2024 08:57:34 +0100 Subject: [PATCH] feat(connect): add core-in-desktop mode --- .../src/impl/core-in-suite-desktop.ts | 172 ++++++++++++++++++ packages/connect-web/src/index.ts | 9 +- packages/connect/src/types/settings.ts | 2 +- 3 files changed, 181 insertions(+), 2 deletions(-) create mode 100644 packages/connect-web/src/impl/core-in-suite-desktop.ts diff --git a/packages/connect-web/src/impl/core-in-suite-desktop.ts b/packages/connect-web/src/impl/core-in-suite-desktop.ts new file mode 100644 index 00000000000..fc967ebd0b5 --- /dev/null +++ b/packages/connect-web/src/impl/core-in-suite-desktop.ts @@ -0,0 +1,172 @@ +import EventEmitter from 'events'; + +// NOTE: @trezor/connect part is intentionally not imported from the index so we do include the whole library. +import { + IFRAME, + UiResponseEvent, + CallMethodPayload, + CallMethodAnyResponse, +} from '@trezor/connect/src/events'; +import * as ERRORS from '@trezor/connect/src/constants/errors'; +import type { + ConnectSettings, + ConnectSettingsPublic, + ConnectSettingsWeb, + Manifest, + Response, +} from '@trezor/connect/src/types'; +import { ConnectFactoryDependencies, factory } from '@trezor/connect/src/factory'; + +import { parseConnectSettings } from '../connectSettings'; +import { Login } from '@trezor/connect/src/types/api/requestLogin'; + +/** + * Base class for CoreInPopup methods for TrezorConnect factory. + * This implementation is directly used here in connect-web, but it is also extended in connect-webextension. + */ +export class CoreInSuiteDesktop implements ConnectFactoryDependencies { + public eventEmitter = new EventEmitter(); + protected _settings: ConnectSettings; + private ws?: WebSocket; + + public constructor() { + this._settings = parseConnectSettings(); + } + + public manifest(data: Manifest) { + this._settings = parseConnectSettings({ + ...this._settings, + manifest: data, + }); + } + + public dispose() { + this.eventEmitter.removeAllListeners(); + this._settings = parseConnectSettings(); + + return Promise.resolve(undefined); + } + + public cancel(_error?: string) {} + + private async handshake() { + return Promise.race([ + new Promise(resolve => { + const listener = (event: WebSocketEventMap['message']) => { + if (event.data === 'handshake') { + resolve(); + removeEventListener('message', listener); + } + }; + this.ws?.addEventListener('message', listener); + this.ws?.send('handshake'); + }), + new Promise((_resolve, reject) => { + setTimeout(() => { + reject(new Error('Handshake timeout')); + }, 1000); + }), + ]); + } + public async init(settings: Partial = {}): Promise { + const newSettings = parseConnectSettings({ + ...this._settings, + ...settings, + }); + + // defaults + if (!newSettings.transports?.length) { + newSettings.transports = ['BridgeTransport', 'WebUsbTransport']; + } + this._settings = newSettings; + + this.ws = new WebSocket('ws://localhost:8090'); + this.ws.addEventListener('error', console.error); + + await new Promise(resolve => { + this.ws?.addEventListener('opened', resolve); + }); + return this.handshake(); + } + + /** + * 1. opens popup + * 2. sends request to popup where the request is handled by core + * 3. returns response + */ + public async call(params: CallMethodPayload): Promise { + try { + await this.handshake(); + + this.ws?.send( + JSON.stringify({ + type: IFRAME.CALL, + payload: params, + }), + ); + + return new Promise(resolve => { + const listener = (event: WebSocketEventMap['message']) => { + try { + resolve(JSON.parse(event.data)); + } catch (err) { + resolve({ + success: false, + payload: { + error: err.message, + }, + }); + } + + this.ws?.removeEventListener('message', listener); + }; + + this.ws?.addEventListener('message', listener); + }); + } catch (err) { + return { + success: false, + payload: { + error: err.message, + }, + }; + } + } + // this shouldn't be needed, ui response should be handled in suite-desktop + uiResponse(_response: UiResponseEvent) { + throw ERRORS.TypedError('Method_InvalidPackage'); + } + + // todo: not supported yet + requestLogin(): Response { + throw ERRORS.TypedError('Method_InvalidPackage'); + } + + // todo: not needed, only because of types + disableWebUSB() { + throw ERRORS.TypedError('Method_InvalidPackage'); + } + + // todo: not needed, only because of types + requestWebUSBDevice() { + throw ERRORS.TypedError('Method_InvalidPackage'); + } + + // todo: not needed, only because of types + renderWebUSBButton() {} +} + +const impl = new CoreInSuiteDesktop(); + +// Exported to enable using directly +export const TrezorConnect = factory({ + // Bind all methods due to shadowing `this` + eventEmitter: impl.eventEmitter, + init: impl.init.bind(impl), + call: impl.call.bind(impl), + manifest: impl.manifest.bind(impl), + requestLogin: impl.requestLogin.bind(impl), + uiResponse: impl.uiResponse.bind(impl), + cancel: impl.cancel.bind(impl), + dispose: impl.dispose.bind(impl), +}); diff --git a/packages/connect-web/src/index.ts b/packages/connect-web/src/index.ts index 052bf1c0994..d251a5cf89f 100644 --- a/packages/connect-web/src/index.ts +++ b/packages/connect-web/src/index.ts @@ -4,6 +4,7 @@ import type { ConnectSettingsPublic, ConnectSettingsWeb } from '@trezor/connect' import { CoreInIframe } from './impl/core-in-iframe'; import { CoreInPopup } from './impl/core-in-popup'; +import { CoreInSuiteDesktop } from './impl/core-in-suite-desktop'; import { getEnv } from './connectSettings'; const IFRAME_ERRORS = ['Init_IframeBlocked', 'Init_IframeTimeout', 'Transport_Missing']; @@ -15,7 +16,7 @@ type ConnectWebExtraMethods = { }; const impl = new TrezorConnectDynamic< - 'iframe' | 'core-in-popup', + 'iframe' | 'core-in-popup' | 'core-in-suite-desktop', ConnectSettingsWeb, ConnectFactoryDependencies & ConnectWebExtraMethods >({ @@ -28,12 +29,18 @@ const impl = new TrezorConnectDynamic< type: 'core-in-popup', impl: new CoreInPopup(), }, + { + type: 'core-in-suite-desktop', + impl: new CoreInSuiteDesktop(), + }, ], getInitTarget: (settings: Partial) => { if (settings.coreMode === 'iframe') { return 'iframe'; } else if (settings.coreMode === 'popup') { return 'core-in-popup'; + } else if (settings.coreMode === 'suite-desktop') { + return 'core-in-suite-desktop'; } else { if (settings.coreMode && settings.coreMode !== 'auto') { console.warn(`Invalid coreMode: ${settings.coreMode}`); diff --git a/packages/connect/src/types/settings.ts b/packages/connect/src/types/settings.ts index 867cc44f6a9..6d8f674430c 100644 --- a/packages/connect/src/types/settings.ts +++ b/packages/connect/src/types/settings.ts @@ -51,7 +51,7 @@ export interface ConnectSettingsInternal { export interface ConnectSettingsWeb { hostLabel?: string; hostIcon?: string; - coreMode?: 'auto' | 'popup' | 'iframe' | 'deeplink'; + coreMode?: 'auto' | 'popup' | 'iframe' | 'deeplink' | 'suite-desktop'; } export interface ConnectSettingsWebextension { /** _extendWebextensionLifetime features makes the service worker in @trezor/connect-webextension stay alive longer */