Skip to content

feat: hardware wallet emulators#560

Open
montelaidev wants to merge 31 commits into
mainfrom
feat/speculos-packages
Open

feat: hardware wallet emulators#560
montelaidev wants to merge 31 commits into
mainfrom
feat/speculos-packages

Conversation

@montelaidev
Copy link
Copy Markdown
Contributor

@montelaidev montelaidev commented May 28, 2026

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-emulator exposes createEmulator(EmulatorType.Ledger, …) with Docker (default off Linux) or native Linux runs, bundled Ethereum ELF apps and docker-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-up when no binary is set. Docker mode now honors custom ports, seed, and display via compose env vars. Trezor is stubbed (not implemented).

@metamask/speculos-up installs/caches Linux speculos binaries (bundled tar.gz + checksums or GitHub speculos-v* releases), symlinks into node_modules/.bin, and ships maintainer Docker/native build scripts plus a build-speculos GitHub workflow (linux-amd64 PyInstaller → release assets).

Monorepo README / tsconfig wire in the new packages; .gitignore adds dist-build/ and .worktrees/.

Reviewed by Cursor Bugbot for commit de887b2. Bugbot is set up for automated code reviews on this repo. Configure here.

…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
@montelaidev montelaidev self-assigned this May 28, 2026
@montelaidev montelaidev requested a review from a team as a code owner May 28, 2026 13:26
@socket-security
Copy link
Copy Markdown

socket-security Bot commented May 28, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​types/​ws@​8.18.11001007480100
Added@​ledgerhq/​devices@​7.0.7100100100100100
Added@​metamask/​speculos-up@​0.0.0-use.local100100100100100

View full report

@socket-security
Copy link
Copy Markdown

socket-security Bot commented May 28, 2026

Warning

MetaMask internal reviewing guidelines:

  • Do not ignore-all
  • Each alert has instructions on how to review if you don't know what it means. If lost, ask your Security Liaison or the supply-chain group
  • Copy-paste ignore lines for specific packages or a group of one kind with a note on what research you did to deem it safe.
    @SocketSecurity ignore npm/PACKAGE@VERSION
Action Severity Alert  (click "▶" to expand/collapse)
Warn Low
Potential code anomaly (AI signal): npm rxjs is 100.0% likely to have a medium risk anomaly

Notes: The code is a standard, non-malicious implementation of a findIndex operator in an RxJS-like library. It does not perform external I/O, data exfiltration, or privileged actions.

Confidence: 1.00

Severity: 0.60

From: ?npm/@ledgerhq/devices@7.0.7npm/rxjs@6.6.7

ℹ Read more on: This package | This alert | What is an AI-detected potential code anomaly?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: An AI system found a low-risk anomaly in this package. It may still be fine to use, but you should check that it is safe before proceeding.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/rxjs@6.6.7. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn Low
Potential code anomaly (AI signal): npm rxjs is 100.0% likely to have a medium risk anomaly

Notes: The analyzed code is a standard, benign RxJS operator (pairwise) that emits pairs of consecutive values. There is no evidence of data exfiltration, backdoors, or malicious behavior. The source map reference is normal for development artifacts.

Confidence: 1.00

Severity: 0.60

From: ?npm/@ledgerhq/devices@7.0.7npm/rxjs@6.6.7

ℹ Read more on: This package | This alert | What is an AI-detected potential code anomaly?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: An AI system found a low-risk anomaly in this package. It may still be fine to use, but you should check that it is safe before proceeding.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/rxjs@6.6.7. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn Low
Potential code anomaly (AI signal): npm rxjs is 100.0% likely to have a medium risk anomaly

Notes: The analyzed code is a standard, non-malicious implementation of a VirtualTimeScheduler used by RxJS for testing or simulated time control. No malicious behavior detected; no data leakage, backdoors, or exfiltration mechanisms present. The security posture is favorable for its intended use as a unit-test/time-simulation utility.

Confidence: 1.00

Severity: 0.60

From: ?npm/@ledgerhq/devices@7.0.7npm/rxjs@6.6.7

ℹ Read more on: This package | This alert | What is an AI-detected potential code anomaly?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: An AI system found a low-risk anomaly in this package. It may still be fine to use, but you should check that it is safe before proceeding.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/rxjs@6.6.7. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn Low
Potential code anomaly (AI signal): npm rxjs is 100.0% likely to have a medium risk anomaly

Notes: The code is a conventional, well-scoped implementation of an RxJS-like concat operator. No malicious behavior, data exfiltration, or suspicious I/O detected in this fragment. Security risk is low; malware likelihood is negligible for this isolated operator function.

Confidence: 1.00

Severity: 0.60

From: ?npm/@ledgerhq/hw-transport@6.31.4npm/@ledgerhq/hw-app-eth@6.42.2npm/@ledgerhq/types-live@6.56.0npm/rxjs@7.8.2

ℹ Read more on: This package | This alert | What is an AI-detected potential code anomaly?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: An AI system found a low-risk anomaly in this package. It may still be fine to use, but you should check that it is safe before proceeding.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/rxjs@7.8.2. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn Low
Potential code anomaly (AI signal): npm tar is 100.0% likely to have a medium risk anomaly

Notes: This module acts as a standard tar extraction wrapper using synchronous and asynchronous code paths. There is no evident malicious activity within this fragment. Security risk hinges on the behavior of the Unpack/UnpackSync implementation and how tar entries are written to disk (e.g., path traversal). No hardcoded secrets or network calls are present here. Recommend ensuring tar extraction handles path traversal and destination path sanitization in Unpack, and consider validating opt.file presence and type before streaming.

Confidence: 1.00

Severity: 0.60

From: packages/speculos-up/package.jsonnpm/@metamask/speculos-up@0.0.0-use.localnpm/tar@7.5.15

ℹ Read more on: This package | This alert | What is an AI-detected potential code anomaly?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: An AI system found a low-risk anomaly in this package. It may still be fine to use, but you should check that it is safe before proceeding.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/tar@7.5.15. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@montelaidev
Copy link
Copy Markdown
Contributor Author

montelaidev commented May 28, 2026

@metamaskbot publish-preview

Comment thread packages/hw-emulator/src/ledger/download.ts Fixed
Comment thread packages/hw-emulator/package.json Outdated
Comment thread packages/hw-emulator/src/ledger/apdu-bridge.ts Outdated
Comment thread packages/hw-emulator/docker-compose.yml Outdated
Comment thread packages/hw-emulator/docker-compose.yml Outdated
Comment thread packages/hw-emulator/src/ledger/docker-manager.ts
if (pending) {
pending.resolve();
}
} else if (response.type === 'APDU_ERROR') {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e3611c6. Configure here.

@montelaidev
Copy link
Copy Markdown
Contributor Author

@metamaskbot publish-preview

@github-actions
Copy link
Copy Markdown

Preview builds have been published. See these instructions (from the core monorepo) for more information about preview builds.

Expand for full list of packages and versions.
{
  "@metamask-previews/account-api": "1.0.4-4bf3baf",
  "@metamask-previews/hw-emulator": "0.1.0-4bf3baf",
  "@metamask-previews/hw-wallet-sdk": "0.8.0-4bf3baf",
  "@metamask-previews/keyring-api": "23.1.0-4bf3baf",
  "@metamask-previews/eth-hd-keyring": "14.1.1-4bf3baf",
  "@metamask-previews/eth-ledger-bridge-keyring": "12.1.0-4bf3baf",
  "@metamask-previews/eth-money-keyring": "3.0.0-4bf3baf",
  "@metamask-previews/eth-qr-keyring": "2.1.0-4bf3baf",
  "@metamask-previews/eth-simple-keyring": "12.0.2-4bf3baf",
  "@metamask-previews/eth-trezor-keyring": "10.1.0-4bf3baf",
  "@metamask-previews/keyring-internal-api": "11.0.1-4bf3baf",
  "@metamask-previews/keyring-internal-snap-client": "10.0.3-4bf3baf",
  "@metamask-previews/keyring-sdk": "2.1.1-4bf3baf",
  "@metamask-previews/eth-snap-keyring": "22.1.0-4bf3baf",
  "@metamask-previews/keyring-snap-client": "9.0.2-4bf3baf",
  "@metamask-previews/keyring-snap-sdk": "9.0.1-4bf3baf",
  "@metamask-previews/keyring-utils": "3.3.1-4bf3baf"
}

Comment thread packages/hw-emulator/src/ledger/apdu-bridge.ts Outdated
@montelaidev
Copy link
Copy Markdown
Contributor Author

@metamaskbot publish-preview

Comment thread packages/hw-emulator/docker-compose.yml Outdated
Comment thread packages/hw-emulator/src/ledger/constants.ts Outdated
@montelaidev
Copy link
Copy Markdown
Contributor Author

@metamaskbot publish-preview

Comment thread .github/workflows/build-speculos.yml
Comment thread packages/speculos-up/src/index.ts
@montelaidev
Copy link
Copy Markdown
Contributor Author

@metamaskbot publish-preview

@github-actions
Copy link
Copy Markdown

Preview builds have been published. See these instructions (from the core monorepo) for more information about preview builds.

Expand for full list of packages and versions.
{
  "@metamask-previews/account-api": "1.0.4-85a9ca9",
  "@metamask-previews/hw-emulator": "0.1.0-85a9ca9",
  "@metamask-previews/hw-wallet-sdk": "0.8.0-85a9ca9",
  "@metamask-previews/keyring-api": "23.1.0-85a9ca9",
  "@metamask-previews/eth-hd-keyring": "14.1.1-85a9ca9",
  "@metamask-previews/eth-ledger-bridge-keyring": "12.1.0-85a9ca9",
  "@metamask-previews/eth-money-keyring": "3.0.0-85a9ca9",
  "@metamask-previews/eth-qr-keyring": "2.1.0-85a9ca9",
  "@metamask-previews/eth-simple-keyring": "12.0.2-85a9ca9",
  "@metamask-previews/eth-trezor-keyring": "10.1.0-85a9ca9",
  "@metamask-previews/keyring-internal-api": "11.0.1-85a9ca9",
  "@metamask-previews/keyring-internal-snap-client": "10.0.3-85a9ca9",
  "@metamask-previews/keyring-sdk": "2.1.1-85a9ca9",
  "@metamask-previews/eth-snap-keyring": "22.1.0-85a9ca9",
  "@metamask-previews/keyring-snap-client": "9.0.2-85a9ca9",
  "@metamask-previews/keyring-snap-sdk": "9.0.1-85a9ca9",
  "@metamask-previews/keyring-utils": "3.3.1-85a9ca9",
  "@metamask-previews/speculosup": "0.1.0-85a9ca9"
}

checksums: Record<string, string>,
): string | undefined {
return checksums[archivePath];
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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().

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8f2d47b. Configure here.

Comment thread .github/workflows/build-speculos.yml Outdated
- arch: arm64
# PyInstaller bundles the host arch; arm64 must build on arm64.
runner: macos-12.0
file_arch: 'ARM aarch64'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 08e9687. Configure here.

Comment thread .github/workflows/build-speculos.yml Outdated
- arch: arm64
# PyInstaller bundles the host arch; arm64 must build on arm64.
runner: macos-12.0
file_arch: 'ARM aarch64'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0ab7c8c. Configure here.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 5 total unresolved issues (including 4 from previous reviews).

Fix All in Cursor

❌ 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}`,
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit de887b2. Configure here.

@montelaidev
Copy link
Copy Markdown
Contributor Author

@metamaskbot publish-preview

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 1, 2026

Preview builds have been published. See these instructions (from the core monorepo) for more information about preview builds.

Expand for full list of packages and versions.
{
  "@metamask-previews/account-api": "1.0.4-de887b2",
  "@metamask-previews/hw-emulator": "0.1.0-de887b2",
  "@metamask-previews/hw-wallet-sdk": "0.8.0-de887b2",
  "@metamask-previews/keyring-api": "23.1.0-de887b2",
  "@metamask-previews/eth-hd-keyring": "14.1.1-de887b2",
  "@metamask-previews/eth-ledger-bridge-keyring": "12.1.0-de887b2",
  "@metamask-previews/eth-money-keyring": "3.0.0-de887b2",
  "@metamask-previews/eth-qr-keyring": "2.1.0-de887b2",
  "@metamask-previews/eth-simple-keyring": "12.0.2-de887b2",
  "@metamask-previews/eth-trezor-keyring": "10.1.0-de887b2",
  "@metamask-previews/keyring-internal-api": "11.0.1-de887b2",
  "@metamask-previews/keyring-internal-snap-client": "10.0.3-de887b2",
  "@metamask-previews/keyring-sdk": "2.1.1-de887b2",
  "@metamask-previews/eth-snap-keyring": "22.1.0-de887b2",
  "@metamask-previews/keyring-snap-client": "9.0.2-de887b2",
  "@metamask-previews/keyring-snap-sdk": "9.0.1-de887b2",
  "@metamask-previews/keyring-utils": "3.3.1-de887b2",
  "@metamask-previews/speculos-up": "0.1.0-de887b2"
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants