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 |
| if (pending) { | ||
| pending.resolve(); | ||
| } | ||
| } else if (response.type === 'APDU_ERROR') { |
There was a problem hiding this comment.
WebHID mock leaks pending exchanges on frame ACKs
Low Severity
The HID_FRAME_ACK handler resolves the pending promise but never calls pendingExchanges.delete(response.id), unlike the HID_EXCHANGE_COMPLETE and APDU_ERROR handlers which both clean up. For multi-frame APDUs, intermediate frame entries accumulate in the pendingExchanges map indefinitely, leaking memory in the browser context over repeated signing operations.
Reviewed by Cursor Bugbot for commit e3611c6. Configure here.
|
@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 |
|
@metamaskbot publish-preview |
|
Preview builds have been published. See these instructions (from the Expand for full list of packages and versions. |
| checksums: Record<string, string>, | ||
| ): string | undefined { | ||
| return checksums[archivePath]; | ||
| } |
There was a problem hiding this comment.
getBundledChecksum uses full path instead of filename
Low Severity
getBundledChecksum looks up checksums using the full archivePath as a key, but checksums.json uses bare filenames (e.g. speculos-v0.25.13-linux-amd64.tar.gz). This means the lookup would always return undefined when given a full path. The function is also exported but never called anywhere in the codebase — the actual checksum verification in verifyBundledChecksum correctly extracts the filename with .split('/').pop().
Reviewed by Cursor Bugbot for commit 8f2d47b. Configure here.
| - arch: arm64 | ||
| # PyInstaller bundles the host arch; arm64 must build on arm64. | ||
| runner: macos-12.0 | ||
| file_arch: 'ARM aarch64' |
There was a problem hiding this comment.
CI workflow arm64 build uses wrong runner platform
Medium Severity
The arm64 matrix entry uses macos-12.0 as the runner, but macos-12 runners are no longer available on GitHub Actions (they were x86_64 Intel when they existed). Even with a valid ARM macOS runner, PyInstaller on macOS produces Mach-O binaries, not Linux ELF binaries. The archive is then named linux-arm64 on line 70, which is incorrect for any macOS-built binary. The verification step would always fail since file output won't contain 'ARM aarch64' for a macOS binary. This workflow can never successfully produce arm64 artifacts.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 08e9687. Configure here.
| - arch: arm64 | ||
| # PyInstaller bundles the host arch; arm64 must build on arm64. | ||
| runner: macos-12.0 | ||
| file_arch: 'ARM aarch64' |
There was a problem hiding this comment.
CI arm64 job uses wrong runner platform
High Severity
The arm64 build job uses runner: macos-12.0, which is an Intel-based macOS runner (now deprecated since Dec 2024). PyInstaller on macOS produces Mach-O binaries, not Linux ELF binaries, yet the archive is named linux-arm64. The file_arch verification for "ARM aarch64" would also fail since macOS binaries report as "Mach-O". This job cannot produce a valid Linux ARM64 binary — it needs a Linux ARM64 runner.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 0ab7c8c. Configure here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 5 total unresolved issues (including 4 from previous reviews).
❌ 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.
| 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 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 test-only surface (APDU/signing bridge, deterministic seeds, Docker/native lifecycle) with no direct production keyring changes, but mistakes in E2E signing mocks could mask real Ledger regressions.
Overview
Adds Ledger Speculos–based hardware wallet emulation for client E2E tests via two new packages and release tooling.
@metamask/hw-emulatorexposescreateEmulator(EmulatorType.Ledger, …)with Docker (default off Linux) or native Linux runs, bundled Ethereum ELF apps anddocker-compose, APDU/REST client, screen automation (Nano buttons vs Stax/Flex touch), a WebSocket ApduBridge plus WebHID mock script for browser tests, and optional integration with@metamask/speculos-upwhen nobinaryis set. Docker mode now honors custom ports, seed, and display via compose env vars. Trezor is stubbed (not implemented).@metamask/speculos-upinstalls/caches Linuxspeculosbinaries (bundledtar.gz+ checksums or GitHubspeculos-v*releases), symlinks intonode_modules/.bin, and ships maintainer Docker/native build scripts plus abuild-speculosGitHub workflow (linux-amd64 PyInstaller → release assets).Monorepo README / tsconfig wire in the new packages;
.gitignoreaddsdist-build/and.worktrees/.Reviewed by Cursor Bugbot for commit de887b2. Bugbot is set up for automated code reviews on this repo. Configure here.