Skip to content

Commit

Permalink
feat(connect): add core-in-desktop mode
Browse files Browse the repository at this point in the history
  • Loading branch information
mroz22 authored and martykan committed Nov 11, 2024
1 parent 7c7b494 commit 0f6ec45
Show file tree
Hide file tree
Showing 3 changed files with 181 additions and 2 deletions.
172 changes: 172 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,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<ConnectSettingsWeb> {
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<void>(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<void>((_resolve, reject) => {
setTimeout(() => {
reject(new Error('Handshake timeout'));
}, 1000);
}),
]);
}
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 = 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<CallMethodAnyResponse> {
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<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
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

0 comments on commit 0f6ec45

Please sign in to comment.