From 60d634f1d8296e6adf6a3a3790ad2327b99bcac6 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Ortiz karliatto Date: Mon, 21 Oct 2024 15:45:43 +0200 Subject: [PATCH] wip --- .../webpack/base.webpack.config.ts | 8 +- .../webpack/core.webpack.config.ts | 2 +- .../connect-web/src/impl/core-in-module.ts | 299 ++++++++++++++++++ packages/connect-web/src/module/index.ts | 31 ++ packages/connect/src/data/DataManager.ts | 4 +- .../suite-build/configs/web.webpack.config.ts | 5 +- 6 files changed, 341 insertions(+), 8 deletions(-) create mode 100644 packages/connect-web/src/impl/core-in-module.ts create mode 100644 packages/connect-web/src/module/index.ts diff --git a/packages/connect-iframe/webpack/base.webpack.config.ts b/packages/connect-iframe/webpack/base.webpack.config.ts index 1b73e0ea59f3..dac5008096d1 100644 --- a/packages/connect-iframe/webpack/base.webpack.config.ts +++ b/packages/connect-iframe/webpack/base.webpack.config.ts @@ -32,7 +32,7 @@ export const config: webpack.Configuration = { // TODO: we are not using contenthash here because we want to use that worker from // different environments (iframe, popup, connect-web, etc.) and we would not know the // name of the file. - filename: './workers/shared-logger-worker.js', + filename: '/workers/shared-logger-worker.js', }, }, { @@ -47,21 +47,21 @@ export const config: webpack.Configuration = { test: /\workers\/blockbook\/index/i, loader: 'worker-loader', options: { - filename: './workers/blockbook-worker.[contenthash].js', + filename: '/workers/blockbook-worker.[contenthash].js', }, }, { test: /\workers\/ripple\/index/i, loader: 'worker-loader', options: { - filename: './workers/ripple-worker.[contenthash].js', + filename: '/workers/ripple-worker.[contenthash].js', }, }, { test: /\workers\/blockfrost\/index/i, loader: 'worker-loader', options: { - filename: './workers/blockfrost-worker.[contenthash].js', + filename: '/workers/blockfrost-worker.[contenthash].js', }, }, { diff --git a/packages/connect-iframe/webpack/core.webpack.config.ts b/packages/connect-iframe/webpack/core.webpack.config.ts index a3c08cdd86e4..62b4c45d2bdc 100644 --- a/packages/connect-iframe/webpack/core.webpack.config.ts +++ b/packages/connect-iframe/webpack/core.webpack.config.ts @@ -15,7 +15,7 @@ export const config: webpack.Configuration = { output: { filename: 'js/[name].js', path: DIST, - publicPath: './', + publicPath: './static/connect', library: { type: 'module', }, diff --git a/packages/connect-web/src/impl/core-in-module.ts b/packages/connect-web/src/impl/core-in-module.ts new file mode 100644 index 000000000000..6a83db827de6 --- /dev/null +++ b/packages/connect-web/src/impl/core-in-module.ts @@ -0,0 +1,299 @@ +import EventEmitter from 'events'; + +// NOTE: @trezor/connect part is intentionally not imported from the index due to NormalReplacementPlugin +// in packages/suite-build/configs/web.webpack.config.ts +import * as ERRORS from '@trezor/connect/src/constants/errors'; +import { + POPUP, + IFRAME, + UI, + UI_EVENT, + DEVICE_EVENT, + RESPONSE_EVENT, + TRANSPORT_EVENT, + BLOCKCHAIN_EVENT, + createErrorMessage, + UiResponseEvent, + CoreEventMessage, + CallMethodPayload, + CORE_EVENT, +} from '@trezor/connect/src/events'; +import type { ConnectSettings, DeviceIdentity, Manifest } from '@trezor/connect/src/types'; +import { ConnectFactoryDependencies, factory } from '@trezor/connect/src/factory'; +import { Log, initLog } from '@trezor/connect/src/utils/debug'; +import { DeferredManager, createDeferredManager } from '@trezor/utils/src/createDeferredManager'; + +import webUSBButton from '../webusb/button'; +import { parseConnectSettings } from '../connectSettings'; + +export class CoreInModule implements ConnectFactoryDependencies { + public eventEmitter = new EventEmitter(); + protected _settings: ConnectSettings; + + private _log: Log; + private _coreManager?: any; + private _messagePromises: DeferredManager<{ + id: number; + success: boolean; + payload: any; + device?: DeviceIdentity; + }>; + + private readonly boundOnCoreEvent = this.onCoreEvent.bind(this); + + public constructor() { + this._settings = parseConnectSettings(); + this._log = initLog('@trezor/connect-web'); + this._messagePromises = createDeferredManager({ initialId: 1 }); + } + + private async initCoreManager() { + const connectSrc = this._settings.connectSrc; + const { initCoreState, initTransport } = await import( + /* webpackIgnore: true */ `${connectSrc}js/core.js` + ).catch(_err => { + this._log.error('_err', _err); + }); + + if (!initCoreState) return; + + if (initTransport) { + this._log.debug('initiating transport with settings: ', this._settings); + await initTransport(this._settings); + } + + this._coreManager = initCoreState(); + return this._coreManager; + } + + public manifest(data: Manifest) { + this._settings = parseConnectSettings({ + ...this._settings, + manifest: data, + }); + } + + public dispose() { + this.eventEmitter.removeAllListeners(); + this._settings = parseConnectSettings(); + if (this._coreManager) { + this._coreManager.dispose(); + } + + return Promise.resolve(undefined); + } + + public cancel(error?: string) { + if (this._coreManager) { + const core = this._coreManager.get(); + if (!core) { + throw ERRORS.TypedError('Runtime', 'postMessage: _core not found'); + } + + core.handleMessage({ + type: POPUP.CLOSED, + payload: error ? { error } : null, + }); + } + } + + // handle message received from Core + private onCoreEvent(message: CoreEventMessage) { + const { event, type, payload } = message; + + if (type === UI.REQUEST_UI_WINDOW) { + this._coreManager.get()?.handleMessage({ type: POPUP.HANDSHAKE }); + + return; + } + + if (type === POPUP.CANCEL_POPUP_REQUEST) return; + + switch (event) { + case RESPONSE_EVENT: { + const { id = 0, success, device } = message; + const resolved = this._messagePromises.resolve(id, { + id, + success, + payload, + device, + }); + if (!resolved) this._log.warn(`Unknown message id ${id}`); + break; + } + case DEVICE_EVENT: + // pass DEVICE event up to html + this.eventEmitter.emit(event, message); + this.eventEmitter.emit(type, payload); // DEVICE_EVENT also emit single events (connect/disconnect...) + break; + + case TRANSPORT_EVENT: + this.eventEmitter.emit(event, message); + this.eventEmitter.emit(type, payload); + break; + + case BLOCKCHAIN_EVENT: + this.eventEmitter.emit(event, message); + this.eventEmitter.emit(type, payload); + break; + + case UI_EVENT: + // pass UI event up + this.eventEmitter.emit(event, message); + this.eventEmitter.emit(type, payload); + break; + + default: + this._log.warn('Undefined message', event, message); + } + } + + public async init(settings: Partial = {}) { + if (this._coreManager && (this._coreManager.get() || this._coreManager.getPending())) { + throw ERRORS.TypedError('Init_AlreadyInitialized'); + } + + this._settings = parseConnectSettings({ ...this._settings, ...settings }); + + if (!this._settings.manifest) { + throw ERRORS.TypedError('Init_ManifestMissing'); + } + this._settings.lazyLoad = true; + + // defaults for connect-web + if (!this._settings.transports?.length) { + this._settings.transports = ['BridgeTransport', 'WebUsbTransport']; + } + + if (!this._coreManager) { + this._coreManager = await this.initCoreManager(); + await this._coreManager.getOrInit(this._settings, this.boundOnCoreEvent); + } + + this._log.enabled = !!this._settings.debug; + } + + initSettings = (settings: Partial = {}) => { + this._settings = parseConnectSettings({ ...this._settings, ...settings, popup: false }); + + if (!this._settings.manifest) { + throw ERRORS.TypedError('Init_ManifestMissing'); + } + + if (!this._settings.transports?.length) { + // default fallback for node + this._settings.transports = ['BridgeTransport']; + } + }; + + public async initCore() { + this.initSettings({ lazyLoad: false }); + + return this._coreManager.getOrInit(this._settings, this.boundOnCoreEvent); + } + + public async call(params: CallMethodPayload) { + let core; + try { + core = + this._coreManager.get() ?? + (await this._coreManager.getPending()) ?? + (await this.initCore()); + } catch (error) { + return createErrorMessage(error); + } + + try { + const { promiseId, promise } = this._messagePromises.create(); + core.handleMessage({ + type: IFRAME.CALL, + payload: params, + id: promiseId, + }); + const response = await promise; + + return response ?? createErrorMessage(ERRORS.TypedError('Method_NoResponse')); + } catch (error) { + this._log.error('call', error); + + return createErrorMessage(error); + } + } + + public uiResponse(response: UiResponseEvent) { + const core = this._coreManager.get(); + if (!core) { + throw ERRORS.TypedError('Init_NotInitialized'); + } + core.handleMessage(response); + } + + public renderWebUSBButton(className?: string) { + webUSBButton(className, this._settings.webusbSrc); + } + + public async requestLogin(params: any) { + if (typeof params.callback === 'function') { + const { callback } = params; + const core = this._coreManager.get(); + + // TODO: set message listener only if _core is loaded correctly + const loginChallengeListener = async (event: MessageEvent) => { + const { data } = event; + if (data && data.type === UI.LOGIN_CHALLENGE_REQUEST) { + try { + const payload = await callback(); + core?.handleMessage({ + type: UI.LOGIN_CHALLENGE_RESPONSE, + payload, + }); + } catch (error) { + core?.handleMessage({ + type: UI.LOGIN_CHALLENGE_RESPONSE, + payload: error.message, + }); + } + } + }; + + core?.on(CORE_EVENT, loginChallengeListener); + const response = await this.call({ + method: 'requestLogin', + ...params, + asyncChallenge: true, + callback: null, + }); + core?.removeListener(CORE_EVENT, loginChallengeListener); + + return response; + } + + return this.call({ method: 'requestLogin', ...params }); + } + + public disableWebUSB() { + throw ERRORS.TypedError('Method_InvalidPackage'); + } + + public async requestWebUSBDevice() { + throw ERRORS.TypedError('Method_InvalidPackage'); + } +} + +const impl = new CoreInModule(); + +// 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/module/index.ts b/packages/connect-web/src/module/index.ts new file mode 100644 index 000000000000..fdb864482b97 --- /dev/null +++ b/packages/connect-web/src/module/index.ts @@ -0,0 +1,31 @@ +import { factory } from '@trezor/connect/src/factory'; +import { TrezorConnectDynamic } from '@trezor/connect/src/impl/dynamic'; +import { CoreInModule } from '../impl/core-in-module'; + +const impl = new TrezorConnectDynamic<'core-in-module'>({ + implementations: [ + { + type: 'core-in-module', + impl: new CoreInModule(), + }, + ], + getInitTarget: () => 'core-in-module', + handleErrorFallback: async () => false, +}); + +const TrezorConnect = factory({ + 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), +}); + +export default TrezorConnect; +export * from '@trezor/connect/src/exports'; diff --git a/packages/connect/src/data/DataManager.ts b/packages/connect/src/data/DataManager.ts index cc17c9ecf1e3..2dc65ec3487b 100644 --- a/packages/connect/src/data/DataManager.ts +++ b/packages/connect/src/data/DataManager.ts @@ -23,12 +23,12 @@ export class DataManager { if (!withAssets) return; const assetPromises = config.assets.map(async asset => { - const json = await httpRequest(`${asset.url}${ts}`, 'json'); + const json = await httpRequest(`${settings.connectSrc}${asset.url}${ts}`, 'json'); this.assets[asset.name] = json; }); await Promise.all(assetPromises); - this.messages = await httpRequest(`${config.messages}${ts}`, 'json'); + this.messages = await httpRequest(`${settings.connectSrc}${config.messages}${ts}`, 'json'); // parse bridge JSON parseBridgeJSON(this.assets.bridge); diff --git a/packages/suite-build/configs/web.webpack.config.ts b/packages/suite-build/configs/web.webpack.config.ts index 316d05652fc4..1c04f2cec0d4 100644 --- a/packages/suite-build/configs/web.webpack.config.ts +++ b/packages/suite-build/configs/web.webpack.config.ts @@ -73,7 +73,10 @@ const config: webpack.Configuration = { }), ), // imports from @trezor/connect in @trezor/suite package need to be replaced by imports from @trezor/connect-web - new webpack.NormalModuleReplacementPlugin(/@trezor\/connect$/, '@trezor/connect-web'), + new webpack.NormalModuleReplacementPlugin( + /@trezor\/connect$/, + '@trezor/connect-web/src/module', + ), ...(!isDev ? [new CssMinimizerPlugin()] : []), ], };