Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

connect - core in suite-desktop #14690

Merged
merged 11 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/connect-explorer/src/components/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export const Settings = () => {
{ value: 'iframe', label: 'Iframe' },
{ value: 'popup', label: 'Popup' },
...(isBetaOnly ? [{ value: 'deeplink', label: 'Deeplink (mobile)' }] : []),
...(isBetaOnly ? [{ value: 'suite-desktop', label: 'Suite desktop' }] : []),
],
},
{
Expand Down
191 changes: 191 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,191 @@
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,
POPUP,
} 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 { Login } from '@trezor/connect/src/types/api/requestLogin';
import { createDeferred, createDeferredManager, DeferredManager } from '@trezor/utils';

import { parseConnectSettings } from '../connectSettings';

/**
* CoreInSuiteDesktop implementation for TrezorConnect factory.
*/
export class CoreInSuiteDesktop implements ConnectFactoryDependencies<ConnectSettingsWeb> {
public eventEmitter = new EventEmitter();
protected _settings: ConnectSettings;
private ws?: WebSocket;
private readonly messages: DeferredManager<CallMethodAnyResponse>;

public constructor() {
this._settings = parseConnectSettings();
this.messages = createDeferredManager();
}

public manifest(data: Manifest) {
this._settings = parseConnectSettings({
...this._settings,
manifest: data,
});
}

public dispose() {
this.eventEmitter.removeAllListeners();
this._settings = parseConnectSettings();
this.ws?.close();

return Promise.resolve(undefined);
}

public cancel(_error?: string) {}

private async handshake() {
const { promise, promiseId } = this.messages.create(1000);
this.ws?.send(
JSON.stringify({
id: promiseId,
type: POPUP.HANDSHAKE,
}),
);
try {
await promise;
} catch (err) {
console.error(err);
throw new Error('Handshake timed out');
}
}
mroz22 marked this conversation as resolved.
Show resolved Hide resolved

public async 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?.close();
const wsOpen = createDeferred(1000);
this.ws = new WebSocket('ws://localhost:21335/connect-ws');
this.ws.addEventListener('opened', () => {
wsOpen.resolve();
});
this.ws.addEventListener('error', () => {
wsOpen.reject(new Error('WebSocket error'));
this.messages.rejectAll(new Error('WebSocket error'));
});
this.ws.addEventListener('message', (event: WebSocketEventMap['message']) => {
try {
const data = JSON.parse(event.data);
this.messages.resolve(data.id, data);
} catch {
// Some undefined message format
}
});
this.ws.addEventListener('close', () => {
wsOpen.reject(new Error('WebSocket closed'));
this.messages.rejectAll(new Error('WebSocket closed'));
});

// Wait for the connection to be opened
if (this.ws.readyState !== WebSocket.OPEN) {
// There is some glitch that when reconnecting the open event doesn't fire
// So we do this as a workaround
setTimeout(() => {
if (this.ws?.readyState === WebSocket.OPEN) {
wsOpen.resolve();
}
}, 500);
await wsOpen.promise;
}

return await 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<CallMethodAnyResponse> {
try {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
await this.init();
}
await this.handshake();

const { promise, promiseId } = this.messages.create();

this.ws?.send(
JSON.stringify({
id: promiseId,
type: IFRAME.CALL,
mroz22 marked this conversation as resolved.
Show resolved Hide resolved
payload: params,
}),
);

return promise;
} 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<Login> {
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),
});
9 changes: 8 additions & 1 deletion packages/connect-web/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand All @@ -15,7 +16,7 @@ type ConnectWebExtraMethods = {
};

const impl = new TrezorConnectDynamic<
'iframe' | 'core-in-popup',
'iframe' | 'core-in-popup' | 'core-in-suite-desktop',
ConnectSettingsWeb,
ConnectFactoryDependencies<ConnectSettingsWeb> & ConnectWebExtraMethods
>({
Expand All @@ -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<ConnectSettingsPublic & ConnectSettingsWeb>) => {
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}`);
Expand Down
3 changes: 3 additions & 0 deletions packages/connect/src/constants/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ export const serializeError = (payload: any) => {
if (payload && payload.error instanceof Error) {
return { error: payload.error.message, code: payload.error.code };
}
if (payload instanceof TrezorError) {
return { error: payload.message, code: payload.code };
}
mroz22 marked this conversation as resolved.
Show resolved Hide resolved

return payload;
};
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 @@ -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 */
Expand Down
12 changes: 12 additions & 0 deletions packages/suite-desktop-api/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
BridgeSettings,
TorSettings,
TraySettings,
ConnectPopupResponse,
ConnectPopupCall,
} from './messages';

// Event messages from renderer to main process
Expand Down Expand Up @@ -72,6 +74,9 @@ export interface RendererChannels {
'tray/settings': TraySettings;

'handshake/event': HandshakeEvent;

// connect
'connect-popup/call': ConnectPopupCall;
}

// Invocation from renderer process
Expand Down Expand Up @@ -99,6 +104,9 @@ export interface InvokeChannels {
'app/auto-start/is-enabled': () => InvokeResult<boolean>;
'tray/change-settings': (payload: TraySettings) => InvokeResult;
'tray/get-settings': () => InvokeResult<TraySettings>;
'connect-popup/enabled': () => boolean;
'connect-popup/ready': () => void;
'connect-popup/response': (response: ConnectPopupResponse) => void;
}

type DesktopApiListener = ListenerMethod<RendererChannels>;
Expand Down Expand Up @@ -161,4 +169,8 @@ export interface DesktopApi {
// Tray
changeTraySettings: DesktopApiInvoke<'tray/change-settings'>;
getTraySettings: DesktopApiInvoke<'tray/get-settings'>;
// Connect popup
connectPopupEnabled: DesktopApiInvoke<'connect-popup/enabled'>;
connectPopupReady: DesktopApiInvoke<'connect-popup/ready'>;
connectPopupResponse: DesktopApiInvoke<'connect-popup/response'>;
}
5 changes: 5 additions & 0 deletions packages/suite-desktop-api/src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,5 +177,10 @@ export const factory = <R extends StrictIpcRenderer<any, IpcRendererEvent>>(
return Promise.resolve({ success: false, error: 'invalid params' });
},
getTraySettings: () => ipcRenderer.invoke('tray/get-settings'),

// Connect popup
connectPopupEnabled: () => ipcRenderer.invoke('connect-popup/enabled'),
connectPopupReady: () => ipcRenderer.invoke('connect-popup/ready'),
connectPopupResponse: response => ipcRenderer.invoke('connect-popup/response', response),
};
};
14 changes: 14 additions & 0 deletions packages/suite-desktop-api/src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,17 @@ export type InvokeResult<Payload = undefined> =
ExtractUndefined<Payload> extends undefined
? { success: true; payload?: Payload } | { success: false; error: string; code?: string }
: { success: true; payload: Payload } | { success: false; error: string; code?: string };

export type ConnectPopupCall = {
id: number;
method: string;
payload: any;
processName?: string;
origin?: string;
};

export type ConnectPopupResponse = {
id: number;
success: boolean;
payload: any;
};
1 change: 1 addition & 0 deletions packages/suite-desktop-api/src/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,6 @@ const validChannels: Array<keyof RendererChannels> = [
'bridge/status',
'bridge/settings',
'tray/settings',
'connect-popup/call',
];
export const isValidChannel = (channel: any) => validChannels.includes(channel);
4 changes: 3 additions & 1 deletion packages/suite-desktop-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
"electron-store": "8.2.0",
"electron-updater": "6.3.9",
"openpgp": "^5.11.2",
"systeminformation": "^5.23.5"
"systeminformation": "^5.23.5",
"ws": "^8.18.0"
},
"devDependencies": {
"@currents/playwright": "^1.3.1",
Expand All @@ -50,6 +51,7 @@
"@trezor/trezor-user-env-link": "workspace:*",
"@trezor/type-utils": "workspace:*",
"@types/electron-localshortcut": "^3.1.3",
"@types/ws": "^8.5.12",
"electron": "33.1.0",
"fs-extra": "^11.2.0",
"glob": "^10.3.10",
Expand Down
17 changes: 11 additions & 6 deletions packages/suite-desktop-core/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ const init = async () => {
interceptor,
mainThreadEmitter,
});
await loadBackgroundModules(undefined);
const backgroundModulesResponse = await loadBackgroundModules(undefined);

// Daemon mode with no UI
const { wasOpenedAtLogin } = app.getLoginItemSettings();
Expand Down Expand Up @@ -208,8 +208,12 @@ const init = async () => {

app.dock?.show();
if (isMacOs()) app.show();
if (!mainWindow.isVisible()) mainWindow.show();
if (mainWindow.isMinimized()) mainWindow.restore();
//if (!mainWindow.isVisible())
mainWindow.show();
//if (mainWindow.isMinimized())
mainWindow.restore();
app.focus();
mainWindow.moveTop();
mainWindow.focus();
};
app.on('second-instance', reactivateWindow);
Expand All @@ -222,9 +226,9 @@ const init = async () => {
// create handler for handshake/load-modules
const loadModulesResponse = (clientData: HandshakeClient) =>
loadModules(clientData)
.then(payload => ({
.then(modulesResponse => ({
success: true as const,
payload,
payload: { ...modulesResponse, ...backgroundModulesResponse },
}))
.catch(err => ({
success: false as const,
Expand Down Expand Up @@ -264,7 +268,8 @@ const init = async () => {
const mainWindow = mainWindowProxy.getInstance();
const windowExists =
mainWindow && !mainWindow.isDestroyed() && mainWindow.isClosable() && !app.isHidden();
const autoStartCurrentlyEnabled = isAutoStartEnabled();
const autoStartCurrentlyEnabled =
isAutoStartEnabled() || app.commandLine.hasSwitch('bridge-test');
logger.info('main', `Before quit, window exists: ${windowExists}`);
if (
!stoppingDaemon &&
Expand Down
Loading
Loading