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

transport - fixes and more testing scenarios #13493

Merged
merged 6 commits into from
Aug 13, 2024
Merged
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
22 changes: 14 additions & 8 deletions .github/workflows/test-transport.yml
Original file line number Diff line number Diff line change
@@ -9,22 +9,20 @@ on:
pull_request:
paths:
- "packages/transport/**"
- "packages/transport-bridge/**"
- "packages/transport-test/**"
- "packages/protobuf/**"
- "packages/protocol/**"
- "packages/trezor-user-env-link/**"
- "packages/utils/**"
- "docker/docker-transport-test.sh"
- "docker/docker-compose.transport-test.yml"
- ".github/workflows/connect-transport-e2e-test.yml"
- "docker/docker-compose.transport-test-ci.yml"
- "yarn.lock"
workflow_dispatch:

jobs:
transport-e2e:
if: github.repository == 'trezor/trezor-suite'
runs-on: ubuntu-latest
env:
COMPOSE_FILE: ./docker/docker-compose.transport-test.yml
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -39,7 +37,15 @@ jobs:
- name: Install dependencies
run: |
echo -e "\nenableScripts: false" >> .yarnrc.yml
yarn workspaces focus @trezor/transport
yarn workspaces focus @trezor/transport-test
- name: Run E2E tests
run: ./docker/docker-transport-test.sh
- name: Setup containers
run: |
docker compose -f ./docker/docker-compose.transport-test-ci.yml pull
docker compose -f ./docker/docker-compose.transport-test-ci.yml up -d
- name: Run E2E tests (old-bridge:emu)
run: yarn workspace @trezor/transport-test test:e2e:old-bridge:emu

- name: Run E2E tests (new-bridge:emu)
run: yarn workspace @trezor/transport-test test:e2e:new-bridge:emu
14 changes: 14 additions & 0 deletions docker/docker-compose.transport-test-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
version: "3.9"
services:
trezor-user-env-unix:
image: ghcr.io/trezor/trezor-user-env
environment:
- SDL_VIDEODRIVER=dummy
- XDG_RUNTIME_DIR=/var/tmp
network_mode: host
# in case local developement on mac is needed, these ports will be useful
# ports:
# - "9002:9002"
# - "9001:9001"
# - "21326:21326"
# - "21325:21326"
15 changes: 0 additions & 15 deletions docker/docker-compose.transport-test.yml

This file was deleted.

5 changes: 0 additions & 5 deletions docker/docker-transport-test.sh

This file was deleted.

25 changes: 13 additions & 12 deletions packages/connect-popup/e2e/tests/transport.test.ts
Original file line number Diff line number Diff line change
@@ -72,18 +72,19 @@ const fixtures = [
timeout: 10000,
}),
},
{
browser: chromium,
description: `iframe and host different origins: auto mode -> popup`,
queryString: `?trezor-connect-src=${simulatedCrossOrigin}&core-mode=auto`,
before: handleSimulatedCrossOrigin,
expect: async () => {
await expect(popup.getByText('Connect Trezor to continue').first()).toBeVisible({
timeout: 30000,
});
await expect(page.locator('iframe')).not.toBeAttached();
},
},
// todo: fails when there are changes in session shared worker https://github.com/trezor/trezor-suite/issues/13762
// {
// browser: chromium,
// description: `iframe and host different origins: auto mode -> popup`,
// queryString: `?trezor-connect-src=${simulatedCrossOrigin}&core-mode=auto`,
// before: handleSimulatedCrossOrigin,
// expect: async () => {
// await expect(popup.getByText('Connect Trezor to continue').first()).toBeVisible({
// timeout: 30000,
// });
// await expect(page.locator('iframe')).not.toBeAttached();
// },
// },
{
browser: chromium,
description: `iframe blocked -> fallback to popup`,
4 changes: 3 additions & 1 deletion packages/transport-bridge/src/core.ts
Original file line number Diff line number Diff line change
@@ -109,6 +109,7 @@ export const createCore = (apiArg: 'usb' | 'udp' | AbstractApi, logger?: Log) =>
};

const enumerate = async ({ signal }: { signal: AbortSignal }) => {
await sessionsClient.enumerateIntent();
const enumerateResult = await api.enumerate(signal);

if (!enumerateResult.success) {
@@ -255,8 +256,9 @@ export const createCore = (apiArg: 'usb' | 'udp' | AbstractApi, logger?: Log) =>
};

const dispose = () => {
abortController.abort();
api.dispose();
sessionsClient.removeAllListeners('descriptors');
sessionsClient.dispose();
};

return {
1 change: 0 additions & 1 deletion packages/transport-bridge/src/http.ts
Original file line number Diff line number Diff line change
@@ -295,7 +295,6 @@ export class TrezordNode {

app.post('/read/:session', [
validateSessionParams,
parseBodyJSON,
(req, res) => {
const signal = this.createAbortSignal(res);
this.core.receive({ session: req.params.session, signal }).then(result => {
5 changes: 5 additions & 0 deletions packages/transport-test/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
rules: {
'no-nested-ternary': 'off', // useful in tests..
},
};
10 changes: 10 additions & 0 deletions packages/transport-test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
This package contains a collection of end-to-end tests of the transport layer using both emulators and real devices.

### Bridge tests

| | needs tenv | manually connect device | manually start bridge |
| ------------------------------------------------------------- | ---------- | ----------------------- | --------------------- |
| yarn workspace @trezor/transport-test test:e2e:old-bridge:hw | no | yes | yes |
| yarn workspace @trezor/transport-test test:e2e:old-bridge:emu | yes | no | no |
| yarn workspace @trezor/transport-test test:e2e:new-bridge:hw | no | yes | no |
| yarn workspace @trezor/transport-test test:e2e:new-bridge:emu | yes | no | no |
175 changes: 175 additions & 0 deletions packages/transport-test/e2e/controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
/* eslint no-console: 0 */

import { WebUSB } from 'usb';

import { TrezorUserEnvLinkClass } from '@trezor/trezor-user-env-link';
import { scheduleAction, Log } from '@trezor/utils';
import { TrezordNode } from '@trezor/transport-bridge/src';

export const env = {
USE_HW: process.env.USE_HW === 'true',
USE_NODE_BRIDGE: process.env.USE_NODE_BRIDGE === 'true',
};

console.log('env', env);

const webusb = new WebUSB({
allowAllDevices: true, // return all devices, not only authorized
});

/**
* Controller based on TrezorUserEnvLink its main purpose is:
* - to bypass communication with trezor-user-env and allow using local hw devices
* - start and stop node bridge (node bridge should be implemented into trezor-user-env however)
*/
class Controller extends TrezorUserEnvLinkClass {
private logger: Console;
private nodeBridge: TrezordNode | undefined = undefined;

private originalApi: {
connect: typeof TrezorUserEnvLinkClass.prototype.connect;
startBridge: typeof TrezorUserEnvLinkClass.prototype.startBridge;
stopBridge: typeof TrezorUserEnvLinkClass.prototype.stopBridge;
startEmu: typeof TrezorUserEnvLinkClass.prototype.startEmu;
stopEmu: typeof TrezorUserEnvLinkClass.prototype.stopEmu;
};

constructor() {
super();

this.logger = console;

this.originalApi = {
connect: super.connect.bind(this),
startBridge: super.startBridge.bind(this),
stopBridge: super.stopBridge.bind(this),
startEmu: super.startEmu.bind(this),
stopEmu: super.stopEmu.bind(this),
};

this.connect = !env.USE_HW
? this.originalApi.connect
: () => {
return Promise.resolve(null);
};

this.startBridge =
!env.USE_HW && !env.USE_NODE_BRIDGE
? (version?: string) => this.originalApi.startBridge(version)
: env.USE_NODE_BRIDGE
? async () => {
this.nodeBridge = new TrezordNode({
port: 21325,
api: !env.USE_HW ? 'udp' : 'usb',
logger: new Log('test-bridge', false),
});

await this.nodeBridge.start();

// todo: this shouldn't be here, nodeBridge should be started when start resolves
await this.waitForBridgeIsRunning(true);

return null;
}
: () => this.waitForBridgeIsRunning(true);

this.stopBridge =
!env.USE_HW && !env.USE_NODE_BRIDGE
? this.originalApi.stopBridge
: env.USE_NODE_BRIDGE
? async () => {
await this.nodeBridge?.stop();

// todo: this shouldn't be here, nodeBridge should be stopped when stop resolves
await this.waitForBridgeIsRunning(false);

return null;
}
: () => this.waitForBridgeIsRunning(false);

this.startEmu = !env.USE_HW
? this.originalApi.startEmu
: env.USE_HW
? () => this.waitForNumberOfDevices(1)
: () => {
console.log('todo: ping local emu');

return Promise.resolve(null);
};

this.stopEmu = !env.USE_HW
? this.originalApi.stopEmu
: env.USE_HW
? () => this.waitForNumberOfDevices(0)
: () => {
console.log('todo: ping local emu');

return Promise.resolve(null);
};
}

private waitForNumberOfDevices = (expected: number) => {
this.logger.log(
`${env.USE_HW ? '[MANUAL ACTION REQUIRED] ' : ''} waiting for ${expected} device to be connected`,
);

return scheduleAction(
async () => {
const devices = await webusb.getDevices();
if (devices.filter(d => d.productName === 'TREZOR').length === expected) {
return null;
}
throw new Error('Condition not met');
},
{
deadline: Date.now() + 60_000,
gap: 1000,
},
);
};

private waitForBridgeIsRunning = (expected: boolean) => {
this.logger.log(
`${env.USE_HW && !env.USE_NODE_BRIDGE ? '[MANUAL ACTION REQUIRED] ' : ''} waiting for bridge ${expected ? 'start' : 'stop'}`,
);

return scheduleAction(
() => {
return fetch('http://localhost:21325/', {
method: 'POST',
headers: {
['Origin']: 'https://wallet.trezor.io',
},
})
.then(res => {
if (res.ok !== expected) {
throw new Error('Condition not met');
}

return res.text();
})
.then((text: string) => {
console.log(`running bridge: ${text}`);

return null;
})
.catch(err => {
if (err.message === 'Condition not met') {
throw err;
}
if (expected) {
throw err;
}

return null;
});
},
{
deadline: Date.now() + 60_000,
gap: 1000,
},
);
};
}

export const controller = new Controller();
26 changes: 26 additions & 0 deletions packages/transport-test/e2e/expect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { DEVICE_TYPE } from '@trezor/transport/src/api/abstract';

import { env } from './controller';

const { USE_HW, USE_NODE_BRIDGE } = env;

const debug = USE_NODE_BRIDGE ? undefined : USE_HW ? false : true;
const debugSession = USE_NODE_BRIDGE ? undefined : null;
const path = USE_NODE_BRIDGE ? expect.any(String) : '1';
const product = USE_HW ? 21441 : USE_NODE_BRIDGE ? undefined : 0;
const vendor = USE_NODE_BRIDGE ? undefined : USE_HW ? 4617 : 0;
const type =
USE_NODE_BRIDGE && USE_HW
? expect.any(Number)
: USE_NODE_BRIDGE && !USE_HW
? DEVICE_TYPE.TypeEmulator
: undefined;

/**
* emu '127.0.0.1:21324' (15)
* hw old bridge '1' (1)
* hw new bridge '185B982B5F37F9D96706EC49' (24)
*/
export const pathLength = USE_HW && USE_NODE_BRIDGE ? 24 : !USE_HW && USE_NODE_BRIDGE ? 15 : 1;

export const descriptor = { debug, debugSession, path, product, vendor, type };
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export default {
import type { Config } from 'jest';

export const config: Config = {
rootDir: './',
moduleFileExtensions: ['ts', 'js'],
modulePathIgnorePatterns: ['node_modules'],
@@ -20,3 +22,6 @@ export default {
testEnvironment: 'node',
globals: {},
};

// eslint-disable-next-line import/no-default-export
export default config;
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import { runCLI } from 'jest';
import { Config } from '@jest/types';

import { TrezorUserEnvLink } from '@trezor/trezor-user-env-link';

import argv from './jest.config';
import { controller as TrezorUserEnvLink } from './controller';
import { config } from './jest.config';

(async () => {
// Before actual tests start, establish connection with trezor-user-env
await TrezorUserEnvLink.connect();

// @ts-expect-error
argv.runInBand = true;
const argv: Config.Argv = {
...config,
runInBand: true,
testPathPattern: [process.argv[2] || ''],
};

// @ts-expect-error
const { results } = await runCLI(argv, [__dirname]);

process.exit(results.numFailedTests);
Original file line number Diff line number Diff line change
@@ -1,21 +1,12 @@
import * as messages from '@trezor/protobuf/messages.json';
import { TrezorUserEnvLink } from '@trezor/trezor-user-env-link';
import { BridgeTransport } from '@trezor/transport';

import { BridgeTransport } from '../../src';
import { controller as TrezorUserEnvLink } from '../controller';
import { pathLength, descriptor as expectedDescriptor } from '../expect';

// todo: introduce global jest config for e2e
jest.setTimeout(60000);

const mnemonicAll = 'all all all all all all all all all all all all';

const emulatorSetupOpts = {
mnemonic: mnemonicAll,
pin: '',
passphrase_protection: false,
label: 'TrezorT',
needs_backup: true,
};

const emulatorStartOpts = { version: '2-main', wipe: true };

describe('bridge', () => {
@@ -24,40 +15,38 @@ describe('bridge', () => {
});

afterAll(async () => {
await TrezorUserEnvLink.send({ type: 'emulator-stop' });
await TrezorUserEnvLink.send({ type: 'bridge-stop' });
await TrezorUserEnvLink.stopEmu();
await TrezorUserEnvLink.stopBridge();
TrezorUserEnvLink.disconnect();
});

let bridge: any;
let devices: any[];
let session: any;
beforeEach(async () => {
// todo: swapping emulator-stop and bridge-stop line can simulate "emulator process died" error
await TrezorUserEnvLink.send({ type: 'emulator-stop' });
await TrezorUserEnvLink.send({ type: 'bridge-stop' });
await TrezorUserEnvLink.send({ type: 'emulator-start', ...emulatorStartOpts });
await TrezorUserEnvLink.send({ type: 'emulator-setup', ...emulatorSetupOpts });
await TrezorUserEnvLink.send({ type: 'bridge-start' });
// await TrezorUserEnvLink.stopEmu();
await TrezorUserEnvLink.stopBridge();
await TrezorUserEnvLink.startEmu(emulatorStartOpts);
await TrezorUserEnvLink.startBridge();
const abortController = new AbortController();
bridge = new BridgeTransport({ messages, signal: abortController.signal });
await bridge.init().promise;

const enumerateResult = await bridge.enumerate().promise;
expect(enumerateResult).toEqual({
expect(enumerateResult).toMatchObject({
success: true,
payload: [
{
path: '1',
path: expect.any(String),
session: null,
product: 0,
vendor: 0,
// we don't use it but bridge returns
debug: true,
debugSession: null,
product: expectedDescriptor.product,
},
],
});

const { path } = enumerateResult.payload[0];
expect(path.length).toEqual(pathLength);

devices = enumerateResult.payload;

const acquireResult = await bridge.acquire({ input: { path: devices[0].path } }).promise;
@@ -70,13 +59,13 @@ describe('bridge', () => {

test(`call(GetFeatures)`, async () => {
const message = await bridge.call({ session, name: 'GetFeatures', data: {} }).promise;

expect(message).toMatchObject({
success: true,
payload: {
type: 'Features',
message: {
vendor: 'trezor.io',
label: 'TrezorT',
},
},
});
@@ -93,35 +82,35 @@ describe('bridge', () => {
type: 'Features',
message: {
vendor: 'trezor.io',
label: 'TrezorT',
},
},
});
});

test(`call(ChangePin) - send(Cancel) - receive`, async () => {
// initiate change pin procedure on device
const callResponse = await bridge.call({ session, name: 'ChangePin', data: {} }).promise;
test(`call(RebootToBootloader) - send(Cancel) - receive`, async () => {
// initiate RebootToBootloader procedure on device (it works regardless device is wiped or not)
const callResponse = await bridge.call({ session, name: 'RebootToBootloader', data: {} })
.promise;
expect(callResponse).toMatchObject({
success: true,
payload: {
type: 'ButtonRequest',
},
});

// cancel change pin procedure
// cancel RebootToBootloader procedure
const sendResponse = await bridge.send({ session, name: 'Cancel', data: {} }).promise;
expect(sendResponse).toEqual({ success: true, payload: undefined });

// receive response
const receiveResponse = await bridge.receive({ session }).promise;

expect(receiveResponse).toMatchObject({
success: true,
payload: {
type: 'Failure',
message: {
code: 'Failure_ActionCancelled',
message: 'Cancelled',
},
},
});
@@ -138,48 +127,6 @@ describe('bridge', () => {
type: 'Features',
message: {
vendor: 'trezor.io',
label: 'TrezorT',
},
},
});
});

test(`call(Backup) - send(Cancel) - receive`, async () => {
// initiate backup procedure on device
const callResponse = await bridge.call({ session, name: 'BackupDevice', data: {} }).promise;
expect(callResponse).toMatchObject({
success: true,
payload: {
type: 'ButtonRequest',
},
});

// cancel backup procedure
const sendResponse = await bridge.send({ session, name: 'Cancel', data: {} }).promise;
expect(sendResponse).toEqual({ success: true });

// receive response
const receiveResponse = await bridge.receive({ session }).promise;
expect(receiveResponse).toMatchObject({
success: true,
payload: {
type: 'Failure',
message: {
code: 'Failure_ActionCancelled',
message: 'Cancelled',
},
},
});

// validate that we can continue with communication
const message = await bridge.call({ session, name: 'GetFeatures', data: {} }).promise;
expect(message).toMatchObject({
success: true,
payload: {
type: 'Features',
message: {
vendor: 'trezor.io',
label: 'TrezorT',
},
},
});
Original file line number Diff line number Diff line change
@@ -1,35 +1,22 @@
import * as messages from '@trezor/protobuf/messages.json';
import { TrezorUserEnvLink } from '@trezor/trezor-user-env-link';
import { BridgeTransport, Descriptor } from '@trezor/transport';

import { BridgeTransport, Descriptor } from '../../src';
import { controller as TrezorUserEnvLink } from '../controller';
import { descriptor as fixtureDescriptor } from '../expect';

// todo: introduce global jest config for e2e
jest.setTimeout(60000);

const mnemonicAll = 'all all all all all all all all all all all all';

const emulatorSetupOpts = {
mnemonic: mnemonicAll,
pin: '',
passphrase_protection: false,
label: 'TrezorT',
needs_backup: true,
};

const wait = () =>
const wait = (ms = 1000) =>
new Promise(resolve => {
setTimeout(() => {
resolve(undefined);
}, 1000);
}, ms);
});

const getDescriptor = (descriptor: any): Descriptor => ({
debug: true,
debugSession: null,
path: '1',
product: 0,
...fixtureDescriptor,
session: '1',
vendor: 0,
...descriptor,
});

@@ -41,30 +28,10 @@ describe('bridge', () => {

let descriptors: any[];

beforeAll(async () => {
await TrezorUserEnvLink.connect();
});

afterAll(async () => {
await TrezorUserEnvLink.send({ type: 'emulator-stop' });
await TrezorUserEnvLink.send({ type: 'bridge-stop' });
TrezorUserEnvLink.disconnect();
});

beforeEach(async () => {
await TrezorUserEnvLink.send({ type: 'emulator-stop' });
await TrezorUserEnvLink.send({ type: 'bridge-stop' });
await TrezorUserEnvLink.send({ type: 'emulator-start', ...emulatorStartOpts });
await TrezorUserEnvLink.send({ type: 'emulator-setup', ...emulatorSetupOpts });
await TrezorUserEnvLink.send({ type: 'bridge-start' });

const abortController = new AbortController();
bridge1 = new BridgeTransport({ messages, signal: abortController.signal });
bridge2 = new BridgeTransport({ messages, signal: abortController.signal });

await bridge1.init().promise;
await bridge2.init().promise;

/**
* set bridge1 and bridge2 descriptors and start listening
*/
const enumerateAndListen = async () => {
const result = await bridge1.enumerate().promise;
expect(result.success).toBe(true);

@@ -73,29 +40,49 @@ describe('bridge', () => {
}

expect(descriptors).toEqual([
{
path: '1',
getDescriptor({
session: null,
product: 0,
vendor: 0,
// we don't use it but bridge returns
debug: true,
debugSession: null,
},
}),
]);

bridge1.handleDescriptorsChange(descriptors);
bridge2.handleDescriptorsChange(descriptors);

bridge1.listen();
bridge2.listen();
};
beforeAll(async () => {
await TrezorUserEnvLink.connect();
});

afterAll(async () => {
await TrezorUserEnvLink.stopEmu();
await TrezorUserEnvLink.stopBridge();
TrezorUserEnvLink.disconnect();
});

beforeEach(async () => {
await TrezorUserEnvLink.stopBridge();
await TrezorUserEnvLink.startEmu(emulatorStartOpts);
await TrezorUserEnvLink.startBridge();

const abortController = new AbortController();
bridge1 = new BridgeTransport({ messages, signal: abortController.signal });
bridge2 = new BridgeTransport({ messages, signal: abortController.signal });

await bridge1.init().promise;
await bridge2.init().promise;
});

test('2 clients. one acquires and releases, the other one is watching', async () => {
await enumerateAndListen();

const bride1spy = jest.spyOn(bridge1, 'emit');
const bride2spy = jest.spyOn(bridge2, 'emit');

const session1 = await bridge1.acquire({ input: { previous: null, path: '1' } }).promise;
const session1 = await bridge1.acquire({
input: { previous: null, path: descriptors[0].path },
}).promise;
expect(session1).toEqual({
success: true,
payload: '1',
@@ -105,7 +92,7 @@ describe('bridge', () => {
await wait();

const expectedDescriptor1 = getDescriptor({
path: '1',
path: descriptors[0].path,
session: '1',
});

@@ -141,12 +128,12 @@ describe('bridge', () => {
return;
}

await bridge1.release({ path: '1', session: session1.payload }).promise;
await bridge1.release({ path: descriptors[0].path, session: session1.payload }).promise;

await wait();

const expectedDescriptor2 = getDescriptor({
path: '1',
path: descriptors[0].path,
session: null,
});

@@ -178,15 +165,21 @@ describe('bridge', () => {
releasedElsewhere: [expectedDescriptor2], // difference here
});

const session2 = await bridge2.acquire({ input: { previous: null, path: '1' } }).promise;
const session2 = await bridge2.acquire({
input: { previous: null, path: descriptors[0].path },
}).promise;
expect(session2).toEqual({ success: true, payload: '2' });
});

test('session can be "stolen" by another client', async () => {
await enumerateAndListen();

const bride1spy = jest.spyOn(bridge1, 'emit');
const bride2spy = jest.spyOn(bridge2, 'emit');

const session1 = await bridge1.acquire({ input: { previous: null, path: '1' } }).promise;
const session1 = await bridge1.acquire({
input: { previous: null, path: descriptors[0].path },
}).promise;

expect(session1).toEqual({ success: true, payload: '1' });
if (!session1.success) {
@@ -197,13 +190,13 @@ describe('bridge', () => {

// bridge 2 steals session
const session2 = await bridge2.acquire({
input: { previous: session1.payload, path: '1' },
input: { previous: session1.payload, path: descriptors[0].path },
}).promise;

expect(session2).toEqual({ success: true, payload: '2' });

const expectedDescriptor = getDescriptor({
path: '1',
path: descriptors[0].path,
session: '2',
});

@@ -237,4 +230,22 @@ describe('bridge', () => {
releasedElsewhere: [],
});
});

test('2 clients enumerate at the same time', async () => {
const promise1 = bridge1.enumerate().promise;
const promise2 = bridge2.enumerate().promise;

const results = await Promise.all([promise1, promise2]);

expect(results).toEqual([
{
success: true,
payload: [getDescriptor({ session: null })],
},
{
success: true,
payload: [getDescriptor({ session: null })],
},
]);
});
});
27 changes: 27 additions & 0 deletions packages/transport-test/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "@trezor/transport-test",
"version": "1.0.0",
"private": true,
"license": "See LICENSE.md in repo root",
"sideEffects": false,
"scripts": {
"depcheck": "yarn g:depcheck",
"lint:js": "yarn g:eslint '**/*.{ts,tsx,js}'",
"type-check": "yarn g:tsc --build",
"test:e2e": "ts-node -O '{\"module\": \"commonjs\", \"esModuleInterop\": true}' ./e2e/run.ts",
"test:e2e:old-bridge:hw": "USE_HW=true USE_NODE_BRIDGE=false yarn test:e2e",
"test:e2e:old-bridge:emu": "USE_HW=false USE_NODE_BRIDGE=false yarn test:e2e",
"test:e2e:new-bridge:hw": "USE_HW=true USE_NODE_BRIDGE=true yarn test:e2e",
"test:e2e:new-bridge:emu": "USE_HW=false USE_NODE_BRIDGE=true yarn test:e2e"
},
"devDependencies": {
"@jest/types": "^29.6.3",
"@trezor/transport": "workspace:*",
"@trezor/transport-bridge": "workspace:*",
"@trezor/trezor-user-env-link": "workspace:^",
"@trezor/utils": "workspace:*",
"jest": "^29.7.0",
"ts-node": "^10.9.1",
"usb": "^2.11.0"
}
}
10 changes: 10 additions & 0 deletions packages/transport-test/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": { "outDir": "libDev" },
"references": [
{ "path": "../transport" },
{ "path": "../transport-bridge" },
{ "path": "../trezor-user-env-link" },
{ "path": "../utils" }
]
}
3 changes: 1 addition & 2 deletions packages/transport/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
const { testPathIgnorePatterns, ...baseConfig } = require('../../jest.config.base');
const { ...baseConfig } = require('../../jest.config.base');

module.exports = {
...baseConfig,
testEnvironment: 'node',
collectCoverage: true,
collectCoverageFrom: ['src/**/*.ts'],
testPathIgnorePatterns: [...testPathIgnorePatterns, 'e2e'],
watchPathIgnorePatterns: ['<rootDir>/libDev', '<rootDir>/lib'],
};
1 change: 0 additions & 1 deletion packages/transport/package.json
Original file line number Diff line number Diff line change
@@ -47,7 +47,6 @@
"build:lib": "yarn g:rimraf -rf lib && yarn g:tsc --build tsconfig.lib.json && ../../scripts/replace-imports.sh ./lib",
"publish:lib": "./scripts/publish-lib.sh",
"test:unit": "jest",
"test:e2e": "ts-node -O '{\"module\": \"commonjs\", \"esModuleInterop\": true}' ./e2e/run.ts",
"prepublishOnly": "yarn tsx ../../scripts/prepublishNPM.js",
"prepublish": "yarn tsx ../../scripts/prepublish.js"
},
21 changes: 21 additions & 0 deletions packages/transport/src/sessions/background.ts
Original file line number Diff line number Diff line change
@@ -69,6 +69,9 @@ export class SessionsBackground extends TypedEmitter<{
case 'handshake':
result = this.handshake();
break;
case 'enumerateIntent':
await this.enumerateIntent();
break;
case 'enumerateDone':
result = await this.enumerateDone(message.payload);
break;
@@ -90,6 +93,9 @@ export class SessionsBackground extends TypedEmitter<{
case 'getPathBySession':
result = this.getPathBySession(message.payload);
break;
case 'dispose':
this.dispose();
break;
default:
throw new Error(ERRORS.UNEXPECTED_ERROR);
}
@@ -116,11 +122,19 @@ export class SessionsBackground extends TypedEmitter<{
return this.success(undefined);
}

private async enumerateIntent() {
await this.waitInQueue();

return this.success(undefined);
}

/**
* enumerate done
* - caller informs about current descriptors
*/
private enumerateDone(payload: EnumerateDoneRequest) {
this.clearLock();

const disconnectedDevices = this.filterDisconnectedDevices(
Object.values(this.descriptors),
payload.descriptors.map(d => d.path), // which paths are occupied paths after last interface read
@@ -300,4 +314,11 @@ export class SessionsBackground extends TypedEmitter<{
error,
};
}

dispose() {
this.locksQueue.forEach(lock => clearTimeout(lock.id));
this.locksTimeoutQueue.forEach(timeout => clearTimeout(timeout));
this.descriptors = {};
this.lastSessionId = 0;
}
}
9 changes: 8 additions & 1 deletion packages/transport/src/sessions/client.ts
Original file line number Diff line number Diff line change
@@ -54,7 +54,9 @@ export class SessionsClient extends TypedEmitter<{
handshake() {
return this.request({ type: 'handshake' });
}

enumerateIntent() {
return this.request({ type: 'enumerateIntent' });
}
enumerateDone(payload: EnumerateDoneRequest) {
return this.request({ type: 'enumerateDone', payload });
}
@@ -76,4 +78,9 @@ export class SessionsClient extends TypedEmitter<{
getPathBySession(payload: GetPathBySessionRequest) {
return this.request({ type: 'getPathBySession', payload });
}
dispose() {
this.removeAllListeners('descriptors');

return this.request({ type: 'dispose' });
}
}
2 changes: 2 additions & 0 deletions packages/transport/src/sessions/types.ts
Original file line number Diff line number Diff line change
@@ -83,13 +83,15 @@ export type Params = {

export interface HandleMessageApi {
handshake: () => HandshakeResponse;
enumerateIntent: () => BackgroundResponse<void>;
enumerateDone: (payload: EnumerateDoneRequest) => EnumerateDoneResponse;
acquireIntent: (payload: AcquireIntentRequest) => AcquireIntentResponse;
acquireDone: (payload: AcquireDoneRequest) => AcquireDoneResponse;
releaseIntent: (payload: ReleaseIntentRequest) => ReleaseIntentResponse;
releaseDone: (payload: ReleaseDoneRequest) => ReleaseDoneResponse;
getSessions: () => GetSessionsResponse;
getPathBySession: (payload: GetPathBySessionRequest) => GetPathBySessionResponse;
dispose: () => BackgroundResponse<void>;
}

type UnwrapParams<T, Fn> = Fn extends () => any
3 changes: 3 additions & 0 deletions packages/transport/src/transports/abstractApi.ts
Original file line number Diff line number Diff line change
@@ -70,6 +70,9 @@ export abstract class AbstractApiTransport extends AbstractTransport {

public enumerate() {
return this.scheduleAction(async signal => {
// todo: consider doing await this.sessionsClient.enumerateIntent() here
// it looks like Webusb does not need it is not needed at least on macos.

// enumerate usb api
const enumerateResult = await this.api.enumerate(signal);

8 changes: 5 additions & 3 deletions packages/trezor-user-env-link/src/api.ts
Original file line number Diff line number Diff line change
@@ -52,7 +52,7 @@ interface ReadAndConfirmShamirMnemonicEmu {
threshold: number;
}

class TrezorUserEnvLinkClass extends TypedEmitter<WebsocketClientEvents> {
export class TrezorUserEnvLinkClass extends TypedEmitter<WebsocketClientEvents> {
private client: WebsocketClient;
public firmwares?: Firmwares;
private defaultFirmware?: string;
@@ -136,14 +136,16 @@ class TrezorUserEnvLinkClass extends TypedEmitter<WebsocketClientEvents> {

return null;
}
startEmu(arg?: StartEmu) {
async startEmu(arg?: StartEmu) {
const params = {
type: 'emulator-start',
version: this.defaultFirmware || '2-main',
...arg,
};

return this.client.send(params);
await this.client.send(params);

return null;
}
startEmuFromUrl({ url, model, wipe }: StartEmuFromUrl) {
return this.client.send({
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -171,6 +171,7 @@
{ "path": "packages/transport" },
{ "path": "packages/transport-bridge" },
{ "path": "packages/transport-native" },
{ "path": "packages/transport-test" },
{
"path": "packages/trezor-user-env-link"
},
17 changes: 16 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
@@ -11603,6 +11603,21 @@ __metadata:
languageName: unknown
linkType: soft

"@trezor/transport-test@workspace:packages/transport-test":
version: 0.0.0-use.local
resolution: "@trezor/transport-test@workspace:packages/transport-test"
dependencies:
"@jest/types": "npm:^29.6.3"
"@trezor/transport": "workspace:*"
"@trezor/transport-bridge": "workspace:*"
"@trezor/trezor-user-env-link": "workspace:^"
"@trezor/utils": "workspace:*"
jest: "npm:^29.7.0"
ts-node: "npm:^10.9.1"
usb: "npm:^2.11.0"
languageName: unknown
linkType: soft

"@trezor/transport@workspace:*, @trezor/transport@workspace:packages/transport":
version: 0.0.0-use.local
resolution: "@trezor/transport@workspace:packages/transport"
@@ -11628,7 +11643,7 @@ __metadata:
languageName: unknown
linkType: soft

"@trezor/trezor-user-env-link@workspace:*, @trezor/trezor-user-env-link@workspace:packages/trezor-user-env-link":
"@trezor/trezor-user-env-link@workspace:*, @trezor/trezor-user-env-link@workspace:^, @trezor/trezor-user-env-link@workspace:packages/trezor-user-env-link":
version: 0.0.0-use.local
resolution: "@trezor/trezor-user-env-link@workspace:packages/trezor-user-env-link"
dependencies: