Hardware wallet support is one of those features that looks finished right up until you try to test it at scale. A developer doesn’t have the right device model.
CI can’t click through confirmation screens. A “small” change in your signing flow breaks only on Ledger, only in one browser, only after a reconnect. The result is a familiar release pattern: hardware wallet fixes land late because they’re slow to reproduce.
A simulator is how teams make this predictable. Not a perfect emulation of secure hardware, and not a security substitute. Just a testing tool that behaves like a hardware wallet from the application’s point of view, so you can run the same flows every day and catch regressions early.
What a hardware wallet simulator should simulate
Device transport behavior your app depends on
Most integrations talk to a hardware wallet through a transport layer and an application protocol on top of it. For Ledger, the hw-transport family exists precisely to abstract those channels across environments such as WebUSB, HID, and others.
Your simulator doesn’t need to implement every transport. It needs to behave like the transport your app uses in tests, including reconnects, timeouts, and “device busy” style errors.
The APDU request and response contract
At the lowest level, many device interactions are APDU-shaped: the app sends a command and receives a structured response. Some web-era transports, such as U2F HID, explicitly frame messages using APDU encoding.
A simulator that models APDU inputs and outputs cleanly gives you a stable surface for integration tests, even if you later swap transports.
User approval prompts and “button clicks”
The pain point in real testing is rarely signature math. It’s the approval flow. Teams need a way to simulate “user reviewed details and approved” as well as “user rejected” without manual clicking.
This is one place where full emulators have a practical advantage. Ledger’s Speculos is designed to run Ledger device apps on a desktop, including UI navigation and button events. Trezor’s emulator is also explicitly positioned for development and testing, with a clear warning that it’s not hardened like physical hardware.
Deterministic keys and repeatable outputs
A testing simulator is only valuable when it is repeatable. You want the same seed, the same derivation path, and the same signatures for the same input across runs. Trezor’s emulator documentation even calls out that it uses a pseudo-random generator and should be treated as development-only.
For your simulator, determinism is a feature. It lets you write “golden tests” that assert exact outputs.
Three simulator levels that teams use in practice
A mocked transport that returns canned responses
This is the lightest option. You intercept calls at the transport boundary and return prerecorded responses for known commands, usually keyed by the command bytes and sometimes by a small amount of context like a derivation path or chain ID. It’s fast, cheap to maintain in the beginning, and ideal for unit tests that focus on your own code: request formatting, parsing, error mapping, retry behavior, and what your UI does when the wallet rejects a request.
The downside is drift. Because responses are canned, your tests can start validating your mock instead of validating reality. If the device application changes its APDU behavior, or if you accidentally change request encoding in a way that still matches your recorded fixtures, tests can remain green while real devices fail.
A protocol simulator that implements a minimal command set
This is the sweet spot for many teams because it keeps the simulator small while still behaving like a device in the ways your app actually cares about. You implement a subset of the app protocol you need (typically public key derivation, transaction signing, and message signing) and compute responses dynamically instead of hardcoding them.
This approach catches integration mistakes that mocks often miss. It validates command sequencing, it forces your application to supply required input fields, and it reveals subtle encoding bugs because the simulator can reject malformed requests the way a real device would. It’s also lightweight enough to run in CI on every pull request, which is where it delivers the most value: it makes “hardware wallet support” testable every day instead of only when someone has the physical device on hand.
A full emulator that runs the real device application
This is what tools like Speculos are for. Speculos is designed to run Ledger device apps on a desktop environment without the physical hardware, which gives you a much closer approximation of real behavior than a custom simulator can provide. Trezor’s firmware emulator serves a similar purpose for their ecosystem by running firmware logic in a development environment. The key advantage here is that you’re no longer guessing how the device app behaves; you’re exercising the real code paths.
The cost is operational complexity. Emulators require setup, they introduce runtime dependencies, and they tend to run slower than protocol simulations. They can also be more sensitive to environmental differences between developer machines and CI. A practical pattern is to use both layers: a protocol simulator for fast everyday coverage, and an emulator suite as a higher-confidence gate for releases or for changes that touch signing, parsing, or UI approvals.
A simple architecture that stays test-friendly
One transport interface that matches production code
Start by mirroring the interface your app already uses. If your wallet integration uses LedgerJS transports, a simulator that exposes the same send/receive shape will plug in cleanly. LedgerJS itself documents that the hw-transport libraries implement the communication layer for Ledger devices across platforms and channels. This lets you swap “real transport” for “simulated transport” without changing business logic.
A minimal command router with explicit errors
Have a single router that inspects the command fields and dispatches to handlers. Return realistic error conditions in a controlled way: user rejected, device locked, wrong app open, and transport timeout. The goal is to make your app’s error UX testable, because that’s where real users struggle.
A deterministic key vault used only for testing
Keep the simulator’s private key material isolated and deterministic. You don’t need secure element semantics. You need stable keys so your test suite can assert exact addresses and signatures.
If your team already uses a seed phrase for fixtures, treat it as test-only. Make it impossible to build production binaries with the simulator enabled.
A prompt engine that can be automated
Even a “simple” simulator benefits from modeling approvals. Give your test runner a way to preconfigure approval behavior: always approve, always reject, approve only if the displayed address matches the expected, or pause until the test explicitly signals “approve.”
Full emulators do this through UI controls. For example, Speculos is commonly used in integration tests and supports automated flows; many projects run tests against Speculos specifically.
A build plan that gets you useful coverage quickly
Step 1: Pick the boundary you want to simulate
Start by choosing where you want “device reality” to begin in your stack. You can simulate at the transport layer, at the protocol layer, or by running a real emulator. If your immediate pain is CI and regression testing, a protocol simulator usually gives the best return with the least setup because it plugs into your existing integration points and still reacts to real inputs. It also forces you to keep request encoding and sequencing honest without requiring a heavy environment.
If your pain is closer to “it works in our tests, fails on real devices,” treat that as a signal that you need a higher-fidelity tier, not that your simulator is pointless. Add an emulator layer later to validate real firmware behavior, UI flows, and edge-case responses. Tools like Ledger’s Speculos are often used for that higher-confidence pass, and Ledger’s own docs position it as a core part of testing even while acknowledging it won’t replicate every physical constraint.
Step 2: Implement the smallest command set that unblocks product tests
Begin with the commands that correspond to real user actions in your product. Most integrations need to retrieve a public key for a derivation path, then sign either a message or a transaction for a small set of supported types. Implement those first and keep their behavior deterministic, because the value of the simulator is repeatability. Once those commands behave consistently, you’ll already be able to cover the majority of regressions that break hardware wallet support.
Resist the temptation to chase completeness early. A simulator becomes fragile when it tries to mirror every device nuance and every firmware edge case. You’ll spend time rebuilding the vendor’s ecosystem instead of testing your own. It’s healthier to expand the command set only when the product genuinely needs it, and to lean on the emulator tier for “real firmware quirks” rather than encoding those quirks permanently into your simulator.
Step 3: Add scripted approvals for both success and failure paths
Approval flow is where hardware wallet support usually breaks in production. Users reject signatures. They approve after a delay. They close the window and come back. Your simulator needs a way to reproduce these outcomes on demand so you can test the app’s behavior without manual clicking. Treat approvals as part of the test API, not as UI behavior that “just happens.”
Once approvals are scriptable, you can validate the full UX contract: the dapp request is shown, the wallet prompts, the app waits correctly, and the result is returned with the right error handling when the user declines or the request times out. This is also where you can test your retry and cancellation rules, which are easy to get wrong and painful to diagnose when they only show up with physical devices.
Step 4: Write golden fixtures for addresses and signatures
Golden fixtures are what keep your simulator honest over time. Pick a test seed, pick a few derivation paths, and record expected addresses and signatures for a small set of canonical inputs. Then make those fixtures part of your continuous testing so changes in encoding, serialization, or derivation logic show up immediately. This catches the kinds of regressions that would otherwise slip through code review because they still “look correct” to a developer.
The goal isn’t to create brittle tests that fail on harmless refactors. The goal is to lock down the behavioral contract between your app and a hardware wallet. If a signature changes, something meaningful changed: the payload, the derivation path, the transaction type handling, or the hashing procedure. Fixtures turn that into a detectable event rather than a late-stage surprise.
Step 5: Run an emulator pass as a higher-confidence test tier
Once the simulator is stable and delivering day-to-day coverage, add a second tier that runs against a real emulator for the device family you support. This tier exists to catch the gaps your simulator will always have: firmware-specific command behavior, UI confirmation quirks, and device-app constraints that are hard to reproduce with a minimal implementation. It also gives you confidence that your integration still works when the device behaves like a device, not like your own code.
This is where tools like Speculos shine for Ledger integrations, because they run device apps without physical hardware and are commonly used in automated test pipelines. For Trezor-style flows, Trezor provides a firmware emulator as well, with an explicit development-only posture. Run this emulator tier less frequently if it’s slower, but run it consistently, especially before releases and after changes that touch signing payloads, derivation logic, or request/response handling.
Conclusion
A simple hardware wallet simulator is a testing tool that replaces the unstable parts of device integration with repeatable behavior. At the lighter end, it looks like a mocked or minimal protocol layer. At the realistic end, it looks like running real device apps inside an emulator such as Ledger’s Speculos or Trezor’s firmware emulator.
The best outcome is not perfect emulation. It’s confidence. When every signature request can be replayed, every rejection can be tested, and every release can run hardware wallet flows in CI, device support stops being a late-stage scramble and starts being part of your normal engineering rhythm.




