Skip to content

Commit 1d7fbaf

Browse files
authored
🔖 (release) [NO-ISSUE]: New release incoming: Signer SOL 1.5.1, Signer ETH 1.9.4, Context Module 1.11.1, Signer Utils 1.1.1, Transport Speculos 1.1.2, Transport Web BLE 1.3.1, Transport RN BLE 1.3.1, Transport Mockserver 1.0.1, Transport Web HID 1.2.2, Transport RN HID 1.0.2, DMK 0.13.0 (#1186)
2 parents 7dde26b + abbb0e1 commit 1d7fbaf

51 files changed

Lines changed: 878 additions & 221 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/clear-signing-tester/README.md‎

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,26 @@ Ethereum Transaction Tester CLI - Clean Architecture Edition
4141

4242
Options:
4343
-V, --version output the version number
44-
--derivation-path <path> Derivation path (default: "44'/60'/0'/0/0") (default: "44'/60'/0'/0/0")
45-
--speculos-url <url> Speculos server URL (default: http://localhost) (default: "http://localhost")
44+
45+
# @config.speculos
46+
--speculos-url <url> Speculos server URL (default: http://localhost)
4647
--speculos-port <port> Speculos server port (random port if not provided)
47-
--device <device> Device type (stax, nanox, nanos, nanos+, flex, apex, default: stax) (default: "stax")
48-
--app-eth-version <version> Ethereum app version (e.g., 1.19.1). If not specified, automatically resolves the latest available version for the device.
49-
--os-version <version> Device OS version (e.g., 1.8.1). If not specified, automatically resolves the latest available OS version for the device.
50-
--plugin <plugin> Plugin to use (e.g., Paraswap). If not specified, uses no plugin.
51-
--plugin-version <version> Plugin version to use. If not specified, automatically resolves the latest available version.
5248
--docker-image-tag <tag> Docker image tag for Speculos (default: latest)
53-
--verbose, -v Enable verbose output (default: false)
54-
--quiet, -q Show only result tables (quiet mode) (default: false)
49+
--device <device> Device type (stax, nanox, nanos, nanos+, flex, apex, default: stax)
50+
--app-eth-version <version> Ethereum app version (e.g., 1.19.1). If not specified, uses latest version for the device.
51+
--os-version <version> Device OS version (e.g., 1.8.1). If not specified, uses latest OS version for the device.
52+
--plugin <plugin> Plugin to use (e.g., Paraswap). If not specified, uses no plugin.
53+
--plugin-version <version> Plugin version to use. If not specified, uses latest version.
54+
--screenshot-folder-path <path> Save screenshots to a folder during transaction signing
55+
56+
# @config.signer
57+
--derivation-path <path> Derivation path (default: "44'/60'/0'/0/0")
58+
59+
# @config.logger
60+
--log-level <level> Console log level: none, error, warn, info, debug (default: info)
61+
--log-file <path> Log output to a file
62+
--file-log-level <level> File log level: none, error, warn, info, debug (requires --log-file)
63+
5564
-h, --help display help for command
5665

5766
Commands:
@@ -154,6 +163,46 @@ Options:
154163
- `--chain-id <number>`: Chain id to use (default to 1)
155164
- `--skip-cal`: Skip CAL (Crypto Asset List) filtering and fetch random transactions directly from Etherscan instead of only CAL-registered transactions
156165
166+
### Logging
167+
168+
The tester supports configurable logging levels for both console and file output:
169+
170+
```bash
171+
# Default: console at info level
172+
pnpm cs-tester cli raw-file ./ressources/raw-erc20.json
173+
174+
# Verbose console output (debug level)
175+
pnpm cs-tester cli --log-level debug raw-file ./ressources/raw-erc20.json
176+
177+
# Quiet mode (errors only)
178+
pnpm cs-tester cli --log-level error raw-file ./ressources/raw-erc20.json
179+
180+
# Log to file with debug level (console stays at info)
181+
pnpm cs-tester cli --log-file ./output.log --file-log-level debug raw-file ./ressources/raw-erc20.json
182+
183+
# Silent console, verbose file
184+
pnpm cs-tester cli --log-level none --log-file ./debug.log --file-log-level debug raw-file ./ressources/raw-erc20.json
185+
```
186+
187+
**Log Levels:**
188+
189+
- `none` - No logging
190+
- `error` - Errors only
191+
- `warn` - Errors and warnings
192+
- `info` - Errors, warnings, and info (default)
193+
- `debug` - All messages including debug
194+
195+
### Screenshots
196+
197+
Save screenshots of each screen during transaction signing:
198+
199+
```bash
200+
# Save screenshots to a folder
201+
pnpm cs-tester cli --screenshot-folder-path ./screenshots raw-file ./ressources/raw-erc20.json
202+
```
203+
204+
Screenshots are saved as `screenshot_1.png`, `screenshot_2.png`, etc. in the specified folder.
205+
157206
### Plugin Support
158207
159208
The tester supports running Ethereum transactions with plugins (e.g., Paraswap, 1inch, etc.). When a plugin is specified, Speculos will run the plugin app with the Ethereum app loaded as a library using the `-l` flag.

‎apps/clear-signing-tester/src/cli/EthereumTransactionTesterCli.ts‎

Lines changed: 104 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
#!/usr/bin/env node
22

3-
import { ConsoleLogger, LogLevel } from "@ledgerhq/device-management-kit";
43
import { Command } from "commander";
54
import { type Container } from "inversify";
65

@@ -13,22 +12,39 @@ import { type TestTypedDataUseCase } from "@root/src/application/usecases/TestTy
1312
import { makeContainer } from "@root/src/di/container";
1413
import { type ClearSigningTesterConfig } from "@root/src/di/modules/configModuleFactory";
1514
import { TYPES } from "@root/src/di/types";
15+
import {
16+
CLI_LOG_LEVELS,
17+
type CliLogLevel,
18+
} from "@root/src/domain/models/config/LoggerConfig";
1619
import { type SpeculosConfig } from "@root/src/domain/models/config/SpeculosConfig";
1720
import { type ServiceController } from "@root/src/domain/services/ServiceController";
1821

1922
export type CliConfig = {
20-
derivationPath: string;
23+
// config.speculos
2124
speculosUrl: string;
2225
speculosPort: number;
23-
verbose: boolean;
24-
quiet: boolean;
26+
speculosVncPort?: number;
27+
dockerImageTag?: string;
2528
device: SpeculosConfig["device"];
2629
appEthVersion?: SpeculosConfig["version"];
2730
osVersion?: SpeculosConfig["os"];
2831
plugin?: string;
2932
pluginVersion?: string;
33+
screenshotFolderPath?: string;
34+
35+
// config.signer
3036
skipCal?: boolean;
31-
dockerImageTag?: string;
37+
38+
// config.logger
39+
logLevel: CliLogLevel;
40+
logFile?: string;
41+
fileLogLevel?: CliLogLevel;
42+
43+
// extras (not in config section but used by CLI)
44+
derivationPath: string;
45+
46+
// Start Speculos only (skip DMK initialization)
47+
onlySpeculos?: boolean;
3248
};
3349

3450
/**
@@ -46,27 +62,22 @@ export class EthereumTransactionTesterCli {
4662
constructor(config: CliConfig) {
4763
this.config = config;
4864

49-
const logger = new ConsoleLogger(
50-
config.quiet
51-
? LogLevel.Error
52-
: config.verbose
53-
? LogLevel.Debug
54-
: LogLevel.Info,
55-
);
56-
5765
const randomPort = Math.floor(Math.random() * 10000) + 10000;
66+
const randomVncPort = Math.floor(Math.random() * 10000) + 20000;
5867

5968
// Create DI container configuration
6069
const diConfig: ClearSigningTesterConfig = {
6170
speculos: {
6271
url: config.speculosUrl || `http://localhost`,
6372
port: config.speculosPort || randomPort,
73+
vncPort: config.speculosVncPort || randomVncPort,
6474
dockerImageTag: config.dockerImageTag || "latest",
6575
device: config.device,
6676
os: config.osVersion,
6777
version: config.appEthVersion,
6878
plugin: config.plugin,
6979
pluginVersion: config.pluginVersion,
80+
screenshotPath: config.screenshotFolderPath,
7081
},
7182
signer: {
7283
originToken: process.env["GATING_TOKEN"] || "test-origin-token",
@@ -78,12 +89,24 @@ export class EthereumTransactionTesterCli {
7889
apps: {
7990
path: process.env["COIN_APPS_PATH"] || "",
8091
},
92+
onlySpeculos: config.onlySpeculos,
8193
};
8294

8395
// Create DI container and resolve tester
8496
this.container = makeContainer({
8597
config: diConfig,
86-
loggers: [logger],
98+
logger: {
99+
cli: {
100+
level: config.logLevel,
101+
},
102+
file: config.logFile
103+
? {
104+
// default to cli log level if file log level is not specified,
105+
level: config.fileLogLevel || config.logLevel,
106+
filePath: config.logFile,
107+
}
108+
: undefined,
109+
},
87110
});
88111

89112
this.controller = this.container.get<ServiceController>(
@@ -143,6 +166,17 @@ export class EthereumTransactionTesterCli {
143166
return port;
144167
},
145168
)
169+
.option(
170+
"--speculos-vnc-port <port>",
171+
"Speculos VNC port (random port if not provided)",
172+
(value: string) => {
173+
const port = parseInt(value);
174+
if (isNaN(port) || port < 1 || port > 65535) {
175+
throw new Error("Invalid port number");
176+
}
177+
return port;
178+
},
179+
)
146180
.option(
147181
"--device <device>",
148182
"Device type (stax, nanox, nanos, nanos+, flex, apex, default: stax)",
@@ -187,8 +221,36 @@ export class EthereumTransactionTesterCli {
187221
"Docker image tag for Speculos (default: latest)",
188222
"latest",
189223
)
190-
.option("--verbose, -v", "Enable verbose output", false)
191-
.option("--quiet, -q", "Show only result tables (quiet mode)", false);
224+
.option(
225+
"--screenshot-folder-path <path>",
226+
"Save screenshots to a folder during transaction signing",
227+
)
228+
.option(
229+
"--log-level <level>",
230+
`Console log level: ${CLI_LOG_LEVELS.join(", ")} (default: info)`,
231+
(value: string) => {
232+
if (!CLI_LOG_LEVELS.includes(value as CliLogLevel)) {
233+
throw new Error(
234+
`Invalid log level '${value}'. Must be one of: ${CLI_LOG_LEVELS.join(", ")}`,
235+
);
236+
}
237+
return value as CliLogLevel;
238+
},
239+
"info" as CliLogLevel,
240+
)
241+
.option("--log-file <path>", "Log output to a file")
242+
.option(
243+
"--file-log-level <level>",
244+
`File log level: ${CLI_LOG_LEVELS.join(", ")} (requires --log-file)`,
245+
(value: string) => {
246+
if (!CLI_LOG_LEVELS.includes(value as CliLogLevel)) {
247+
throw new Error(
248+
`Invalid log level '${value}'. Must be one of: ${CLI_LOG_LEVELS.join(", ")}`,
249+
);
250+
}
251+
return value as CliLogLevel;
252+
},
253+
);
192254

193255
// Set up signal handlers that work with the CLI instance
194256
const handleShutdown = async (signal: string) => {
@@ -197,11 +259,13 @@ export class EthereumTransactionTesterCli {
197259
process.exit(0);
198260
};
199261

200-
process.on("SIGINT", () => handleShutdown("SIGINT"));
201-
process.on("SIGTERM", () => handleShutdown("SIGTERM"));
262+
process.once("SIGINT", () => handleShutdown("SIGINT"));
263+
process.once("SIGTERM", () => handleShutdown("SIGTERM"));
202264

203265
program.hook("preAction", async (_, command) => {
204266
const config = command.parent!.opts() as CliConfig;
267+
// Set onlySpeculos flag when running start-speculos command
268+
config.onlySpeculos = command.name() === "start-speculos";
205269
cli = new EthereumTransactionTesterCli(config);
206270
await cli.initialize();
207271
});
@@ -280,6 +344,16 @@ export class EthereumTransactionTesterCli {
280344
exitCode = await cli!.handleContractFile(file, options.skipCal);
281345
});
282346

347+
// Start Speculos command (no signing tests)
348+
program
349+
.command("start-speculos")
350+
.description(
351+
"Start Speculos emulator without running any signing tests. Press Ctrl+C to stop.",
352+
)
353+
.action(async () => {
354+
await cli!.handleStartSpeculos();
355+
});
356+
283357
return program;
284358
}
285359

@@ -418,6 +492,18 @@ export class EthereumTransactionTesterCli {
418492

419493
return result.exitCode;
420494
}
495+
496+
/**
497+
* Handle start-speculos command
498+
* Starts Speculos and keeps it running until interrupted
499+
*/
500+
async handleStartSpeculos(): Promise<void> {
501+
console.log("\nSpeculos is running.");
502+
console.log("Press Ctrl+C to stop.\n");
503+
504+
// Wait indefinitely until interrupted
505+
await new Promise<void>(() => {});
506+
}
421507
}
422508

423509
// Main execution when run directly

‎apps/clear-signing-tester/src/di/container.ts‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { type LoggerSubscriberService } from "@ledgerhq/device-management-kit";
21
import { Container } from "inversify";
32

3+
import { type LoggerConfig } from "@root/src/domain/models/config/LoggerConfig";
4+
45
import { applicationModuleFactory } from "./modules/applicationModuleFactory";
56
import {
67
type ClearSigningTesterConfig,
@@ -11,20 +12,20 @@ import { loggerModuleFactory } from "./modules/loggerModuleFactory";
1112

1213
type MakeContainerArgs = {
1314
config: ClearSigningTesterConfig;
14-
loggers?: LoggerSubscriberService[];
15+
logger: LoggerConfig;
1516
};
1617

1718
export const makeContainer = ({
1819
config,
19-
loggers = [],
20+
logger,
2021
}: MakeContainerArgs): Container => {
2122
const container = new Container();
2223

2324
container.loadSync(
2425
configModuleFactory(config),
2526
infrastructureModuleFactory(config),
2627
applicationModuleFactory(),
27-
loggerModuleFactory({ subscribers: loggers }),
28+
loggerModuleFactory({ config: logger }),
2829
);
2930

3031
return container;

‎apps/clear-signing-tester/src/di/modules/configModuleFactory.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export type ClearSigningTesterConfig = {
1111
signer: SignerConfig;
1212
etherscan: EtherscanConfig;
1313
apps: AppsConfig;
14+
onlySpeculos?: boolean;
1415
};
1516

1617
export const configModuleFactory = (config: ClearSigningTesterConfig) =>

‎apps/clear-signing-tester/src/di/modules/infrastructureModuleFactory.ts‎

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { type EtherscanAdapter } from "@root/src/domain/adapters/EtherscanAdapte
99
import { type FileReader } from "@root/src/domain/adapters/FileReader";
1010
import { type JsonParser } from "@root/src/domain/adapters/JsonParser";
1111
import { type ScreenReader } from "@root/src/domain/adapters/ScreenReader";
12+
import { type ScreenshotSaver } from "@root/src/domain/adapters/ScreenshotSaver";
1213
import { type TransactionCrafter } from "@root/src/domain/adapters/TransactionCrafter";
1314
import { type ContractInput } from "@root/src/domain/models/ContractInput";
1415
import { type TransactionInput } from "@root/src/domain/models/TransactionInput";
@@ -28,6 +29,7 @@ import { EthersTransactionCrafter } from "@root/src/infrastructure/adapters/evm/
2829
import { HttpCalAdapter } from "@root/src/infrastructure/adapters/external/HttpCalAdapter";
2930
import { HttpEtherscanAdapter } from "@root/src/infrastructure/adapters/external/HttpEtherscanAdapter";
3031
import { SpeculosScreenReader } from "@root/src/infrastructure/adapters/speculos/SpeculosScreenReader";
32+
import { SpeculosScreenshotSaver } from "@root/src/infrastructure/adapters/speculos/SpeculosScreenshotSaver";
3133
import { NodeDockerContainer } from "@root/src/infrastructure/adapters/system/NodeDockerContainer";
3234
import { NodeFileReader } from "@root/src/infrastructure/adapters/system/NodeFileReader";
3335
import { NodeJsonParser } from "@root/src/infrastructure/adapters/system/NodeJsonParser";
@@ -113,17 +115,27 @@ export const infrastructureModuleFactory = (config: ClearSigningTesterConfig) =>
113115

114116
// Service Controllers Array (ordered for startup/shutdown)
115117
bind<ServiceController[]>(TYPES.ServiceControllers)
116-
.toDynamicValue((context) => [
117-
// Start order: Speculos -> DMK
118-
context.get<ServiceController>(TYPES.SpeculosServiceController),
119-
context.get<ServiceController>(TYPES.DMKServiceController),
120-
])
118+
.toDynamicValue((context) => {
119+
const controllers: ServiceController[] = [
120+
context.get<ServiceController>(TYPES.SpeculosServiceController),
121+
];
122+
// Only add DMK controller if not in onlySpeculos mode
123+
if (!config.onlySpeculos) {
124+
controllers.push(
125+
context.get<ServiceController>(TYPES.DMKServiceController),
126+
);
127+
}
128+
return controllers;
129+
})
121130
.inSingletonScope();
122131

123132
// Adapters
124133
bind<ScreenReader>(TYPES.ScreenReader)
125134
.to(SpeculosScreenReader)
126135
.inSingletonScope();
136+
bind<ScreenshotSaver>(TYPES.ScreenshotSaver)
137+
.to(SpeculosScreenshotSaver)
138+
.inSingletonScope();
127139
bind<FileReader>(TYPES.FileReader).to(NodeFileReader).inSingletonScope();
128140
bind<JsonParser>(TYPES.JsonParser).to(NodeJsonParser).inSingletonScope();
129141
bind<DockerContainer>(TYPES.DockerContainer)

0 commit comments

Comments
 (0)