Repository navigation
feat!: accept any transport that can send an APDU (DMK support) - #739
Open
chcmedeiros wants to merge 8 commits into
Open
chcmedeiros wants to merge 8 commits into
chcmedeiros wants to merge 8 commits into
Conversation
OasisApp only needs send and decorateAppAPIMethods. Describe those structurally so a DMK-backed transport works without a hw-transport dependency, while existing Transport callers keep compiling.
The previous commit narrowed `Transport` -- a type `src/types.ts` has re-exported
since long before the DMK work -- and, through the JSDoc on the constructor,
`app.transport` with it. Anyone annotating with either and then calling `close`,
`exchange` or `on` stopped compiling, silently and for no stated reason.
hw-transport is still removed from `dependencies`: Ledger is retiring it, and
keeping it only to re-export its type would defeat the point. So the narrowing
stands, but it is now named, minimised and documented.
Split the two roles apart:
- `LedgerTransport` (new) is the structural `send` + `decorateAppAPIMethods`
surface the app actually needs, satisfied by a hw-transport `Transport` and by
a DMK transport alike.
- `Transport` stays exported as a deprecated alias of it, so `import type
{ Transport }` still resolves and passing a hw-transport is unaffected.
- `OasisApp` and `App` become generic in the transport. This is what keeps the
common case working: `new OasisApp(await TransportWebUSB.create())` infers `T`
as that class, so `app.transport.close()` still typechecks even though this
package no longer knows what hw-transport is. A DMK caller keeps its own
members the same way.
What still compiles, verified against this branch: reaching through
`app.transport` for `close()` on an app built from a WebUSB transport; assigning
a real `Transport` to the exported alias; a DMK-style transport, reading a
member only it has; a bare `App` annotation calling `send`.
What no longer compiles, asserted with `@ts-expect-error` so it cannot regress
unnoticed: reaching through the *exported* `Transport` type for `close()`, and
doing the same through a bare `OasisApp` annotation. Both are fixed by
annotating with the concrete transport class, or by letting inference do it.
Types build with no hw-transport import left in the emitted `.d.ts`, eslint is
clean, and the 3 unit tests pass. package.json loses exactly one line; yarn.lock
loses one specifier, hw-transport staying only for devDependencies.
BREAKING CHANGE: the exported `Transport` type is now structural rather than
`Transport` from `@ledgerhq/hw-transport`, so reaching through it -- or through
a bare `OasisApp`/`App` annotation -- for a hw-transport member no longer
typechecks. Import `Transport` from `@ledgerhq/hw-transport` directly, or
annotate with the transport class you construct. Needs a major: 1.0.0 -> 2.0.0.
chcmedeiros
force-pushed
the
feat/dmk-transport-support
branch
from
September 14, 2026 10:01
3039c9a to
033db03
Compare
ainhoa-a
approved these changes
Sep 14, 2026
Mirror @zondax/ledger-js 2.1.0, which deprecates non-DMK transports on its BaseApp. OasisApp does not extend BaseApp, so it gets its own copy: - The constructor gains JSDoc overloads. A DMKTransport selects the primary signature; anything else (an hw-transport Transport, a plain object implementing send and decorateAppAPIMethods) selects a second one marked @deprecated. The primary overload is a conditional rather than T & DMKTransport, because inference skips the identical DMKTransport constituent and would leave app.transport without its members. - Constructing over a non-DMK transport logs a one-time console warning. Naming DMKTransport needs the real class, so @zondax/ledger-js ^2.1.0 becomes a runtime dependency. It has no dependencies of its own. Against the JS source, construct signatures built from JSDoc @overload do not carry the class template, so the type-only example in types.ts passes its transport as any. The emitted dist/index.d.ts infers T normally.
@zondax/ledger-js declares engines.node >=20 and yarn classic enforces it, so yarn install --frozen-lockfile fails on Node 18 (end-of-life since April 2025).
0xPxt
approved these changes
Sep 15, 2026
matevz
reviewed
Sep 16, 2026
matevz
left a comment
Member
There was a problem hiding this comment.
We're using @ledgerhq/hw-transport-webusb heavily in https://github.com/oasisprotocol/wallet, so merging this and releasing a new 2.0.0 breaking version of @oasisprotocol/ledger will also require some work on that front. I would appreciate some migration hints (see below).
Drop src/deprecation.js, per review. The once-per-process warning now lives in OasisApp's module and points at a new README section, "Migrating to the Device Management Kit", which covers what 2.0.0 changes on its own, the hw-transport-webusb -> DMK WebHID mapping with a before/after example, the behaviour that differs, and the transport kits for node-hid, Web BLE and React Native BLE. The deprecation tests load a fresh module per test instead of importing a test-only reset hook.
For each OasisApp method, a diff from the hw-transport-webusb call to the DMK one, with what the method returns and what happens on the device. Also covers path formats, the lack of a default timeout on methods that wait for the user, and the 0x6986 rejection code.
lubej
reviewed
Sep 16, 2026
Per review. `yarn test:types` builds the declarations and compiles tests/types/inference.ts against dist/*.d.ts, resolved through the package's own import paths. It checks what consumers compile against: the JS source cannot bind `T` through JSDoc `@overload` constructors. It asserts that `app.transport` keeps a hw-transport's and a DMKTransport's own members, and is not `any`; that a bare `OasisApp` annotation and the deprecated `Transport` alias no longer reach `close()`; that `OasisApp<typeof transport>` fixes the bare annotation; and that a transport without `send` is rejected. Breaking the emitted declarations (widening `transport` to `any`, dropping the generic, aliasing `Transport` to `any`, dropping `send`) fails it. typeOnlyTest moves out of src/types.ts into the same file, without the `as any` it needed against the source. CI runs the new script. The README suggested `OasisApp<TransportWebUSB>`, which does not compile for `TransportWebUSB.create()`: that returns the base `Transport`. Suggest `OasisApp<typeof transport>` instead.
Per review: pin the exact version rather than relying on yarn.lock alone. The lockfile entry changes only its range key; the resolved tarball and integrity hash are unchanged. Consumers who import DMKTransport directly need that same version, or they get a second copy whose DMKTransport fails OasisApp's instanceof check. The README install line and note now say so.
lubej
approved these changes
Sep 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Ledger deprecated LedgerJS (
@ledgerhq/hw-transport) in favour of the Device Management Kit, ahead of a September 2026 cutoff.OasisApponly ever callssendanddecorateAppAPIMethodson the transport, so DMK support is a type change, not a rewrite.What
LedgerTransport(new) describes those two methods structurally. A hw-transportTransportsatisfies it, and so does aDMKTransportfrom@zondax/ledger-jsv2 — or any object implementing the two.@ledgerhq/hw-transportleavesdependencies. The emitteddist/*.d.tscontains nohw-transportimport at all.hw-transport-node-hid/hw-transport-webusbstay as devDependencies for tests and the type-only example.Transportstays exported as a@deprecatedalias ofLedgerTransport, soimport type { Transport }keeps resolving and passing a hw-transport is unaffected.OasisAppandAppare generic in the transport. This is what keeps the common case working — see below.@zondax/ledger-jsas a runtime dependency and moves CI to Node 22 — see the next three sections.Non-DMK transports are deprecated
Accepting both keeps existing wallets working, but
DMKTransportis the path forward. This mirrors@zondax/ledger-js2.1.0, which does the same on itsBaseApp;OasisAppdoesn't extendBaseApp, so it carries its own copy.Constructor overloads (JSDoc
@overload, emitted todist/index.d.tsas ordinary TypeScript):DMKTransporthas private members, so only a real instance matches the first overload. A hw-transportTransportor a plain object implementing the two methods lands on the deprecated one, which editors render as a strikethrough. The first overload is a conditional rather thanT & DMKTransporton purpose: inference skips the identicalDMKTransportconstituent of an intersection, which would leaveTat its default and costapp.transportits members.One-time runtime warning. Constructing over anything that isn't
instanceof DMKTransportlogs once per process:tscnever reports deprecations, so this was checked with the TypeScript language service (getSuggestionDiagnostics, what editors use) against the emitteddist/index.d.ts, TypeScript 5.6.2:app.transportkeeps its membersnew OasisApp(new DMKTransport(dmk, id))setScrambleKey()compilesnew OasisApp(transportWebUSB)close()compilesnew OasisApp(transportWebUSB, scrambleKey)new OasisApp({ send, decorateAppAPIMethods })Things to know:
instanceofis spoofable and the transport runs in the caller's own process; the device screen is what protects against a lying host.@zondax/ledger-jswould see the warning for a genuineDMKTransport, and its type would land on the deprecated overload too..d.ts), TypeScript does not carry the class template into construct signatures built from JSDoc@overload, sotypeOnlyTestinsrc/types.tsnow passes its transportas any. That check is about the methods' response types, which don't depend onT; consumers readdist/index.d.ts, which infers normally (table above).New runtime dependency:
@zondax/ledger-js ^2.1.0Naming
DMKTransport, for the overload and theinstanceof, needs the real class, so@zondax/ledger-js(Apache-2.0, maintained by Zondax) becomes a dependency. It has no dependencies of its own, so it adds exactly one package to the graph. Flagging it explicitly since it puts a Zondax package in this repo's runtime tree; the alternative was a structural heuristic (flag transports that haveexchange/close), which can't tell a hand-rolled transport from a DMK one.CI moves to Node 22
@zondax/ledger-jsdeclaresengines.node >=20, and yarn classic enforcesengineson install. Verified locally with the CI command (yarn install --frozen-lockfile):error @zondax/ledger-js@2.1.0: The engine "node" is incompatible with this module. Expected version ">=20". Got "18.20.8"So
ci-tests.ymlandrelease.ymlmove from Node 18 (end-of-life since April 2025) to 22. The same applies to consumers: a project still on Node 18 installing this package with yarn classic will hit that error (npm and pnpm only warn). That lands in the same major as the type change below.This is a breaking change (1.0.0 → 2.0.0)
src/types.tshas re-exported hw-transport'sTransportsince long before this work, so it is part of the published API. Removing the dependency means that type cannot be re-exported, and the name necessarily narrows. Rather than let that happen silently, it is scoped and asserted.Still compiles — verified against this branch:
const app = new OasisApp(await TransportWebUSB.create()); await app.transport.close()Tis inferred asTransportWebUSB, so the class's own members are intactconst t: Transport = realHwTransportTransportstill satisfies the aliasAppannotation callingsendsendis on the structural typeNo longer compiles — each asserted with
@ts-expect-errorin a throwaway probe, so neither can regress unnoticed:Transporttype forclose()/exchange()/on()Transportfrom@ledgerhq/hw-transportdirectlyOasisApp/AppannotationOasisApp<TransportWebUSB>, or let inference do itThe generic is doing the load-bearing work here: without it,
app.transportwould collapse to the structural type and every consumer reaching through it would break, not just those who annotate explicitly.Also breaking, see above: yarn-classic installs on Node < 20.
Verification
yarn build(Babel +tsctypes)hw-transportimport in emitted.d.tsyarn test:uniteslint src/ tests/deprecation.spec.jsyarn install --frozen-lockfileon Node 18 / 22dist/index.d.ts(language service)yarn.lockloses hw-transport as a direct dependency (it remains for devDependencies) and gains@zondax/ledger-js.Integration tests were not run here (they need a physical device).
Note for maintainers
I have not bumped
versioninpackage.json— releases here are driven by pushing av*tag, so the bump is yours to make. It needs to be a major.