feat: hardware wallet emulators - #560
Conversation
…ages - @metamask/speculos: Speculos emulator lifecycle, transport, and device interaction - Speculos class with start/stop lifecycle (auto-detect Docker vs native) - SpeculosClient for TCP APDU and REST API communication - ApduBridge WebSocket HID relay for browser E2E testing - DockerManager and ProcessManager for container/native binary lifecycle - DeviceInteraction handlers for button (Nano) and touch (Stax/Flex) - WebHID mock script for browser injection - Bundled ELF apps (nanosp, nanox, stax, flex), NVRAM, docker-compose.yml - @metamask/speculos-up: Binary downloader for native Speculos - ensureBinary() with SHA-256 checksum verification - Platform detection (linux-x64, linux-arm64, darwin-arm64) - CLI entry point for standalone setup
- Add explicit return types to all arrow functions and callbacks - Replace negated conditions with positive equivalents - Fix JSDoc param names and descriptions for fingerTap/fingerSwipe/connectWithResilience - Replace void IIFEs with proper async method extraction (apdu-bridge #handleMessage) - Use nullish coalescing assignment (??=) where appropriate - Wrap async callbacks in void-safe wrappers for setTimeout (no-misused-promises) - Replace resolves.toBeUndefined patterns with direct await + assertion - Add toThrow messages and increase timeouts for delay-based tests - Use type assertions instead of non-null assertions for Record lookups - Remove unused imports and suppress inherent Node.js rule violations in tests - Prune eslint-suppressions.json to only permanent suppressions
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
Warning MetaMask internal reviewing guidelines:
|
|
@metamaskbot publish-preview |
|
@metamaskbot publish-preview |
|
Preview builds have been published. See these instructions (from the Expand for full list of packages and versions. |
|
@metamaskbot publish-preview |
|
@metamaskbot publish-preview |
|
Preview builds have been published. See these instructions (from the Expand for full list of packages and versions. |
| cacheDir, | ||
| `speculos-${version}-${String(platform)}-${resolvedArch}`, | ||
| ); | ||
| } |
There was a problem hiding this comment.
Cache path mismatch breaks binary path resolution
High Severity
getInstallDir() computes a human-readable cache path like speculos-${version}-${platform}-${arch}, but downloadAndInstall() stores binaries under a SHA-256 hash of speculos-v${version}-${platform}-${arch}. These produce completely different directory names, so getSpeculosBinaryPath() and isSpeculosInstalled() will always return null/false even after a successful install. This also breaks the hw-emulator integration that calls getSpeculosBinaryPath() to auto-resolve the binary.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit de887b2. Configure here.
|
@metamaskbot publish-preview |
|
Preview builds have been published. See these instructions (from the Expand for full list of packages and versions. |
|
This PR is marked as stale because it has been open for 60 days with no activity. Please remove the stale label or leave a comment, or it will be closed in 14 days. |
|
This PR was closed due to no follow-up activity in the last 14 days. Thank you for your contributions. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 5 potential issues.
There are 6 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit de887b2. Configure here.
| return cachedPackageDir; | ||
| } catch { | ||
| return undefined; | ||
| } |
There was a problem hiding this comment.
CLI skips bundled Speculos archives
High Severity
resolvePackageDir() uses require.resolve, which is not available in ESM. The published CLI is dist/cli.mjs, so the lookup always fails closed and downloadAndInstall() never uses the pre-packaged archives. The CLI then tries a GitHub download instead of the offline bundled binaries.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit de887b2. Configure here.
| continue; | ||
| } | ||
| const target = join(file.parentPath, file.name); | ||
| const filePath = join(binDir, relative(cachePath, target)); |
There was a problem hiding this comment.
Install uses unsupported Dirent API
Medium Severity
installBinaries() reads file.parentPath on each dirent. That property exists only on Node 18.20+ and 20.12+, but the package engines allow ^18.18 || >=20. On those earlier supported versions parentPath is undefined and path.join throws, so install crashes after a successful extract.
Reviewed by Cursor Bugbot for commit de887b2. Configure here.
| config.deviceModel, | ||
| ); | ||
|
|
||
| this.#started = true; |
There was a problem hiding this comment.
Failed start leaks running emulator
High Severity
start() launches Docker or the native process before connecting, and only sets #started after connectWithRetry succeeds. If the connect step fails, stop() returns immediately because #started is still false, leaving the container or process running. DockerManager.start() also resets status to idle on health-check timeout without tearing the container down.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit de887b2. Configure here.
| getNvramPath(): string { | ||
| // eslint-disable-next-line no-restricted-globals | ||
| return `${__dirname}/../../nvram/main_nvram.bin`; | ||
| } |
There was a problem hiding this comment.
Native mode never loads bundled NVRAM
Medium Severity
getNvramPath() is never used when starting native Speculos. The process manager only passes --load-nvram with no file path or cwd, so the bundled nvram/main_nvram.bin is not loaded. Touch-device blind signing, which is treated as pre-enabled via NVRAM, will not be active in native mode.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit de887b2. Configure here.
| pip install pyinstaller | ||
|
|
||
| - name: Install speculos | ||
| run: pip install speculos==${{ github.event.inputs.version || '0.25.13' }} |
There was a problem hiding this comment.
Tag builds ignore pushed version
Medium Severity
The workflow is triggered by speculos-v* tags, but every version expression uses github.event.inputs.version || '0.25.13'. Tag pushes have no workflow inputs, so CI always builds Speculos 0.25.13 and publishes the release under speculos-v0.25.13 instead of the tag that triggered the run.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit de887b2. Configure here.


This PR introduces hardware wallet emulators to be used in clients for e2e testing. In this initial release, it only includes the ledger speculos emulator
Examples
Note
Medium Risk
Large new E2E stack (HID/APDU bridge, signing flow timing, Docker/process lifecycle) affects how Ledger flows are tested but does not change production keyring or auth code paths.
Overview
Adds Ledger Speculos–based hardware wallet emulation for client E2E tests via two new packages and release tooling.
@metamask/hw-emulatorexposescreateEmulator()with Docker (default off Linux) and native Linux modes, bundled Ethereum ELF apps and NVRAM,SpeculosClient/DockerManager/ProcessManager, screen automation for Nano and touch devices, and anApduBridgeplus injectable WebHID mock script so browser tests can talk to Speculos without real devices. Native start can resolve the Speculos binary through optional@metamask/speculos-up. Docker compose wiring now honors custom ports, seed, and display via env vars.@metamask/speculos-upinstalls managed Linux Speculos binaries (bundled archives with SHA-256 checks, GitHub release fallback, cache,mm-speculos-upCLI). Maintainers getbuild-speculos.yml(tagspeculos-v*) and local Docker/native build scripts for linux-amd64/arm64 archives.The monorepo README, root TypeScript project references, and
.gitignore(dist-build/,.worktrees/) are updated to include the new packages.Reviewed by Cursor Bugbot for commit de887b2. Bugbot is set up for automated code reviews on this repo. Configure here.