From a8b60872d1a7bd2385db51d5a89f5f46b46ea3bb Mon Sep 17 00:00:00 2001 From: Martin Varmuza Date: Thu, 16 Jan 2025 16:15:56 +0100 Subject: [PATCH 01/20] chore(connect): [wip] Device handshake blind spots --- packages/connect/src/device/Device.ts | 1 + .../src/device/__tests__/DeviceList.test.ts | 14 ++++---------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/packages/connect/src/device/Device.ts b/packages/connect/src/device/Device.ts index aabf9b99b17..9eba2b960be 100644 --- a/packages/connect/src/device/Device.ts +++ b/packages/connect/src/device/Device.ts @@ -363,6 +363,7 @@ export class Device extends TypedEmitter { error.message === TRANSPORT_ERROR.HTTP_ERROR // bridge died during device initialization ) { // disconnected, do nothing + this.emitLifecycle(DEVICE.DISCONNECT); } else if ( // we don't know what really happened error.message === TRANSPORT_ERROR.UNEXPECTED_ERROR || diff --git a/packages/connect/src/device/__tests__/DeviceList.test.ts b/packages/connect/src/device/__tests__/DeviceList.test.ts index 73440cabf49..705a1dc484e 100644 --- a/packages/connect/src/device/__tests__/DeviceList.test.ts +++ b/packages/connect/src/device/__tests__/DeviceList.test.ts @@ -141,20 +141,14 @@ describe('DeviceList', () => { list.init({ transports: [transport], pendingTransportEvent: true }); const transportFirstEvent = list.pendingConnection(); - // NOTE: this behavior is wrong, if device creation fails DeviceList shouldn't wait 10 secs. - jest.useFakeTimers(); - // move 9 sec forward - await jest.advanceTimersByTimeAsync(9 * 1000); // no events yet expect(eventsSpy).toHaveBeenCalledTimes(0); - // move 2 sec forward - await jest.advanceTimersByTimeAsync(2 * 1000); - // promise should be resolved by now await transportFirstEvent; - jest.useRealTimers(); + const events = eventsSpy.mock.calls.map(call => call[0]); + expect(events).toEqual(['device-disconnect', 'transport-start']); - expect(eventsSpy).toHaveBeenCalledTimes(1); - expect(eventsSpy.mock.calls[0][0]).toEqual('transport-start'); + // todo: device should also disappear from DeviceList but it does not. this needs to get discussed + expect(list.getDeviceCount()).toEqual(0); }); it('.init() with pendingTransportEvent (unreadable device)', async () => { From 3be009ce116f84fcfe9d782094b03b71476d3452 Mon Sep 17 00:00:00 2001 From: Martin Varmuza Date: Fri, 17 Jan 2025 16:55:19 +0100 Subject: [PATCH 02/20] chore(metadata-utils): move hash lenght sanity check to both methods --- packages/suite/src/utils/suite/metadata.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/suite/src/utils/suite/metadata.ts b/packages/suite/src/utils/suite/metadata.ts index b41c72c4909..976f8043aef 100644 --- a/packages/suite/src/utils/suite/metadata.ts +++ b/packages/suite/src/utils/suite/metadata.ts @@ -26,11 +26,7 @@ const deriveHmac = (metadataKey: string) => { const buf = Buffer.from('0123456789abcdeffedcba9876543210', 'hex'); hmac.update(buf); - return hmac.digest(); -}; - -export const deriveAesKey = (metadataKey: string) => { - const hash = deriveHmac(metadataKey); + const hash = hmac.digest(); if (hash.length !== 64 && Buffer.byteLength(hash) !== 64) { throw new Error( `Strange buffer length when deriving account hmac ${hash.length} ; ${Buffer.byteLength( @@ -38,6 +34,13 @@ export const deriveAesKey = (metadataKey: string) => { )}`, ); } + + return hash; +}; + +export const deriveAesKey = (metadataKey: string) => { + const hash = deriveHmac(metadataKey); + const secondHalf = hash.subarray(32, 64); return secondHalf.toString('hex'); From a5268f544f05a3425cd7b765550c75e15e69a295 Mon Sep 17 00:00:00 2001 From: Martin Varmuza Date: Wed, 22 Jan 2025 10:38:46 +0100 Subject: [PATCH 03/20] chore(connect-explorer): warn instead of error when json is invalid --- packages/connect-explorer/src/actions/methodActions.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/connect-explorer/src/actions/methodActions.ts b/packages/connect-explorer/src/actions/methodActions.ts index 93059adbe46..15f64377a1a 100644 --- a/packages/connect-explorer/src/actions/methodActions.ts +++ b/packages/connect-explorer/src/actions/methodActions.ts @@ -158,6 +158,10 @@ export const onCodeChange = (value: string) => (dispatch: Dispatch, getState: Ge }; fields.forEach(processField); } catch (error) { - console.error('Invalid JSON', error); + if (error.message.includes('JSON5:')) { + console.warn('Invalid JSON', error); + } else { + console.error(error); + } } }; From 5aa78cf3a1483ff199316a262206c9aad7e1f594 Mon Sep 17 00:00:00 2001 From: Martin Varmuza Date: Wed, 29 Jan 2025 14:08:28 +0100 Subject: [PATCH 04/20] ci: run unit tests in nightly without cache and coverage --- .github/workflows/test-misc.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-misc.yml b/.github/workflows/test-misc.yml index f571d11a4de..020f45e6d6d 100644 --- a/.github/workflows/test-misc.yml +++ b/.github/workflows/test-misc.yml @@ -71,7 +71,7 @@ jobs: - run: yarn install --immutable - run: yarn message-system-sign-config - - run: yarn test:unit + - run: yarn test:unit --detectOpenHandles --no-cache --coverage false utility-scripts: runs-on: ubuntu-latest From 15d2c1e2ffd6c1c5cf933751753abbf98f26d082 Mon Sep 17 00:00:00 2001 From: Martin Varmuza Date: Wed, 29 Jan 2025 17:59:56 +0100 Subject: [PATCH 05/20] chore(transport): update long from 4.0.0 to 5.2.0 --- packages/transport/package.json | 2 +- yarn.lock | 17 +++++------------ 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/packages/transport/package.json b/packages/transport/package.json index 262a7450dcd..fcdc9c84e78 100644 --- a/packages/transport/package.json +++ b/packages/transport/package.json @@ -67,7 +67,7 @@ "@trezor/protocol": "workspace:*", "@trezor/utils": "workspace:*", "cross-fetch": "^4.0.0", - "long": "^4.0.0", + "long": "^5.2.0", "protobufjs": "7.4.0", "usb": "^2.14.0" }, diff --git a/yarn.lock b/yarn.lock index 69378db16e6..77af9ede985 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13365,7 +13365,7 @@ __metadata: cross-fetch: "npm:^4.0.0" jest: "npm:29.7.0" jest-environment-node: "npm:^29.7.0" - long: "npm:^4.0.0" + long: "npm:^5.2.0" protobufjs: "npm:7.4.0" ts-node: "npm:^10.9.1" tsx: "npm:^4.16.3" @@ -30319,17 +30319,10 @@ __metadata: languageName: node linkType: hard -"long@npm:^4.0.0": - version: 4.0.0 - resolution: "long@npm:4.0.0" - checksum: 10/8296e2ba7bab30f9cfabb81ebccff89c819af6a7a78b4bb5a70ea411aa764ee0532f7441381549dfa6a1a98d72abe9138bfcf99f4fa41238629849bc035b845b - languageName: node - linkType: hard - -"long@npm:^5.0.0": - version: 5.2.3 - resolution: "long@npm:5.2.3" - checksum: 10/9167ec6947a825b827c30da169a7384eec6c0c9ec2f0b9c74da2e93d81159bbe39fb09c3f13dae9721d4b807ccfa09797a7dd1012f5d478e3e33ca3c78b608e6 +"long@npm:^5.0.0, long@npm:^5.2.0": + version: 5.2.4 + resolution: "long@npm:5.2.4" + checksum: 10/c27c060a683d4d76dc48da12ded0ae49c610aaf10d028ec938829d7bebe916979dcc8b67ed71f8bf6d845a90151b66a9b741a3ee51ec874908e496c2a576697a languageName: node linkType: hard From 130b71be6009a9db76f0fdf759ad01560208127d Mon Sep 17 00:00:00 2001 From: Martin Varmuza Date: Mon, 3 Feb 2025 15:25:29 +0100 Subject: [PATCH 06/20] wip: send beacon --- packages/connect-web/src/module/index.ts | 4 +++ packages/connect/src/device/Device.ts | 7 ++--- packages/connect/src/device/DeviceList.ts | 29 +++++++++++---------- packages/transport/src/transports/bridge.ts | 5 ++++ 4 files changed, 28 insertions(+), 17 deletions(-) diff --git a/packages/connect-web/src/module/index.ts b/packages/connect-web/src/module/index.ts index 91792bd4ea4..76f3c33dc43 100644 --- a/packages/connect-web/src/module/index.ts +++ b/packages/connect-web/src/module/index.ts @@ -79,3 +79,7 @@ const TrezorConnect = factory( export default TrezorConnect; export * from '@trezor/connect/src/exports'; + +window.addEventListener('beforeunload', () => { + impl.dispose(); +}); diff --git a/packages/connect/src/device/Device.ts b/packages/connect/src/device/Device.ts index 9eba2b960be..7fe87725165 100644 --- a/packages/connect/src/device/Device.ts +++ b/packages/connect/src/device/Device.ts @@ -1171,12 +1171,13 @@ export class Device extends TypedEmitter { return null; } - async dispose() { + dispose() { this.removeAllListeners(); if (this.session && this.lastAcquiredHere) { try { - await this.cancelableAction?.(); - await this.commands?.cancel(); + // note for review: these deleted lines might not be needed anymore - in case of reload we don't have enough time + // anyway, so seems to be enough to release session so that we don't get 'unacquired' device after reloading. If + // anything stays on the screen, it will get flushed away by the initial Cancel (see device run method) return this.transport.release({ session: this.session, diff --git a/packages/connect/src/device/DeviceList.ts b/packages/connect/src/device/DeviceList.ts index db0b1d209fd..4d1ec05ad67 100644 --- a/packages/connect/src/device/DeviceList.ts +++ b/packages/connect/src/device/DeviceList.ts @@ -545,26 +545,27 @@ export class DeviceList extends TypedEmitter implements IDevic return this.devices.all().find(d => d.features?.device_id === deviceId); } - async dispose() { + dispose() { this.removeAllListeners(); const promises = typedObjectKeys(this.transport) .concat(typedObjectKeys(this.locks)) .filter(arrayDistinct) - .map(apiType => + .map(apiType => { this.transportLock(apiType, 'Disposing', async () => { const transport = this.transport[apiType]; + if (transport) { delete this.transport[apiType]; - await this.stopTransport(transport); + this.stopTransport(transport); } - }), - ); + }); + }); - await Promise.all(promises); + Promise.all(promises); } - private async stopTransport(transport: Transport) { + private stopTransport(transport: Transport) { clearTimeout(this.scheduledUpgradeChecks[transport.apiType]); const devices = this.devices.clear(transport); @@ -576,16 +577,16 @@ export class DeviceList extends TypedEmitter implements IDevic }); // release all devices - await Promise.all( + return Promise.all( devices.map(async device => { this.authPenaltyManager.remove(device); // TODO is this right? - await device.dispose(); + return device.dispose(); }), - ); - - // now we can be relatively sure that release calls have been dispatched - // and we can safely kill all async subscriptions in transport layer - transport?.stop(); + ).finally(() => { + // now we can be relatively sure that release calls have been dispatched + // and we can safely kill all async subscriptions in transport layer + transport?.stop(); + }); } async enumerate() { diff --git a/packages/transport/src/transports/bridge.ts b/packages/transport/src/transports/bridge.ts index a924cbbf501..82d9fa35f73 100644 --- a/packages/transport/src/transports/bridge.ts +++ b/packages/transport/src/transports/bridge.ts @@ -184,6 +184,11 @@ export class BridgeTransport extends AbstractTransport { onClose, signal, }: AbstractTransportMethodParams<'release'>) { + if (onClose && typeof navigator !== 'undefined' && 'sendBeacon' in navigator) { + navigator.sendBeacon(`${this.url}/release/${session}?beacon=1`); + return Promise.resolve(this.success(null)); + } + return this.scheduleAction( async signal => { const releasePromise = this.post('/release', { From 0da4a5f1b4f5c463c5e06120e4ade209619343e2 Mon Sep 17 00:00:00 2001 From: Martin Varmuza Date: Tue, 4 Feb 2025 12:17:43 +0100 Subject: [PATCH 07/20] chore(connect): log a possible issue --- packages/connect/src/device/Device.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/connect/src/device/Device.ts b/packages/connect/src/device/Device.ts index 7fe87725165..9af33d873c5 100644 --- a/packages/connect/src/device/Device.ts +++ b/packages/connect/src/device/Device.ts @@ -566,6 +566,7 @@ export class Device extends TypedEmitter { // do not initialize while firstRunPromise otherwise `features.session_id` could be affected await Promise.race([ + // todo: there is a issue at least with bridge (old and new) that getFeatures timeout does not kill the request and sending subsequent initialize gets 'other call in progress' error this.getFeatures().finally(() => clearTimeout(getFeaturesTimeoutId)), // note: tested on 24.7.2024 and whatever is written below this line is still valid // We do not support T1B1 <1.9.0 but we still need Features even from not supported devices to determine your version From 8e09a45f3d97c4b8e6719b94f88141794daef125 Mon Sep 17 00:00:00 2001 From: Martin Varmuza Date: Mon, 3 Feb 2025 16:03:09 +0100 Subject: [PATCH 08/20] wip(transport): maybe enumerate on disconnected error --- packages/transport/src/api/usb.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/transport/src/api/usb.ts b/packages/transport/src/api/usb.ts index dd806fd98ab..5f3490b1efe 100644 --- a/packages/transport/src/api/usb.ts +++ b/packages/transport/src/api/usb.ts @@ -518,6 +518,9 @@ export class UsbApi extends AbstractApi { 'The device was disconnected.', ].some(disconnectedErr => err.message.includes(disconnectedErr)) ) { + // make sure that local descriptors are updated and higher layers are notified + this.enumerate(); + return this.error({ error: ERRORS.DEVICE_DISCONNECTED_DURING_ACTION }); } From f67c5a1a10dc338d910a809d359a0f6ea815990d Mon Sep 17 00:00:00 2001 From: Martin Varmuza Date: Mon, 3 Feb 2025 16:02:49 +0100 Subject: [PATCH 09/20] fix(transport): stop narrowing disconnected errors --- packages/transport/src/api/usb.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/transport/src/api/usb.ts b/packages/transport/src/api/usb.ts index 5f3490b1efe..b943da38478 100644 --- a/packages/transport/src/api/usb.ts +++ b/packages/transport/src/api/usb.ts @@ -503,18 +503,15 @@ export class UsbApi extends AbstractApi { return [hidDevices, nonHidDevices]; } + // old bridge narrows multiple different errors to "device was disconnected" error. // https://github.com/trezor/trezord-go/blob/db03d99230f5b609a354e3586f1dfc0ad6da16f7/usb/libusb.go#L545 + // I am not sure if this is correct so at this point we only narrow disconnected error from node-usb and webusb private handleReadWriteError(err: Error) { if ( [ // node usb - 'LIBUSB_TRANSFER_ERROR', - 'LIBUSB_ERROR_PIPE', - 'LIBUSB_ERROR_IO', 'LIBUSB_ERROR_NO_DEVICE', - 'LIBUSB_ERROR_OTHER', // web usb - ERRORS.INTERFACE_DATA_TRANSFER, 'The device was disconnected.', ].some(disconnectedErr => err.message.includes(disconnectedErr)) ) { From 0660c663d4e46ad91438dd7375149418e9ed1886 Mon Sep 17 00:00:00 2001 From: Martin Varmuza Date: Mon, 3 Feb 2025 15:35:28 +0100 Subject: [PATCH 10/20] note(connect): log possible issue in device list --- packages/connect/src/device/Device.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/connect/src/device/Device.ts b/packages/connect/src/device/Device.ts index 9af33d873c5..6a2c95504e3 100644 --- a/packages/connect/src/device/Device.ts +++ b/packages/connect/src/device/Device.ts @@ -375,6 +375,8 @@ export class Device extends TypedEmitter { error.code === 'Device_InitializeFailed' ) { this.emitLifecycle(DEVICE.CONNECT_UNACQUIRED); + // todo: shouldn't we enumerate here to prevent the case where backend has a different session from the local one and acquiring + // keeps returning "wrong previous session"? } else if ( // device was claimed by another application on transport api layer (claimInterface in usb nomenclature) but never released (releaseInterface in usb nomenclature) // the only remedy for this is to reconnect device manually From 157a16a619efa40b0001425436b684e090eacabe Mon Sep 17 00:00:00 2001 From: Martin Varmuza Date: Mon, 3 Feb 2025 15:31:52 +0100 Subject: [PATCH 11/20] wip(transport): synchronize reset.device --- packages/transport/src/api/usb.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/transport/src/api/usb.ts b/packages/transport/src/api/usb.ts index b943da38478..16111f25b73 100644 --- a/packages/transport/src/api/usb.ts +++ b/packages/transport/src/api/usb.ts @@ -37,6 +37,7 @@ export class UsbApi extends AbstractApi { private debugLink?: boolean; private synchronizeCreateDevices = getSynchronize(); private synchronizeGetDevices = getSynchronize(); + private synchronizeDeviceReset = getSynchronize(); constructor({ usbInterface, logger, forceReadSerialOnConnect, debugLink }: ConstructorParams) { super({ logger }); @@ -204,7 +205,7 @@ export class UsbApi extends AbstractApi { this.debugLink ? DEBUGLINK_ENDPOINT_ID : ENDPOINT_ID, this.chunkSize, ), - { signal, onAbort: () => device?.reset() }, + { signal, onAbort: () => this.synchronizeDeviceReset(() => device?.reset()) }, ); this.logger?.debug( `usb: device.transferIn done. status: ${res.status}, byteLength: ${res.data?.byteLength}.`, @@ -247,7 +248,7 @@ export class UsbApi extends AbstractApi { this.debugLink ? DEBUGLINK_ENDPOINT_ID : ENDPOINT_ID, newArray, ), - { signal, onAbort: () => device?.reset() }, + { signal, onAbort: () => this.synchronizeDeviceReset(() => device?.reset()) }, ); this.logger?.debug(`usb: device.transferOut done.`); if (result.status !== 'ok') { @@ -320,7 +321,10 @@ export class UsbApi extends AbstractApi { try { // reset fails on ChromeOS and windows this.logger?.debug('usb: device.reset'); - await this.abortableMethod(() => device?.reset(), { signal }); + await this.abortableMethod( + () => this.synchronizeDeviceReset(() => device?.reset()), + { signal }, + ); this.logger?.debug(`usb: device.reset done.`); } catch (err) { this.logger?.error( @@ -359,7 +363,7 @@ export class UsbApi extends AbstractApi { if (!this.debugLink) { try { // NOTE: `device.reset()` interrupts transfers for all interfaces (debugLink and normal) - await device.reset(); + await this.synchronizeDeviceReset(() => device?.reset()); } catch (err) { this.logger?.error( `usb: device.reset error ${err}. device: ${this.formatDeviceForLog(device)}`, From d302dc51db29da59a649e02938d1ffefb3c6e9b8 Mon Sep 17 00:00:00 2001 From: Martin Varmuza Date: Mon, 3 Feb 2025 15:26:24 +0100 Subject: [PATCH 12/20] wip(transport-bridge): some additional test --- .../transport-test/e2e/bridge/bridge.test.ts | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/packages/transport-test/e2e/bridge/bridge.test.ts b/packages/transport-test/e2e/bridge/bridge.test.ts index 699f4db8a3d..f4e61e35611 100644 --- a/packages/transport-test/e2e/bridge/bridge.test.ts +++ b/packages/transport-test/e2e/bridge/bridge.test.ts @@ -319,4 +319,43 @@ describe('bridge', () => { await Promise.all([bridge.receive({ session }), bridge.enumerate()]); }); } + + test.only('call and abort', async () => { + const promise = bridge.call({ session, name: 'GetFeatures', data: {} }); + await Promise.resolve(); + bridge.stop(); + expect(promise).resolves.toMatchObject({ + error: 'Aborted by signal', + message: undefined, + success: false, + }); + + const bridge2 = new BridgeTransport({ messages, id: '' }); + expect(await bridge2.init()).toMatchObject({ success: true }); + + // enumerate + const enumerateResult = await bridge2.enumerate(); + + // acquire + const acquireResult = await bridge2.acquire({ + input: { path: descriptors[0].path, previous: session }, + }); + + // getFeatures + const message = await bridge2.call({ + session: acquireResult.payload, + name: 'GetFeatures', + data: {}, + }); + + console.log('message', message); + + // double release + bridge2.release({ session: acquireResult.payload }); + await bridge2.release({ session: acquireResult.payload }); + + // enumerate + const enumerateResult2 = await bridge2.enumerate(); + console.log('enumerateResult2', enumerateResult2); + }); }); From 0760e634ae09f4835cda7d7c74e558d673f3b3bb Mon Sep 17 00:00:00 2001 From: Martin Varmuza Date: Mon, 3 Feb 2025 15:26:02 +0100 Subject: [PATCH 13/20] wip(connect): device lifecycle test --- .../e2e/tests/device/deviceLifecycle.test.ts | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 packages/connect/e2e/tests/device/deviceLifecycle.test.ts diff --git a/packages/connect/e2e/tests/device/deviceLifecycle.test.ts b/packages/connect/e2e/tests/device/deviceLifecycle.test.ts new file mode 100644 index 00000000000..d8237086632 --- /dev/null +++ b/packages/connect/e2e/tests/device/deviceLifecycle.test.ts @@ -0,0 +1,84 @@ +import TrezorConnect from '../../../src'; +import { getController, initTrezorConnect, setup } from '../../common.setup'; + +const controller = getController(); + +describe('TrezorConnect device lifecycle tests', () => { + beforeAll(async () => { + await controller.connect(); + }); + afterAll(() => { + controller.dispose(); + TrezorConnect.dispose(); + }); + + it('TrezorConnect.init -> call', async () => { + TrezorConnect.dispose(); + TrezorConnect.removeAllListeners(); + await controller.stopEmu(); + await controller.startBridge(); + await initTrezorConnect(controller, { autoConfirm: false }); + + const firstDeviceEventSpy = jest.fn(); + TrezorConnect.on('device-connect', firstDeviceEventSpy); + const selectDeviceEventPromise = new Promise(resolve => { + TrezorConnect.on('ui-select_device', resolve); + }); + + TrezorConnect.getAddress({ + path: "m/44'/1'/0'/0/0", + showOnTrezor: false, + }); + + expect(await selectDeviceEventPromise).toMatchObject({ webusb: false, devices: [] }); + expect(firstDeviceEventSpy).toHaveBeenCalledTimes(0); + }); + + [1, 100, 1000].forEach(delay => { + it(`TrezorConnect.init -> startEmu -> wait ${delay}ms -> stopEmu -> startEmu -> device-connect event`, async () => { + TrezorConnect.dispose(); + TrezorConnect.removeAllListeners(); + await setup(controller, { + mnemonic: 'mnemonic_all', + }); + await controller.stopEmu(); + await initTrezorConnect(controller, { autoConfirm: false }); + + const deviceConnectEventPromise = new Promise(resolve => { + TrezorConnect.on('device-connect', resolve); + }); + + await controller.startEmu(); + + // testing disconnecting device during the initial reading of the device + await new Promise(resolve => setTimeout(resolve, delay)); + await controller.stopEmu(); + await controller.startEmu(); + + await deviceConnectEventPromise; + }); + }); + + it('TrezorConnect.init -> start emu -> device-connect event -> call', async () => { + TrezorConnect.dispose(); + TrezorConnect.removeAllListeners(); + await setup(controller, { + mnemonic: 'mnemonic_all', + }); + await controller.stopEmu(); + await initTrezorConnect(controller, { autoConfirm: false }); + + const deviceConnectEventPromise = new Promise(resolve => { + TrezorConnect.on('device-connect', resolve); + }); + + await controller.startEmu(); + await deviceConnectEventPromise; + + const response = await TrezorConnect.getAddress({ + path: "m/44'/1'/0'/0/0", + showOnTrezor: false, + }); + expect(response.success).toBe(true); + }); +}); From d8a6b2522b8531d43a2264a8a019ce685e39b34b Mon Sep 17 00:00:00 2001 From: Martin Varmuza Date: Mon, 3 Feb 2025 15:22:28 +0100 Subject: [PATCH 14/20] test(suite): reload during discovery to test 'unexpected message' error does not appear --- packages/connect/src/core/index.ts | 5 ++++- .../suite-web/e2e/tests/wallet/discovery.test.ts | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/connect/src/core/index.ts b/packages/connect/src/core/index.ts index 1106f3db7a2..62a7946b8e0 100644 --- a/packages/connect/src/core/index.ts +++ b/packages/connect/src/core/index.ts @@ -715,8 +715,11 @@ const onCallDevice = async ( // TODO: This requires a massive refactoring https://github.com/trezor/trezor-suite/issues/5323 // @ts-expect-error TODO: messageResponse should be assigned from the response of "inner" function const response = messageResponse; - if (response) { + // todo: shouldReleaseSession should probably be removed (it was added recently). it looks like that we could + // receive 'disconnected during action' which does not mean that device got physically disconnected. + // see + // https://github.com/trezor/trezord-go/blob/db03d99230f5b609a354e3586f1dfc0ad6da16f7/usb/libusb.go#L545 const shouldReleaseSession = response.success || (!response.success && diff --git a/packages/suite-web/e2e/tests/wallet/discovery.test.ts b/packages/suite-web/e2e/tests/wallet/discovery.test.ts index f0ee800cf5c..dba2b715408 100644 --- a/packages/suite-web/e2e/tests/wallet/discovery.test.ts +++ b/packages/suite-web/e2e/tests/wallet/discovery.test.ts @@ -1,6 +1,8 @@ // @group_wallet // @retry=2 +import { getRandomInt } from '@trezor/utils'; + import { onNavBar } from '../../support/pageObjects/topBarObject'; // discovery should end within this time frame @@ -29,6 +31,18 @@ describe('Discovery', () => { cy.log('all available networks should return something from discovery'); cy.getTestElement('@dashboard/loading', { timeout: 1000 * 10 }); + + // wait randomly between 1000 and 3000 ms + cy.wait(getRandomInt(1, 40) * 100); + // trigger reload to simulate interruption. we want to make sure that communication with the device does not + // end up in some de-synced state. if this test becomes flaky, this reload might be the reason. + cy.reload(); + + // device appears as connected + cy.getTestElement('@deviceStatus-connected'); + // dashboard is still loading, discovery starts, no error appears + cy.getTestElement('@dashboard/loading'); + cy.getTestElement('@dashboard/loading', { timeout: DISCOVERY_LIMIT }).should('not.exist'); ['btc', ...coinsToActivate].forEach(symbol => { cy.getTestElement(`@wallet/coin-balance/value-${symbol}`); From 78d3bbfaf651ef86919727c1b193719791cf9347 Mon Sep 17 00:00:00 2001 From: Martin Varmuza Date: Thu, 23 Jan 2025 15:33:57 +0100 Subject: [PATCH 15/20] chore(transport-test): add a todo note --- packages/transport-bridge/src/http.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/transport-bridge/src/http.ts b/packages/transport-bridge/src/http.ts index 20aab0dfcf5..5660aedef6a 100644 --- a/packages/transport-bridge/src/http.ts +++ b/packages/transport-bridge/src/http.ts @@ -178,6 +178,8 @@ export class TrezordNode { }; res.addListener('close', listener); + // todo: is listener removed correctly? I noted it was firing long after the request was settled. + return abortController.signal; } From 00b8878cf39a148767bfd67b744dce67f23b86cc Mon Sep 17 00:00:00 2001 From: Martin Varmuza Date: Wed, 5 Feb 2025 18:52:44 +0100 Subject: [PATCH 16/20] fix(transport): clear locks also on device disconnect --- packages/transport/src/api/usb.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/transport/src/api/usb.ts b/packages/transport/src/api/usb.ts index 16111f25b73..e39bbb966e6 100644 --- a/packages/transport/src/api/usb.ts +++ b/packages/transport/src/api/usb.ts @@ -75,6 +75,7 @@ export class UsbApi extends AbstractApi { const index = this.devices.findIndex(d => d.path === device.serialNumber); if (index > -1) { + delete this.lock[this.devices[index].path]; this.devices.splice(index, 1); this.emit('transport-interface-change', this.devicesToDescriptors()); } else { From 0323c217820993f1c7e9c2ecf300f21965ccb891 Mon Sep 17 00:00:00 2001 From: Martin Varmuza Date: Wed, 5 Feb 2025 22:09:36 +0100 Subject: [PATCH 17/20] wip: https://github.com/trezor/trezor-suite/issues/13550 --- packages/transport/src/api/usb.ts | 53 ++++++++++--------- .../src/discovery/discoveryThunks.ts | 13 ++++- 2 files changed, 39 insertions(+), 27 deletions(-) diff --git a/packages/transport/src/api/usb.ts b/packages/transport/src/api/usb.ts index e39bbb966e6..f10df195f36 100644 --- a/packages/transport/src/api/usb.ts +++ b/packages/transport/src/api/usb.ts @@ -307,33 +307,34 @@ export class UsbApi extends AbstractApi { }); } - if (first) { - try { - this.logger?.debug(`usb: device.selectConfiguration ${CONFIGURATION_ID}`); - await this.abortableMethod(() => device.selectConfiguration(CONFIGURATION_ID), { - signal, - }); - this.logger?.debug(`usb: device.selectConfiguration done: ${CONFIGURATION_ID}.`); - } catch (err) { - this.logger?.error( - `usb: device.selectConfiguration error ${err}. device: ${this.formatDeviceForLog(device)}`, - ); - } - try { - // reset fails on ChromeOS and windows - this.logger?.debug('usb: device.reset'); - await this.abortableMethod( - () => this.synchronizeDeviceReset(() => device?.reset()), - { signal }, - ); - this.logger?.debug(`usb: device.reset done.`); - } catch (err) { - this.logger?.error( - `usb: device.reset error ${err}. device: ${this.formatDeviceForLog(device)}`, - ); - // empty - } + // commenting out first fixes https://github.com/trezor/trezor-suite/issues/13550#issuecomment-2637659335 + // if (first) { + try { + this.logger?.debug(`usb: device.selectConfiguration ${CONFIGURATION_ID}`); + await this.abortableMethod(() => device.selectConfiguration(CONFIGURATION_ID), { + signal, + }); + this.logger?.debug(`usb: device.selectConfiguration done: ${CONFIGURATION_ID}.`); + } catch (err) { + this.logger?.error( + `usb: device.selectConfiguration error ${err}. device: ${this.formatDeviceForLog(device)}`, + ); + } + try { + // reset fails on ChromeOS and windows + this.logger?.debug('usb: device.reset'); + await this.abortableMethod(() => this.synchronizeDeviceReset(() => device?.reset()), { + signal, + }); + this.logger?.debug(`usb: device.reset done.`); + } catch (err) { + this.logger?.error( + `usb: device.reset error ${err}. device: ${this.formatDeviceForLog(device)}`, + ); + // empty } + // } + try { const interfaceId = this.debugLink ? DEBUGLINK_INTERFACE_ID : INTERFACE_ID; this.logger?.debug(`usb: device.claimInterface: ${interfaceId}`); diff --git a/suite-common/wallet-core/src/discovery/discoveryThunks.ts b/suite-common/wallet-core/src/discovery/discoveryThunks.ts index 623da079c85..9df70a63487 100644 --- a/suite-common/wallet-core/src/discovery/discoveryThunks.ts +++ b/suite-common/wallet-core/src/discovery/discoveryThunks.ts @@ -421,7 +421,7 @@ export const startDiscoveryThunk = createThunk( thunks: { initMetadata, fetchAndSaveMetadata }, actions: { requestAuthConfirm }, } = extra; - const device = selectDevice(getState()); + let device = selectDevice(getState()); const metadata = selectMetadata(getState()); const discovery = selectDeviceDiscovery(getState()); @@ -469,9 +469,16 @@ export const startDiscoveryThunk = createThunk( // metadata are enabled in settings but metadata master key does not exist for this device // try to generate device metadata master key if passphrase is not used if (!authConfirm && metadataEnabled) { + console.log('discovery: initMetadata'); await dispatch(initMetadata(false)); + console.log('discovery: initMetadata done'); } + device = selectDevice(getState()); + if (device?.status !== 'available') { + console.log('device is not available anymore'); + return; + } dispatch( startDiscovery({ ...discovery, @@ -566,6 +573,7 @@ export const startDiscoveryThunk = createThunk( }; TrezorConnect.on(UI.BUNDLE_PROGRESS, onBundleProgress); + console.log('startDiscoveryThunk: TrezorConnect.getAccountInfo'); const result = await TrezorConnect.getAccountInfo({ device, bundle, @@ -598,6 +606,8 @@ export const startDiscoveryThunk = createThunk( }), ); // try to generate device metadata master key + // todo: why is it here again? + console.log('init metadata 2'); await dispatch(initMetadata(false)); } if (currentDiscovery.status === DiscoveryStatus.RUNNING) { @@ -661,6 +671,7 @@ export const startDiscoveryThunk = createThunk( } } + // todo: isn't it better to reload device from state and check its properties such as status and connected instead of error codes? if ( result.payload.error && device.connected && From c5855bf01abb36b8ed687857095b54d95e15337e Mon Sep 17 00:00:00 2001 From: Martin Varmuza Date: Tue, 11 Feb 2025 10:10:15 +0100 Subject: [PATCH 18/20] chore(monorepo): unify copy-webpack-plugin default import naming --- .../electron-renderer-with-assets/webpack/config.dev.js | 4 ++-- .../electron-renderer-with-assets/webpack/config.prod.js | 4 ++-- .../connect-explorer/webpack/webextension.webpack.config.ts | 4 ++-- packages/connect-popup/webpack/prod.webpack.config.ts | 4 ++-- .../connect-webextension/webpack/inline.webpack.config.ts | 4 ++-- packages/suite-build/configs/desktop.webpack.config.ts | 4 ++-- packages/suite-build/configs/web.webpack.config.ts | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/connect-examples/electron-renderer-with-assets/webpack/config.dev.js b/packages/connect-examples/electron-renderer-with-assets/webpack/config.dev.js index e80ada7bf1b..5529106be17 100644 --- a/packages/connect-examples/electron-renderer-with-assets/webpack/config.dev.js +++ b/packages/connect-examples/electron-renderer-with-assets/webpack/config.dev.js @@ -1,4 +1,4 @@ -const CopyPlugin = require('copy-webpack-plugin'); +const CopyWebpackPlugin = require('copy-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const path = require('path'); const { WebpackPluginServe } = require('webpack-plugin-serve'); @@ -28,7 +28,7 @@ module.exports = { ], }, plugins: [ - new CopyPlugin({ + new CopyWebpackPlugin({ patterns: [ { from: path.join(__dirname, '..', '..', '..', 'connect-iframe', 'build'), diff --git a/packages/connect-examples/electron-renderer-with-assets/webpack/config.prod.js b/packages/connect-examples/electron-renderer-with-assets/webpack/config.prod.js index fe6ab260f9b..014eb71e378 100644 --- a/packages/connect-examples/electron-renderer-with-assets/webpack/config.prod.js +++ b/packages/connect-examples/electron-renderer-with-assets/webpack/config.prod.js @@ -1,4 +1,4 @@ -const CopyPlugin = require('copy-webpack-plugin'); +const CopyWebpackPlugin = require('copy-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const path = require('path'); @@ -25,7 +25,7 @@ module.exports = { modules: ['node_modules'], }, plugins: [ - new CopyPlugin({ + new CopyWebpackPlugin({ patterns: [ { from: path.join(__dirname, '..', '..', '..', 'connect-iframe', 'build'), diff --git a/packages/connect-explorer/webpack/webextension.webpack.config.ts b/packages/connect-explorer/webpack/webextension.webpack.config.ts index 8849f71bf9c..1498339002e 100644 --- a/packages/connect-explorer/webpack/webextension.webpack.config.ts +++ b/packages/connect-explorer/webpack/webextension.webpack.config.ts @@ -1,4 +1,4 @@ -import CopyPlugin from 'copy-webpack-plugin'; +import CopyWebpackPlugin from 'copy-webpack-plugin'; import HtmlWebpackPlugin from 'html-webpack-plugin'; import path from 'path'; import webpack from 'webpack'; @@ -111,7 +111,7 @@ const config: webpack.Configuration = { inject: true, minify: false, }), - new CopyPlugin({ + new CopyWebpackPlugin({ patterns: [ { from: path.join(__dirname, '..', 'src-webextension', 'manifest.json'), diff --git a/packages/connect-popup/webpack/prod.webpack.config.ts b/packages/connect-popup/webpack/prod.webpack.config.ts index 8dfc2002ab1..1beffe3f674 100644 --- a/packages/connect-popup/webpack/prod.webpack.config.ts +++ b/packages/connect-popup/webpack/prod.webpack.config.ts @@ -1,5 +1,5 @@ import { execSync } from 'child_process'; -import CopyPlugin from 'copy-webpack-plugin'; +import CopyWebpackPlugin from 'copy-webpack-plugin'; import HtmlWebpackPlugin from 'html-webpack-plugin'; import path from 'path'; import TerserPlugin from 'terser-webpack-plugin'; @@ -107,7 +107,7 @@ const config: webpack.Configuration = { minify: false, urls: URLS, }), - new CopyPlugin({ + new CopyWebpackPlugin({ patterns: [ { from: `${STATIC_SRC}/popup.css`, diff --git a/packages/connect-webextension/webpack/inline.webpack.config.ts b/packages/connect-webextension/webpack/inline.webpack.config.ts index 164595dddf0..3a5659b54d6 100644 --- a/packages/connect-webextension/webpack/inline.webpack.config.ts +++ b/packages/connect-webextension/webpack/inline.webpack.config.ts @@ -1,4 +1,4 @@ -import CopyPlugin from 'copy-webpack-plugin'; +import CopyWebpackPlugin from 'copy-webpack-plugin'; import path from 'path'; import webpack from 'webpack'; @@ -30,7 +30,7 @@ const config: webpack.Configuration = { module: prod.module, resolve: prod.resolve, plugins: [ - new CopyPlugin({ + new CopyWebpackPlugin({ patterns: [ { from: `${path.join(__dirname, '..', 'dist', 'content-script.js')}`, diff --git a/packages/suite-build/configs/desktop.webpack.config.ts b/packages/suite-build/configs/desktop.webpack.config.ts index 1653a530560..9ee7fd3eb9b 100644 --- a/packages/suite-build/configs/desktop.webpack.config.ts +++ b/packages/suite-build/configs/desktop.webpack.config.ts @@ -1,4 +1,4 @@ -import CopyPlugin from 'copy-webpack-plugin'; +import CopyWebpackPlugin from 'copy-webpack-plugin'; import HtmlWebpackPlugin from 'html-webpack-plugin'; import path from 'path'; import webpack from 'webpack'; @@ -25,7 +25,7 @@ const config: webpack.Configuration = { path: path.join(baseDir, 'build'), }, plugins: [ - new CopyPlugin({ + new CopyWebpackPlugin({ patterns: ['bin', 'fonts', 'images', 'videos', 'guide/assets'] .map(dir => ({ from: path.join(__dirname, '..', '..', 'suite-data', 'files', dir), diff --git a/packages/suite-build/configs/web.webpack.config.ts b/packages/suite-build/configs/web.webpack.config.ts index a56660b2716..06a866a7503 100644 --- a/packages/suite-build/configs/web.webpack.config.ts +++ b/packages/suite-build/configs/web.webpack.config.ts @@ -1,4 +1,4 @@ -import CopyPlugin from 'copy-webpack-plugin'; +import CopyWebpackPlugin from 'copy-webpack-plugin'; import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'; import HtmlWebpackPlugin from 'html-webpack-plugin'; import path from 'path'; @@ -17,7 +17,7 @@ const config: webpack.Configuration = { path: path.join(baseDir, 'build'), }, plugins: [ - new CopyPlugin({ + new CopyWebpackPlugin({ patterns: ['browser-detection', 'fonts', 'images', 'oauth', 'videos', 'guide/assets'] .map(dir => ({ from: path.join(__dirname, '..', '..', 'suite-data', 'files', dir), From 38daebb20c3d18c34a5ce641df5f8dc1aa714586 Mon Sep 17 00:00:00 2001 From: Martin Varmuza Date: Tue, 11 Feb 2025 10:29:58 +0100 Subject: [PATCH 19/20] fix(suite-web): start using local sharedworker on localhost and production --- packages/suite-build/configs/web.webpack.config.ts | 11 +++++++++++ suite-common/connect-init/src/connectInitThunks.ts | 9 ++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/suite-build/configs/web.webpack.config.ts b/packages/suite-build/configs/web.webpack.config.ts index 06a866a7503..60a028257bb 100644 --- a/packages/suite-build/configs/web.webpack.config.ts +++ b/packages/suite-build/configs/web.webpack.config.ts @@ -40,6 +40,17 @@ const config: webpack.Configuration = { concurrency: 100, }, }), + new CopyWebpackPlugin({ + patterns: [ + { + from: path.join( + __dirname, + '../../connect-iframe/build/workers/sessions-background-sharedworker.js', + ), + to: path.join(baseDir, 'build', 'static'), + }, + ], + }), // Html files ...routes.map( route => diff --git a/suite-common/connect-init/src/connectInitThunks.ts b/suite-common/connect-init/src/connectInitThunks.ts index a96a96e1098..39129ffbd33 100644 --- a/suite-common/connect-init/src/connectInitThunks.ts +++ b/suite-common/connect-init/src/connectInitThunks.ts @@ -118,13 +118,13 @@ export const connectInitThunk = createThunk( // ====================================================== ==================== ==================== // localhost:8000 localhost:8088 NO // https://dev.suite.sldev.cz/suite-web/develop/web/ https://dev.suite.sldev.cz/connect/develop/ YES - connect - // suite.trezor.io/web connect.trezor.io/9(x.y)/ YES - connect + // suite.trezor.io/web connect.trezor.io/9(x.y)/ NO let _sessionsBackgroundUrl: string | null = null; - + const localSharedWorker = '/static/sessions-background-sharedworker.js'; if (typeof window !== 'undefined' && !isNative()) { if (window.location.origin.includes('localhost')) { - _sessionsBackgroundUrl = null; + _sessionsBackgroundUrl = localSharedWorker; } else if (window.location.origin.endsWith('dev.suite.sldev.cz')) { // we are expecting accompanying connect build at specified location const assetPrefixArr = (process.env.ASSET_PREFIX || '').split('/').filter(Boolean); @@ -141,8 +141,7 @@ export const connectInitThunk = createThunk( _sessionsBackgroundUrl = `${window.location.origin}/${relevantSegments.join('/')}/workers/sessions-background-sharedworker.js`; } else { - _sessionsBackgroundUrl = - 'https://connect.trezor.io/9/workers/sessions-background-sharedworker.js'; + _sessionsBackgroundUrl = localSharedWorker; } } From 0ac67a21fa4f15bc6a84523df0d3f49ce195d892 Mon Sep 17 00:00:00 2001 From: Martin Varmuza Date: Tue, 11 Feb 2025 11:40:19 +0100 Subject: [PATCH 20/20] wip: crazy idea for pinging from sharedworker --- packages/connect/src/device/DeviceList.ts | 29 +++++----- .../src/sessions/background-browser.ts | 2 + packages/transport/src/sessions/background.ts | 14 +++++ packages/transport/src/sessions/client.ts | 2 + packages/transport/src/sessions/types.ts | 1 + .../transport/src/transports/abstractApi.ts | 3 +- packages/transport/src/transports/bridge.ts | 53 +++++++++++++++++-- 7 files changed, 82 insertions(+), 22 deletions(-) diff --git a/packages/connect/src/device/DeviceList.ts b/packages/connect/src/device/DeviceList.ts index 4d1ec05ad67..cec2f74e812 100644 --- a/packages/connect/src/device/DeviceList.ts +++ b/packages/connect/src/device/DeviceList.ts @@ -400,23 +400,18 @@ export class DeviceList extends TypedEmitter implements IDevic } private scheduleUpgradeCheck(apiType: TransportApiType, initParams: InitParams) { - clearTimeout(this.scheduledUpgradeChecks[apiType]); - this.scheduledUpgradeChecks[apiType] = setTimeout(async () => { - const transport = this.transport[apiType]; - const transports = this.transports.filter(t => t.apiType === apiType); - if (!transport || transport === transports[0]) return; - for (const t of transports) { - if (t === transport) break; - if (await t.ping()) { - this.transportLock(apiType, 'Upgrading', signal => - this.createInitPromise(apiType, initParams, signal), - ).catch(() => {}); - - return; - } - } - this.scheduleUpgradeCheck(apiType, initParams); - }, 1000); + this.scheduledUpgradeChecks[apiType]; + const transport = this.transport[apiType]; + const transports = this.transports.filter(t => t.apiType === apiType); + if (!transport || transport === transports[0]) return; + for (const t of transports) { + if (t === transport) break; + t.on('upgrade-available', () => { + this.transportLock(apiType, 'Upgrading', signal => + this.createInitPromise(apiType, initParams, signal), + ).catch(() => {}); + }); + } } private async selectTransport( diff --git a/packages/transport/src/sessions/background-browser.ts b/packages/transport/src/sessions/background-browser.ts index a63bf9e79c2..1ff3316727d 100644 --- a/packages/transport/src/sessions/background-browser.ts +++ b/packages/transport/src/sessions/background-browser.ts @@ -24,6 +24,7 @@ export class BrowserSessionsBackground implements SessionsBackgroundInterface { return new Promise(resolve => { const onmessage = (message: MessageEvent) => { + console.log('on message', message); if (params.id === message.data.id) { resolve(message.data); background.port.removeEventListener('message', onmessage); @@ -42,6 +43,7 @@ export class BrowserSessionsBackground implements SessionsBackgroundInterface { }); } + on(event: 'upgrade-available', listener: () => void): void; on(event: 'descriptors', listener: (descriptors: Descriptor[]) => void): void; on(event: 'releaseRequest', listener: (descriptor: Descriptor) => void): void; on(event: 'descriptors' | 'releaseRequest', listener: (descriptors: any) => void): void { diff --git a/packages/transport/src/sessions/background.ts b/packages/transport/src/sessions/background.ts index c061f244783..6bf34d54221 100644 --- a/packages/transport/src/sessions/background.ts +++ b/packages/transport/src/sessions/background.ts @@ -44,6 +44,7 @@ export class SessionsBackground */ descriptors: Descriptor[]; releaseRequest: Descriptor; + ['upgrade-available']: void; }> implements SessionsBackgroundInterface { @@ -130,6 +131,19 @@ export class SessionsBackground } private handshake() { + // check localhost:21235 periodically using POST request every 1 second. + // if it return 200, emit 'upgrade-available' event + // now write the code!! + setInterval(async () => { + const response = await fetch('http://127.0.0.1:21325/', { + method: 'POST', + }); + console.log('ping response', response); + if (response.status === 200) { + this.emit('upgrade-available'); + } + }, 1000); + return this.success(undefined); } diff --git a/packages/transport/src/sessions/client.ts b/packages/transport/src/sessions/client.ts index 99d96095983..cf2078d89e4 100644 --- a/packages/transport/src/sessions/client.ts +++ b/packages/transport/src/sessions/client.ts @@ -19,6 +19,7 @@ import { export class SessionsClient extends TypedEmitter<{ descriptors: Descriptor[]; releaseRequest: Descriptor; + 'upgrade-available': void; }> { // used only for debugging - discriminating sessions clients in sessions background log private caller = getWeakRandomId(3); @@ -40,6 +41,7 @@ export class SessionsClient extends TypedEmitter<{ this.background = background; background.on('descriptors', descriptors => this.emit('descriptors', descriptors)); background.on('releaseRequest', descriptor => this.emit('releaseRequest', descriptor)); + background.on('upgrade-available', () => this.emit('upgrade-available')); } private request(params: M) { diff --git a/packages/transport/src/sessions/types.ts b/packages/transport/src/sessions/types.ts index 98010c1f1c5..ccb2a99e6a0 100644 --- a/packages/transport/src/sessions/types.ts +++ b/packages/transport/src/sessions/types.ts @@ -115,6 +115,7 @@ export type HandleMessageResponse

= P extends { type: infer T } export interface SessionsBackgroundInterface { on(event: 'descriptors', listener: (descriptors: Descriptor[]) => void): void; on(event: 'releaseRequest', listener: (descriptor: Descriptor) => void): void; + on(event: 'upgrade-available', listener: () => void): void; handleMessage(message: M): Promise>; dispose(): void; } diff --git a/packages/transport/src/transports/abstractApi.ts b/packages/transport/src/transports/abstractApi.ts index 1e9283c650b..1439edc1d39 100644 --- a/packages/transport/src/transports/abstractApi.ts +++ b/packages/transport/src/transports/abstractApi.ts @@ -41,6 +41,7 @@ export abstract class AbstractApiTransport extends AbstractTransport { return this.scheduleAction( async () => { const handshakeRes = await this.sessionsClient.handshake(); + console.log('handshakeRes', handshakeRes); this.stopped = !handshakeRes.success; return handshakeRes; @@ -109,7 +110,7 @@ export abstract class AbstractApiTransport extends AbstractTransport { const { path } = input; const acquireIntentResponse = await this.sessionsClient.acquireIntent(input); - + console.log('acquireIntentResponse', acquireIntentResponse); if (!acquireIntentResponse.success) { return this.error({ error: acquireIntentResponse.error }); } diff --git a/packages/transport/src/transports/bridge.ts b/packages/transport/src/transports/bridge.ts index 82d9fa35f73..451114454d8 100644 --- a/packages/transport/src/transports/bridge.ts +++ b/packages/transport/src/transports/bridge.ts @@ -24,6 +24,10 @@ import * as bridgeApiResult from '../utils/bridgeApiResult'; import { createProtocolMessage } from '../utils/bridgeProtocolMessage'; import { receiveAndParse } from '../utils/receive'; import { buildMessage } from '../utils/send'; +import { SessionsBackground } from '../sessions/background'; +import { SessionsClient } from '../sessions/client'; +import { SessionsBackgroundInterface } from '../sessions/types'; +import { BrowserSessionsBackground } from '../sessions/background-browser'; const DEFAULT_URL = 'http://127.0.0.1:21325'; @@ -79,23 +83,64 @@ export class BridgeTransport extends AbstractTransport { public name = 'BridgeTransport' as const; public apiType = 'usb' as const; + protected sessionsClient: SessionsClient; + protected sessionsBackground: SessionsBackgroundInterface; constructor(params: BridgeConstructorParameters) { const { url = DEFAULT_URL, latestVersion, ...rest } = params || {}; super(rest); this.url = url; this.latestVersion = latestVersion; + console.log(params); + this.sessionsBackgroundUrl = params.sessionsBackgroundUrl || 'meow'; + this.sessionsBackground = new SessionsBackground(); + this.sessionsClient = new SessionsClient(this.sessionsBackground); + + console.log('bridge sessionsBackgroundUrl', this.sessionsBackground); + + console.log('bridge sessionsClient', this.sessionsClient); + + this.sessionsClient.on('descriptors', descriptors => { + console.log('bridge transport descriptors', descriptors); + }); + this.sessionsClient.on('upgrade-available', () => { + console.log('bridge client caught upgrade available'); + this.emit('upgrade-available', true); + }); } - ping({ signal }: AbstractTransportMethodParams<'ping'> = {}) { - return this.scheduleAction(signal => this.post('/', { signal }), { signal }) - .then(({ success }) => success) - .catch(() => false); + private async trySetSessionsBackground() { + if (!this.sessionsBackgroundUrl) { + this.logger?.log( + 'No sessionsBackgroundUrl provided. Falling back to use local module.', + ); + + return; + } + try { + const response = await fetch(this.sessionsBackgroundUrl, { method: 'HEAD' }); + if (!response.ok) { + console.warn( + `Failed to fetch sessions-background SharedWorker from url: ${this.sessionsBackgroundUrl}`, + ); + } else { + this.sessionsBackground = new BrowserSessionsBackground(this.sessionsBackgroundUrl); + // sessions client initiated with a request fn facilitating communication with a session backend (shared worker in case of webusb) + this.sessionsClient.setBackground(this.sessionsBackground); + } + } catch (err) { + console.warn( + 'Unable to load background-sharedworker. Falling back to use local module. Say bye bye to tabs synchronization. Error details: ', + err.message, + ); + } } public init({ signal }: AbstractTransportMethodParams<'init'> = {}) { return this.scheduleAction( async signal => { + await this.trySetSessionsBackground(); + const response = await this.post('/', { signal, });