From d9d4f45c9233dbed564250a5aafc14084fa96eeb Mon Sep 17 00:00:00 2001 From: Martin Varmuza Date: Thu, 3 Oct 2024 07:23:44 +0200 Subject: [PATCH] wip: core-in-desktop mode --- .../src/actions/trezorConnectActions.ts | 7 +- .../src/impl/core-in-suite-desktop.ts | 131 ++++++++++++++++++ packages/connect-web/src/index.ts | 17 ++- packages/connect/src/types/settings.ts | 2 +- packages/suite-desktop-core/src/app.ts | 20 +++ .../suite-desktop-core/src/modules/index.ts | 2 + .../src/modules/trezor-connect.ts | 53 ++++++- 7 files changed, 225 insertions(+), 7 deletions(-) create mode 100644 packages/connect-web/src/impl/core-in-suite-desktop.ts diff --git a/packages/connect-explorer/src/actions/trezorConnectActions.ts b/packages/connect-explorer/src/actions/trezorConnectActions.ts index d56caf9a9522..719582c114e8 100644 --- a/packages/connect-explorer/src/actions/trezorConnectActions.ts +++ b/packages/connect-explorer/src/actions/trezorConnectActions.ts @@ -141,9 +141,9 @@ export const init = } // Get default coreMode from URL params (?core-mode=auto) - const urlParams = new URLSearchParams(window.location.search); - const coreMode = (urlParams.get('core-mode') as ConnectOptions['coreMode']) || 'auto'; - + // const urlParams = new URLSearchParams(window.location.search); + // const coreMode = (urlParams.get('core-mode') as ConnectOptions['coreMode']) || 'auto'; + const coreMode: ConnectOptions['coreMode'] = 'suite-desktop'; const connectOptions = { coreMode, transportReconnect: true, @@ -156,6 +156,7 @@ export const init = }, trustedHost: false, connectSrc: window.__TREZOR_CONNECT_SRC, + ...options, }; 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 000000000000..2d825d0e0cf9 --- /dev/null +++ b/packages/connect-web/src/impl/core-in-suite-desktop.ts @@ -0,0 +1,131 @@ +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, + 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'; +// import { createDeferred } from '@trezor/utils'; + +/** + * 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) {} + + public 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); + this.ws.addEventListener('open', function open() {}); + + return Promise.resolve(); + } + + /** + * 1. opens popup + * 2. sends request to popup where the request is handled by core + * 3. returns response + */ + public async call(params: CallMethodPayload): Promise { + this.ws?.send( + JSON.stringify({ + type: IFRAME.CALL, + payload: params, + }), + ); + + return new Promise(resolve => { + this.ws?.addEventListener('message', function message(event) { + console.log('received: %s', event.data); + resolve(JSON.parse(event.data)); + }); + }); + } + + uiResponse(_response: UiResponseEvent) { + // this shouldn't be needed, ui response should be handled in suite-desktop + throw ERRORS.TypedError('Method_InvalidPackage'); + } + + renderWebUSBButton() {} + + requestLogin(): Response { + // todo: not supported yet + throw ERRORS.TypedError('Method_InvalidPackage'); + } + + disableWebUSB() { + // todo: not supported yet, probably not needed + throw ERRORS.TypedError('Method_InvalidPackage'); + } + + requestWebUSBDevice() { + // not needed - webusb pairing happens in popup + throw ERRORS.TypedError('Method_InvalidPackage'); + } +} + +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), + renderWebUSBButton: impl.renderWebUSBButton.bind(impl), + disableWebUSB: impl.disableWebUSB.bind(impl), + requestWebUSBDevice: impl.requestWebUSBDevice.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 7ead4ff22a56..24b53de5cc14 100644 --- a/packages/connect-web/src/index.ts +++ b/packages/connect-web/src/index.ts @@ -1,13 +1,14 @@ import { ConnectFactoryDependencies, factory } from '@trezor/connect/src/factory'; import { CoreInIframe } from './impl/core-in-iframe'; import { CoreInPopup } from './impl/core-in-popup'; +import { CoreInSuiteDesktop } from './impl/core-in-suite-desktop'; import ProxyEventEmitter from './utils/proxy-event-emitter'; import type { ConnectSettings, ConnectSettingsPublic, Manifest } from '@trezor/connect/src/types'; import EventEmitter from 'events'; import { CallMethodPayload } from '@trezor/connect/src/events'; import { getEnv } from './connectSettings'; -type TrezorConnectType = 'core-in-popup' | 'iframe'; +type TrezorConnectType = 'core-in-popup' | 'iframe' | 'core-in-suite-desktop'; const IFRAME_ERRORS = ['Init_IframeBlocked', 'Init_IframeTimeout', 'Transport_Missing']; @@ -20,20 +21,30 @@ export class TrezorConnectDynamicImpl implements ConnectFactoryDependencies { private currentTarget: TrezorConnectType = 'iframe'; private coreInIframeImpl: CoreInIframe; private coreInPopupImpl: CoreInPopup; + private coreInSuiteDesktopImpl: CoreInSuiteDesktop; private lastSettings?: Partial; public constructor() { this.coreInIframeImpl = new CoreInIframe(); this.coreInPopupImpl = new CoreInPopup(); + this.coreInSuiteDesktopImpl = new CoreInSuiteDesktop(); this.eventEmitter = new ProxyEventEmitter([ this.coreInIframeImpl.eventEmitter, this.coreInPopupImpl.eventEmitter, + this.coreInSuiteDesktopImpl.eventEmitter, ]); } private getTarget() { - return this.currentTarget === 'iframe' ? this.coreInIframeImpl : this.coreInPopupImpl; + switch (this.currentTarget) { + case 'iframe': + return this.coreInIframeImpl; + case 'core-in-popup': + return this.coreInPopupImpl; + case 'core-in-suite-desktop': + return this.coreInSuiteDesktopImpl; + } } private async switchTarget(target: TrezorConnectType) { @@ -61,6 +72,8 @@ export class TrezorConnectDynamicImpl implements ConnectFactoryDependencies { this.currentTarget = 'iframe'; } else if (settings.coreMode === 'popup') { this.currentTarget = 'core-in-popup'; + } else if (settings.coreMode === 'suite-desktop') { + this.currentTarget = 'core-in-suite-desktop'; } else { // Default to auto mode with iframe as the first choice settings.coreMode = 'auto'; diff --git a/packages/connect/src/types/settings.ts b/packages/connect/src/types/settings.ts index 842b2fdbe66a..047d7ee980af 100644 --- a/packages/connect/src/types/settings.ts +++ b/packages/connect/src/types/settings.ts @@ -23,7 +23,7 @@ export interface ConnectSettingsPublic { lazyLoad?: boolean; interactionTimeout?: number; trustedHost: boolean; - coreMode?: 'auto' | 'popup' | 'iframe'; + coreMode?: 'auto' | 'popup' | 'iframe' | 'suite-desktop'; /* _extendWebextensionLifetime features makes the service worker in @trezor/connect-webextension stay alive longer */ _extendWebextensionLifetime?: boolean; /** diff --git a/packages/suite-desktop-core/src/app.ts b/packages/suite-desktop-core/src/app.ts index 49009a7d19ca..60d7a1a18bd3 100644 --- a/packages/suite-desktop-core/src/app.ts +++ b/packages/suite-desktop-core/src/app.ts @@ -194,6 +194,26 @@ const initUi = async ({ mainThreadEmitter, }); + mainThreadEmitter.on('focus-window', () => { + console.log('====focus===='); + let mainWindow = mainWindowProxy.getInstance(); + if (!mainWindow || mainWindow.isDestroyed()) { + logger.info('main', 'Main window destroyed, recreating'); + mainWindow = createMainWindow(winBounds); + mainWindowProxy.setInstance(mainWindow); + } + + app.dock?.show(); + if (isMacOs()) app.show(); + if (!mainWindow.isVisible()) mainWindow.show(); + if (mainWindow.isMinimized()) mainWindow.restore(); + mainWindow.focus(); + }); + + mainThreadEmitter.on('blur-window', () => { + app.hide(); + }); + app.on('second-instance', () => { // Someone tried to run a second instance, we should focus our window. logger.info('main', 'Second instance detected, focusing main window'); diff --git a/packages/suite-desktop-core/src/modules/index.ts b/packages/suite-desktop-core/src/modules/index.ts index 3661caaf4c9c..4ee132020f2c 100644 --- a/packages/suite-desktop-core/src/modules/index.ts +++ b/packages/suite-desktop-core/src/modules/index.ts @@ -70,6 +70,8 @@ interface MainThreadMessages { 'module/request-interceptor': InterceptedEvent; 'module/reset-tor-circuits': Extract; 'module/tor-status-update': TorStatus; + 'focus-window': void; + 'blur-window': void; } export const mainThreadEmitter = new TypedEmitter(); export type MainThreadEmitter = typeof mainThreadEmitter; diff --git a/packages/suite-desktop-core/src/modules/trezor-connect.ts b/packages/suite-desktop-core/src/modules/trezor-connect.ts index 960eb53b6eae..e1808226fa84 100644 --- a/packages/suite-desktop-core/src/modules/trezor-connect.ts +++ b/packages/suite-desktop-core/src/modules/trezor-connect.ts @@ -1,4 +1,5 @@ import { ipcMain } from 'electron'; +import { WebSocketServer } from 'ws'; import TrezorConnect from '@trezor/connect'; import { createIpcProxyHandler, IpcProxyHandlerOptions } from '@trezor/ipc-proxy'; @@ -7,7 +8,7 @@ import type { Module } from './index'; export const SERVICE_NAME = '@trezor/connect'; -export const init: Module = ({ store }) => { +export const init: Module = ({ store, mainThreadEmitter }) => { const { logger } = global; logger.info(SERVICE_NAME, `Starting service`); @@ -25,9 +26,59 @@ export const init: Module = ({ store }) => { onRequest: async (method, params) => { logger.debug(SERVICE_NAME, `call ${method}`); if (method === 'init') { + console.log('aprams, params', params); const response = await TrezorConnect[method](...params); await setProxy(true); + const wss = new WebSocketServer({ + port: 8090, + perMessageDeflate: { + zlibDeflateOptions: { + // See zlib defaults. + chunkSize: 1024, + memLevel: 7, + level: 3, + }, + zlibInflateOptions: { + chunkSize: 10 * 1024, + }, + // Other options settable: + clientNoContextTakeover: true, // Defaults to negotiated value. + serverNoContextTakeover: true, // Defaults to negotiated value. + serverMaxWindowBits: 10, // Defaults to negotiated value. + // Below options specified as default values. + concurrencyLimit: 10, // Limits zlib concurrency for perf. + threshold: 1024, // Size (in bytes) below which messages + // should not be compressed if context takeover is disabled. + }, + }); + + wss.on('connection', function connection(ws) { + ws.on('error', console.error); + + ws.on('message', async function message(data) { + try { + const parsed = JSON.parse(data.toString()); + const { method, ...rest } = parsed.payload; + console.log('method', method); + console.log('rest', rest); + // focus renderer window + mainThreadEmitter.emit('focus-window'); + // @ts-expect-error + const response = await TrezorConnect[method](rest); + console.log('response', response); + ws.send(JSON.stringify(response)); + } catch (err) { + console.log('=== err', err); + } finally { + // blur renderer window + mainThreadEmitter.emit('blur-window'); + } + }); + + ws.send('ack'); + }); + return response; }