Skip to content

Commit

Permalink
wip: core-in-desktop mode
Browse files Browse the repository at this point in the history
  • Loading branch information
mroz22 committed Oct 3, 2024
1 parent 03fd53f commit d9d4f45
Show file tree
Hide file tree
Showing 7 changed files with 225 additions and 7 deletions.
7 changes: 4 additions & 3 deletions packages/connect-explorer/src/actions/trezorConnectActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -156,6 +156,7 @@ export const init =
},
trustedHost: false,
connectSrc: window.__TREZOR_CONNECT_SRC,

...options,
};

Expand Down
131 changes: 131 additions & 0 deletions packages/connect-web/src/impl/core-in-suite-desktop.ts
Original file line number Diff line number Diff line change
@@ -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<ConnectSettingsPublic> = {}): Promise<void> {
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<CallMethodAnyResponse> {
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<Login> {
// 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),
});
17 changes: 15 additions & 2 deletions packages/connect-web/src/index.ts
Original file line number Diff line number Diff line change
@@ -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'];

Expand All @@ -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<ConnectSettings>;

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) {
Expand Down Expand Up @@ -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';
Expand Down
2 changes: 1 addition & 1 deletion packages/connect/src/types/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
/**
Expand Down
20 changes: 20 additions & 0 deletions packages/suite-desktop-core/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
2 changes: 2 additions & 0 deletions packages/suite-desktop-core/src/modules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ interface MainThreadMessages {
'module/request-interceptor': InterceptedEvent;
'module/reset-tor-circuits': Extract<InterceptedEvent, { type: 'CIRCUIT_MISBEHAVING' }>;
'module/tor-status-update': TorStatus;
'focus-window': void;
'blur-window': void;
}
export const mainThreadEmitter = new TypedEmitter<MainThreadMessages>();
export type MainThreadEmitter = typeof mainThreadEmitter;
Expand Down
53 changes: 52 additions & 1 deletion packages/suite-desktop-core/src/modules/trezor-connect.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ipcMain } from 'electron';
import { WebSocketServer } from 'ws';

import TrezorConnect from '@trezor/connect';
import { createIpcProxyHandler, IpcProxyHandlerOptions } from '@trezor/ipc-proxy';
Expand All @@ -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`);

Expand All @@ -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;
}

Expand Down

0 comments on commit d9d4f45

Please sign in to comment.