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

feat: Bluetooth components for onboarding #15514

Closed
wants to merge 31 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
26c8476
feat: create `@trezor/websocket-client` package
szymonlesisz Dec 5, 2024
1fefa31
chore(blockchain-link): use `@trezor/websocket-client` package
szymonlesisz Dec 5, 2024
fc94782
chore(trezor-user-env-link): use `@trezor/websocket-client` package
szymonlesisz Dec 5, 2024
b5d5453
chore: eslint ignore `.cache`
szymonlesisz Dec 6, 2024
8e51017
fix(blockchain-link): CustomError with message passed as code
szymonlesisz Dec 9, 2024
f30fab9
feat(connect): store devices per transport
marekrjpolak Oct 24, 2024
9e071cf
refactor(connect): unify setTransports and init
marekrjpolak Nov 1, 2024
19e3042
refactor(connect): DeviceList init rewritten into abortable tasks
marekrjpolak Nov 8, 2024
f87a3a1
refactor(connect): reuse existing transports
marekrjpolak Nov 8, 2024
819a339
refactor(connect): improved handling of transport error
marekrjpolak Nov 15, 2024
21acb41
refactor(connect): better locking in device list
marekrjpolak Nov 15, 2024
8f36c56
refactor(connect): emit non-serialized devices from device list
marekrjpolak Nov 21, 2024
ef09b54
refactor(connect): transport-specific wait for devices
marekrjpolak Nov 21, 2024
9215012
test(connect): unit test adjustment
marekrjpolak Nov 8, 2024
5067d20
test(connect): WIP
marekrjpolak Nov 29, 2024
6c86b66
tmp: multi transport fixes
szymonlesisz Dec 12, 2024
cc1877e
feat: create `@trezor/transport-bluetooth` package
szymonlesisz Feb 6, 2024
f7fbf06
feat(transport-bluetooth): add server source
szymonlesisz Feb 7, 2024
e4dda62
feat(transport-bluetooth): add dev mode UI
szymonlesisz Feb 20, 2024
52e1505
feat(transport-bluetooth): add docker files
szymonlesisz Feb 8, 2024
3c0803e
feat(connect): add `bluetoothProps` to Device
szymonlesisz Dec 4, 2024
5af1247
TMP: inherit process output
szymonlesisz Feb 8, 2024
402e094
feat(suite-desktop): add bluetooth module
szymonlesisz Feb 7, 2024
147fb59
feat(suite-desktop): add bluetooth requests via desktopApi
szymonlesisz Feb 8, 2024
8163684
transport-bluetooth in suite
szymonlesisz Dec 3, 2024
5d60596
WIP: protobuf EraseBonds
szymonlesisz Feb 12, 2024
f1a976e
feat(suite): add EraseBonds to debug settings
szymonlesisz Feb 13, 2024
1b66955
transport-bluetooth with websocket package
szymonlesisz Dec 5, 2024
68e7a78
latest binaries
szymonlesisz Dec 5, 2024
70ecebc
WIP: design
peter-sanderson Nov 27, 2024
e4c7f42
temp: add mock Bluetooth Calls
peter-sanderson Dec 4, 2024
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
28 changes: 17 additions & 11 deletions packages/blockchain-link-types/src/constants/errors.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const PREFIX = 'blockchain_link/';

const ERROR: { [key: string]: string | undefined } = {
connect: undefined,
worker_not_found: 'Worker not found',
worker_invalid: 'Invalid worker object',
worker_timeout: 'Worker timeout',
Expand All @@ -19,23 +20,28 @@ export class CustomError extends Error {

message = '';

constructor(code: string, message = '') {
constructor(codeOrMessage: string, message = '') {
// test reports that super is not covered, TODO: investigate more
super(message);

this.message = message;

if (typeof code === 'string') {
const c =
code.indexOf(PREFIX) === 0 ? code.substring(PREFIX.length, code.length) : code;
this.code = `${PREFIX}${c}`;
const msg = ERROR[c];
if (typeof msg === 'string') {
if (this.message === '') {
this.message = msg;
} else if (message.indexOf('+') === 0) {
this.message = `${msg} ${message.substring(1)}`;
if (typeof codeOrMessage === 'string') {
const isPrefixed = codeOrMessage.indexOf(PREFIX) === 0;
const code = isPrefixed ? codeOrMessage.substring(PREFIX.length) : codeOrMessage;
const knownCode = Object.keys(ERROR).includes(code);
if (isPrefixed || knownCode) {
this.code = `${PREFIX}${code}`;
const codeMessage = ERROR[code];
if (codeMessage) {
if (this.message === '') {
this.message = codeMessage;
} else if (message.indexOf('+') === 0) {
this.message = `${codeMessage} ${message.substring(1)}`;
}
}
} else if (this.message === '') {
this.message = code;
}
}

Expand Down
1 change: 0 additions & 1 deletion packages/blockchain-link/jest.config.unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ module.exports = {
...testPathIgnorePatterns,
'src/types',
'src/ui',
'src/utils/ws.ts',
'fixtures',
'unit/worker/index.ts',
],
Expand Down
12 changes: 4 additions & 8 deletions packages/blockchain-link/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,25 +26,21 @@
],
"main": "src/index.ts",
"browser": {
"socks-proxy-agent": "./src/utils/socks-proxy-agent.ts",
"ws": "./src/utils/ws.ts"
"socks-proxy-agent": "./src/utils/socks-proxy-agent.ts"
},
"react-native": {
"__comment__": "Hotfix for issue where RN metro bundler resolve relatives paths wrong",
"ws": "@trezor/blockchain-link/src/utils/ws-native.ts",
"socks-proxy-agent": "@trezor/blockchain-link/src/utils/socks-proxy-agent.ts"
},
"publishConfig": {
"main": "./lib/index.js",
"types": "lib/index.d.ts",
"typings": "lib/index.d.ts",
"browser": {
"socks-proxy-agent": "./lib/utils/socks-proxy-agent.js",
"ws": "./lib/utils/ws.js"
"socks-proxy-agent": "./lib/utils/socks-proxy-agent.js"
},
"react-native": {
"__comment__": "Hotfix for issue where RN metro bundler resolve relatives paths wrong",
"ws": "@trezor/blockchain-link/lib/utils/ws-native.js",
"socks-proxy-agent": "@trezor/blockchain-link/lib/utils/socks-proxy-agent.js"
}
},
Expand Down Expand Up @@ -83,11 +79,11 @@
"@trezor/env-utils": "workspace:*",
"@trezor/utils": "workspace:*",
"@trezor/utxo-lib": "workspace:*",
"@trezor/websocket-client": "workspace:*",
"@types/web": "^0.0.174",
"events": "^3.3.0",
"ripple-lib": "^1.10.1",
"socks-proxy-agent": "8.0.4",
"ws": "^8.18.0"
"socks-proxy-agent": "8.0.4"
},
"peerDependencies": {
"tslib": "^2.6.2"
Expand Down
203 changes: 14 additions & 189 deletions packages/blockchain-link/src/workers/baseWebsocket.ts
Original file line number Diff line number Diff line change
@@ -1,115 +1,27 @@
import WebSocket from 'ws';

import { createDeferred, createDeferredManager, TypedEmitter } from '@trezor/utils';
import { CustomError } from '@trezor/blockchain-link-types/src/constants/errors';
import { TimerId } from '@trezor/type-utils';
import { WebsocketClient, WebsocketRequest, WebsocketResponse } from '@trezor/websocket-client';

interface Subscription<T> {
id: string;
type: T;
callback: (result: any) => void;
}

interface Options {
url: string;
timeout?: number;
pingTimeout?: number;
connectionTimeout?: number;
keepAlive?: boolean;
agent?: WebSocket.ClientOptions['agent'];
headers?: WebSocket.ClientOptions['headers'];
onSending?: (message: Record<string, any>) => void;
}

const DEFAULT_TIMEOUT = 20 * 1000;
const DEFAULT_PING_TIMEOUT = 50 * 1000;

type EventMap = { [event: string]: any };

type WsEvents = {
error: string;
disconnected: undefined;
};

export abstract class BaseWebsocket<T extends EventMap> extends TypedEmitter<T & WsEvents> {
readonly options: Options;

private readonly messages;
export abstract class BaseWebsocket<T extends Record<string, any>> extends WebsocketClient<T> {
private readonly subscriptions: Subscription<keyof T>[] = [];
private readonly emitter: TypedEmitter<WsEvents> = this;

private ws?: WebSocket;
private pingTimeout?: TimerId;
private connectPromise?: Promise<void>;

protected abstract ping(): Promise<unknown>;

protected abstract createWebsocket(): WebSocket;

constructor(options: Options) {
super();
this.options = options;
this.messages = createDeferredManager({
timeout: this.options.timeout || DEFAULT_TIMEOUT,
onTimeout: this.onTimeout.bind(this),
});
}

private setPingTimeout() {
if (this.pingTimeout) {
clearTimeout(this.pingTimeout);
}
this.pingTimeout = setTimeout(
this.onPing.bind(this),
this.options.pingTimeout || DEFAULT_PING_TIMEOUT,
);
}

private onTimeout() {
const { ws } = this;
if (!ws) return;
this.messages.rejectAll(new CustomError('websocket_timeout'));
ws.close();
}

private async onPing() {
async onPing() {
// make sure that connection is alive if there are subscriptions
if (this.ws && this.isConnected()) {
try {
if (this.subscriptions.length > 0 || this.options.keepAlive) {
await this.ping();
} else {
this.ws.close();
}
} catch {
// empty
}
if (this.subscriptions.length > 0 || this.options.keepAlive) {
await this.ping();
} else {
this.disconnect();
}
}

private onError() {
this.onClose();
}

protected sendMessage(message: Record<string, any>) {
const { ws } = this;
if (!ws) throw new CustomError('websocket_not_initialized');
const { promiseId, promise } = this.messages.create();

const req = { id: promiseId.toString(), ...message };

this.setPingTimeout();

this.options.onSending?.(message);

ws.send(JSON.stringify(req));

return promise;
}

protected onMessage(message: string) {
protected onMessage(message: WebsocketResponse) {
try {
const resp = JSON.parse(message);
const resp = JSON.parse(message.toString());
const { id, data } = resp;

const messageSettled = data.error
Expand All @@ -128,8 +40,12 @@ export abstract class BaseWebsocket<T extends EventMap> extends TypedEmitter<T &
} catch {
// empty
}
}

this.setPingTimeout();
protected sendMessage(message: WebsocketRequest) {
return super.sendMessage(message).catch(error => {
throw new CustomError(error.message);
});
}

protected addSubscription<E extends keyof T>(type: E, callback: (result: T[E]) => void) {
Expand All @@ -146,95 +62,4 @@ export abstract class BaseWebsocket<T extends EventMap> extends TypedEmitter<T &

return index;
}

async connect() {
// if connecting already, just return the promise
if (this.connectPromise) {
return this.connectPromise;
}

if (this.ws?.readyState === WebSocket.CLOSING) {
await new Promise<void>(resolve => this.emitter.once('disconnected', resolve));
}

// create deferred promise
const dfd = createDeferred(-1);
this.connectPromise = dfd.promise;

const ws = this.createWebsocket();

// set connection timeout before WebSocket initialization
const connectionTimeout = setTimeout(
() => {
ws.emit('error', new CustomError('websocket_timeout'));
try {
ws.once('error', () => {}); // hack; ws throws uncaughtably when there's no error listener
ws.close();
} catch {
// empty
}
},
this.options.connectionTimeout || this.options.timeout || DEFAULT_TIMEOUT,
);

ws.once('error', error => {
clearTimeout(connectionTimeout);
this.onClose();
dfd.reject(new CustomError('websocket_runtime_error', error.message));
});
ws.on('open', () => {
clearTimeout(connectionTimeout);
this.init();
dfd.resolve();
});

this.ws = ws;

// wait for onopen event
return dfd.promise.finally(() => {
this.connectPromise = undefined;
});
}

private init() {
const { ws } = this;
if (!ws || !this.isConnected()) {
throw Error('Websocket init cannot be called');
}

// remove previous listeners and add new listeners
ws.removeAllListeners();
ws.on('error', this.onError.bind(this));
ws.on('message', this.onMessage.bind(this));
ws.on('close', () => {
this.onClose();
});
}

disconnect() {
this.emitter.emit('disconnected');
this.ws?.close();
}

isConnected() {
return this.ws?.readyState === WebSocket.OPEN;
}

private onClose() {
if (this.pingTimeout) {
clearTimeout(this.pingTimeout);
}

this.disconnect();

this.ws?.removeAllListeners();
this.messages.rejectAll(
new CustomError('websocket_runtime_error', 'Websocket closed unexpectedly'),
);
}

dispose() {
this.removeAllListeners();
this.onClose();
}
}
7 changes: 2 additions & 5 deletions packages/blockchain-link/src/workers/blockbook/websocket.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import WebSocket from 'ws';

import { CustomError } from '@trezor/blockchain-link-types/src/constants/errors';
import type {
BlockNotification,
Expand Down Expand Up @@ -48,9 +46,8 @@ export class BlockbookAPI extends BaseWebsocket<BlockbookEvents> {
url += suffix;
}

// initialize connection,
// options are not used in web builds (see ./src/utils/ws)
return new WebSocket(url, {
return this.initWebsocket({
url,
agent: this.options.agent,
headers: {
Origin: 'https://node.trezor.io',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import WebSocket from 'ws';

import type {
Send,
BlockContent,
Expand All @@ -23,8 +21,8 @@ export class BlockfrostAPI extends BaseWebsocket<BlockfrostEvents> {
protected createWebsocket() {
const { url } = this.options;

// options are not used in web builds (see ./src/utils/ws)
return new WebSocket(url, {
return this.initWebsocket({
url,
agent: this.options.agent,
headers: {
Origin: 'https://node.trezor.io',
Expand Down
8 changes: 4 additions & 4 deletions packages/blockchain-link/tests/unit/errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,16 @@ describe('Custom errors', () => {
expect(error.message).toBe('Worker not found');
});

it('Error with custom code and custom message', () => {
it('Error with custom prefixed code and custom message', () => {
const error = new CustomError('blockchain_link/custom', 'Custom message');
expect(error.code).toBe('blockchain_link/custom');
expect(error.message).toBe('Custom message');
});

it('Error with custom code and without message', () => {
it('Error with custom code as message', () => {
const error = new CustomError('custom');
expect(error.code).toBe('blockchain_link/custom');
expect(error.message).toBe('Message not set');
expect(error.code).toBe(undefined);
expect(error.message).toBe('custom');
});

it('Error without code and with custom message', () => {
Expand Down
1 change: 1 addition & 0 deletions packages/blockchain-link/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
{ "path": "../env-utils" },
{ "path": "../utils" },
{ "path": "../utxo-lib" },
{ "path": "../websocket-client" },
{ "path": "../e2e-utils" },
{ "path": "../eslint" },
{ "path": "../type-utils" }
Expand Down
Loading
Loading