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

@montelaidev montelaidev commented May 28, 2026

Copy link
Copy Markdown
Contributor

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-emulator exposes createEmulator() 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 an ApduBridge plus 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-up installs managed Linux Speculos binaries (bundled archives with SHA-256 checks, GitHub release fallback, cache, mm-speculos-up CLI). Maintainers get build-speculos.yml (tag speculos-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.

…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

socket-security Bot commented May 28, 2026

Copy link
Copy Markdown

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

socket-security Bot commented May 28, 2026

Copy link
Copy Markdown

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

montelaidev commented May 28, 2026

Copy link
Copy Markdown
Contributor Author

@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
Comment thread packages/hw-emulator/src/ledger/webhid-mock-script.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-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
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"
}

Comment thread packages/speculos-up/src/utils.ts
Comment thread .github/workflows/build-speculos.yml Outdated
Comment thread .github/workflows/build-speculos.yml Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

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

github-actions Bot commented Jun 1, 2026

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-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"
}

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

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.

@github-actions github-actions Bot added the Stale label Aug 1, 2026
@github-actions

Copy link
Copy Markdown

This PR was closed due to no follow-up activity in the last 14 days. Thank you for your contributions.

@github-actions github-actions Bot closed this Aug 16, 2026
@montelaidev montelaidev reopened this Sep 10, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

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 5 potential issues.

There are 6 total unresolved issues (including 1 from previous review).

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.

return cachedPackageDir;
} catch {
return undefined;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Reviewed by Cursor Bugbot for commit de887b2. Configure here.

continue;
}
const target = join(file.parentPath, file.name);
const filePath = join(binDir, relative(cachePath, target));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit de887b2. Configure here.

config.deviceModel,
);

this.#started = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Reviewed by Cursor Bugbot for commit de887b2. Configure here.

getNvramPath(): string {
// eslint-disable-next-line no-restricted-globals
return `${__dirname}/../../nvram/main_nvram.bin`;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

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' }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Reviewed by Cursor Bugbot for commit de887b2. Configure here.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants