12 Iterator Helpers bugs in Midnight SDK under Node v22 — Array.from() workarounds included

Hi team,

While working on a Mainnet deploy from a CLI-based script (Node v22, @midnight-ntwrk/midnight-js-* SDK), I hit a series of TypeError failures caused by Iterator Helpers assumptions in the SDK. Sharing the full list of affected sites and the workaround I applied, in case it’s useful to the SDK team or other developers who run into the same issues.


Environment

  • Node: v22.x

  • SDK packages affected:

    • @midnight-ntwrk/wallet-sdk-shielded v3.0.0

    • @midnight-ntwrk/wallet-sdk-unshielded-wallet v3.0.0

    • @midnight-ntwrk/wallet-sdk-facade v4.0.0

    • @midnight-ntwrk/wallet-sdk-dust-wallet v4.0.0

    • @midnight-ntwrk/wallet-sdk-capabilities (balancer submodule)

    • @midnight-ntwrk/compact-runtime

  • Pattern: SDK chains .entries() / .keys() / .values() (returning WASM-backed Map iterators) with subsequent .filter() / .map() / .every() / .some() / .find() / .forEach() / .toArray() etc., assuming TC39 Iterator Helpers behavior. Under Node v22, these chains throw TypeError: X.entries(...).filter is not a function.


Root cause (my understanding)

The SDK appears to be written against a JS runtime where Iterator Helpers (Stage 4 proposal) are available on the objects returned by WASM bindings. In Node v22, the Iterator Helpers proposal is implemented but the iterators returned from WASM-bound Map instances do not inherit from Iterator.prototype, so the helper methods (.filter, .map, etc.) are not available on them.

Wrapping each call site with Array.from(...) converts the iterator to a plain array, which has the equivalent methods as Array.prototype methods — resolving the failures without any functional change.


Full list of affected sites (12 total across 8 files)

I identified these by running a regex scan across node_modules/@midnight-ntwrk/ for the pattern:

regex

\.(entries|values|keys)\(\s*\)\s*(\n\s*)?\.(filter|map|flatMap|reduce|every|some|find|toArray|forEach)\b
# File Line Method chain
1 wallet-sdk-shielded/dist/v1/CoreWallet.js -– MapIterator.map()
2 wallet-sdk-shielded/dist/v1/TransactionOps.js ~52 tx.imbalances(0).entries().filter(...) (getGuaranteedImbalances)
3 wallet-sdk-shielded/dist/v1/TransactionOps.js -– tx.imbalances(segment).entries().filter(...) (getFallibleImbalances)
4 wallet-sdk-shielded/dist/v1/TransactionImbalances.js -– imbalances.guaranteed.entries().every(...)
5 wallet-sdk-shielded/dist/v1/TransactionImbalances.js -– segmentImbalances.entries().every(...)
6 wallet-sdk-unshielded-wallet/dist/v1/TransactionOps.js ~31 transaction.intents?.keys(...).toArray() (getSegments)
7 wallet-sdk-unshielded-wallet/dist/v1/TransactionOps.js ~64 transaction.imbalances(...).entries().filter(...)
8 wallet-sdk-facade/dist/transaction.js -– iterator chain (2 sites)
9 wallet-sdk-facade/dist/transaction.js -– (same file, second site)
10 wallet-sdk-capabilities/dist/balancer/Imbalances.js ~35-50 merge/apply iterator chains
11 wallet-sdk-dust-wallet/dist/v1/Transacting.js ~177-180 transaction.imbalances(0, totalFee).entries().find(...) (feeImbalance)
12 compact-runtime/dist/contract-dependencies.js ~168 stateMap.keys().forEach(...) (compactMapADTDependencies)

Example patch (one of 12)

Before (wallet-sdk-shielded/dist/v1/TransactionOps.js):

js

getGuaranteedImbalances: (tx) => {
    const rawGuaranteedImbalances = tx
        .imbalances(0)
        .entries()
        .filter(([token]) => token.tag === 'shielded')
        .map(([token, value]) => {
        return [token.raw, value];
    });
    return Imbalances.fromEntries(rawGuaranteedImbalances);
},

After:

js

getGuaranteedImbalances: (tx) => {
    const rawGuaranteedImbalances = Array.from(tx.imbalances(0).entries())
        .filter(([token]) => token.tag === 'shielded')
        .map(([token, value]) => [token.raw, value]);
    return Imbalances.fromEntries(rawGuaranteedImbalances);
},

All 12 sites follow the same pattern: wrap the iterator-returning call with Array.from(...).


Result after applying all 12 patches

  • Wallet sync :white_check_mark:

  • Provider configuration :white_check_mark:

  • Proof generation :white_check_mark:

  • TX construction :white_check_mark:

  • TX submission to node :white_check_mark: (got past every SDK failure, reached the actual substrate node)

(The deploy itself then failed at the substrate level with 1016 Immediately Dropped, which I believe is a separate issue unrelated to the SDK — filed in another topic re: mNIGHT/bridge requirements.)


Suggested fixes for the SDK

  1. Short-term: wrap all .entries()/.values()/.keys() calls from WASM-returned Maps with Array.from(...) in the SDK source. This is minimally invasive and cross-runtime compatible.

  2. Long-term: either (a) document the required Node version / polyfill, or (b) update the WASM bindings layer to return objects that inherit from Iterator.prototype so Iterator Helpers work natively.

  3. Testing: add a CI job running the SDK against Node v22 (current LTS is v22.x).


Persisting the workaround

Since these edits live in node_modules/, they’re wiped by npm install. For production use, the fix should be persisted with patch-package:

bash

npx patch-package @midnight-ntwrk/wallet-sdk-shielded
# ... repeat for each of the 8 affected packages

Plus a postinstall script:

json

"scripts": {
  "postinstall": "patch-package"
}

Happy to contribute

If it would help the SDK team, I can:

  • Share the full .orig vs patched diff for all 12 sites as a gist

  • Open a PR against the relevant SDK repos (if public)

  • Provide the full deploy log reproducing each failure in sequence

Let me know what format is most useful.

Thanks!

Takuya Ogura ECOSUS CO., LTD. (Thailand) Running GYOTAK food-traceability on Midnight

one misalignment based on compatibility matrix

is using wallet-sdk-facade@4.0.0 when the compatibility matrix specifies 3.0.0

just not sure if that would cause the runtime errors that you are seeing.

@hei349 Thanks — that’s an extremely important catch.

Confirmed on our side: we’re indeed running:

  • @midnight-ntwrk/wallet-sdk-facade@4.0.0
  • @midnight-ntwrk/wallet-sdk-dust-wallet@4.0.0

Both are 1 major ahead of the support matrix
(Compatibility matrix | Midnight Docs), which
specifies facade 3.0.0. Notably, the official wallet release notes
also stop at 3.0.0 (2026-03-20, “ledger v8 across the SDK”); there
appear to be no public 4.0.0 release notes, even though the package
is installable from npm via ^4.0.0 ranges.

Other Midnight packages in our deps (compact-runtime 0.15.0,
ledger-v8 8.0.3, midnight-js 4.0.4, onchain-runtime-v3 3.0.0,
compact-js 2.5.0, platform-js 2.2.4) all match the matrix exactly.

Looking at our git history, this misalignment was introduced on
2026-04-24 during an “SDK upgrade” — most likely facade and
dust-wallet got pulled to 4.x by a caret range while the rest of
the family stayed on 3.x.

Plan on our side:

  1. Verify the patches/ directory doesn’t break when downgrading
    facade/dust-wallet to 3.x.
  2. Pin to ^3.0.0 (matrix-aligned), reinstall.
  3. Re-run the deploy and check which of the 12 Iterator Helpers
    errors persist vs. disappear.

Will report back with the diff. If, after aligning to the matrix,
some errors remain, those would be much more confidently
real SDK bugs (not version-skew artifacts).

This is exactly the kind of thing that’s hard to spot when the
package manager will happily resolve 4.x without complaint.
Genuinely appreciate the second pair of eyes — would have spent
days chasing ghosts otherwise.

-– Takuya

markdown

**Update: matrix alignment + investigation results**

Following up on @hei349's catch about wallet-sdk-facade@4.0.0 vs the matrix-specified 3.0.0. We took the time to verify properly before reporting back. Some of what we found is more nuanced than a simple version mismatch.

## TL;DR

- ✅ Aligning to matrix (facade@3.0.0, dust-wallet@3.0.0) was necessary, but **only fixes 2/12 of the originally reported bugs.**
- ⚠️ The remaining 10/12 bugs persist on the matrix-specified versions themselves.
- ⚠️ The full matrix-compliant + ledger-v8-aligned configuration **cannot be installed by npm without `overrides`**, due to peer-dep contradictions inside the SDK.
- ✅ With matrix + overrides + 6 patches (covering all 12 sites), the wallet/ledger pipeline runs cleanly.

Working config + key findings below. The SDK team may find some of this useful — the bugs are more structural than I originally suggested.

---

## Verification methodology

I downgraded `wallet-sdk-facade` and `wallet-sdk-dust-wallet` from `^4.0.0` to `^3.0.0` to match the matrix, removed the two `+4.0.0.patch` files, ran `npm install`, and re-tested using a read-only smoke test that exercises wallet build, sync, and `getCatchLedgerState` (i.e., touches every SDK module that previously failed).

When this surfaced new failures, I drilled in via:
- per-package version checks against `node_modules/<pkg>/package.json`
- bisecting `import`s to isolate which module-load path threw
- `find node_modules/@midnight-ntwrk -mindepth 3 ... -name "package.json"` to detect duplicate installs
- `git blob` hash comparisons across SDK versions

---

## Findings

### 1. Matrix alignment is necessary but insufficient (10/12 bugs persist)

After downgrading facade and dust-wallet to 3.0.0, the same Iterator Helpers TypeErrors reappeared on the matrix-specified versions. Distribution of the original 12 bugs:

| Package (matrix-aligned version) | Bug sites | Already on matrix? |
|---|---|---|
| `wallet-sdk-shielded@3.0.0` | 5 | ✅ |
| `wallet-sdk-unshielded-wallet@3.0.0` | 2 | ✅ |
| `wallet-sdk-capabilities@3.x` | 1 | ✅ |
| `compact-runtime@0.15.0` | 1 | ✅ |
| `wallet-sdk-facade@3.0.0` | 2 | ✅ (after downgrade) |
| `wallet-sdk-dust-wallet@3.0.0` | 1 | ✅ (after downgrade) |

So only 3/12 sites were specific to the 4.0.0 packages. The other 9 were on the matrix-specified version all along.

### 2. Major version bump without code change (facade transaction.js)

I generated patches for facade@3.0.0 by editing the same buggy idiom in the same function (`hasTTLExpired`). When I diffed them against the existing 4.0.0 patches:

@midnight-ntwrk+wallet-sdk-facade+3.0.0.patch: index baf5ba0..3fcfca9 @midnight-ntwrk+wallet-sdk-facade+4.0.0.patch: index baf5ba0..3fcfca9 ← byte-identical


`facade/dist/transaction.js` is **byte-for-byte identical between 3.0.0 and 4.0.0**. The major version bump did not touch this file. Whatever motivated the major bump, it left the buggy code paths exactly as they were.

### 3. Same bug across minor versions (capabilities 3.2.0 vs 3.3.0)

The `wallet-sdk-capabilities+3.3.0.patch` applied successfully to `capabilities@3.2.0` resolved by npm — `patch-package` reported a fuzzy match that landed on the same line range. I read both versions of `dist/balancer/Imbalances.js`: the `allTokenTypes.values().map().filter().toArray()` chain exists identically in both. Same idiom, same line, two minor versions apart.

### 4. ⚠️ Matrix-compliant config doesn't install cleanly (npm doppelganger)

This was the surprising one. After matrix alignment, even read-only scripts crashed at module load time with `[Object: null prototype]` — singleton state mismatch. Investigation:

top-level: nested under wallet-sdk-facade@3.0.0: wallet-sdk-shielded@3.0.0 wallet-sdk-shielded@2.1.0 wallet-sdk-unshielded-wallet@3.0.0 wallet-sdk-unshielded-wallet@2.1.0 wallet-sdk-abstractions@2.1.0 wallet-sdk-abstractions@2.0.0 … (5 more pairs)


`wallet-sdk-facade@3.0.0`'s `package.json` declares `wallet-sdk-shielded@^2.1.0` and `wallet-sdk-unshielded-wallet@^2.1.0` as dependencies. The compatibility matrix tells users to align to facade@3.0.0, but doesn't mention that this requires shielded/unshielded@2.1.0 (which is from the pre-ledger-v8 era). If you instead pair facade@3.0.0 with shielded@3.0.0 (the version that the wallet release notes describe as "ledger v8 across the SDK"), npm cannot reconcile and creates a duplicate installation. Both versions get loaded simultaneously, breaking module-level singletons (`setNetworkId`, etc.).

The only fix I found was an explicit `overrides` block in `package.json` listing 8 wallet-sdk-* packages.

### 5. Iterator Helpers idiom is SDK-wide coding standard

The 12 bug sites span 6 packages (compact-runtime, capabilities, shielded, unshielded-wallet, dust-wallet, facade) and are all the same idiom:

```js
someMap.values()         // or .keys() / .entries()
  .map(...).filter(...)  // chaining iterator helper methods
  .toArray()             // returning the materialized array
```

These don't read like ad-hoc mistakes — they read like a coding convention. Once one developer used this pattern, others followed.

### 6. Transitive collateral downgrade (abstractions)

After matrix alignment, `wallet-sdk-abstractions` resolved to 2.0.0 (lowest compatible) instead of 2.1.0. `NoOpTransactionHistoryStorage` was added in 2.1.0; our app imports it; module load failed with a clean SyntaxError that node v22's ESM loader had hidden behind the same `[Object: null prototype]` symptom. Pinning `^2.1.0` directly fixed it.

This is the kind of detail the matrix doesn't currently document.

---

## Working configuration

For anyone hitting the same wall, here's the configuration that runs cleanly under Node v22 + ts-node:

```json
{
  "dependencies": {
    "@midnight-ntwrk/compact-runtime": "^0.15.0",
    "@midnight-ntwrk/ledger-v8": "^8.0.3",
    "@midnight-ntwrk/midnight-js": "^4.0.4",
    // ... other midnight-js-* sub-packages all ^4.0.4
    "@midnight-ntwrk/wallet-sdk-abstractions": "^2.1.0",
    "@midnight-ntwrk/wallet-sdk-address-format": "^3.1.0",
    "@midnight-ntwrk/wallet-sdk-dust-wallet": "^3.0.0",
    "@midnight-ntwrk/wallet-sdk-facade": "^3.0.0",
    "@midnight-ntwrk/wallet-sdk-hd": "^3.0.1",
    "@midnight-ntwrk/wallet-sdk-indexer-client": "^1.2.1",
    "@midnight-ntwrk/wallet-sdk-shielded": "^3.0.0",
    "@midnight-ntwrk/wallet-sdk-unshielded-wallet": "^3.0.0"
  },
  "overrides": {
    "@midnight-ntwrk/wallet-sdk-shielded": "^3.0.0",
    "@midnight-ntwrk/wallet-sdk-unshielded-wallet": "^3.0.0",
    "@midnight-ntwrk/wallet-sdk-abstractions": "^2.1.0",
    "@midnight-ntwrk/wallet-sdk-address-format": "^3.1.0",
    "@midnight-ntwrk/wallet-sdk-indexer-client": "^1.2.1",
    "@midnight-ntwrk/wallet-sdk-capabilities": "^3.2.0",
    "@midnight-ntwrk/wallet-sdk-runtime": "^1.0.2",
    "@midnight-ntwrk/wallet-sdk-utilities": "^1.1.0"
  }
}
```

Plus 6 `patches/*.patch` files via `patch-package` (all wrapping `Array.from(...)` around the iterator helper call sites). Smoke test: `check-orphan.ts` runs to completion in ~7 minutes with `RESULT: FOUND` and zero TypeErrors.

---

## Suggestions for the SDK team

If any of this is actionable on your end:

1. **Compatibility matrix should include peer/transitive dependency versions.** Currently it's a single facade version with no mention of which shielded/unshielded/abstractions versions are intended to pair with it. The current facade@3.0.0 entry implicitly requires shielded@2.1.0 (via `peerDependencies`), which conflicts with the wallet release notes pointing users at shielded@3.0.0.

2. **Iterator Helpers idiom should be removed across the SDK.** It's not a node-version issue you can document around — under node v22, WASM-bound Map iterators don't inherit from `Iterator.prototype`, so the helpers aren't available regardless of node flags. `Array.from(...)` first, then chain Array methods.

3. **CI under node v22 LTS** would catch all 12 of these on first import.

4. **Major version bumps probably shouldn't ship with byte-identical buggy files.** The 3.0.0/4.0.0 facade transaction.js identity is a real signal here.

---

## Happy to contribute

If useful, I can share:
- the full `patches/` directory (6 files, all `Array.from(...)` wrappers, ~80 lines of diff total)
- the smoke test script
- the full reproduction log

Thanks again @hei349 — without that pointer about the matrix mismatch, I would have stayed pointed at "12 SDK bugs" without ever discovering the layered npm/peer-dep/transitive issues underneath. The matrix question turned out to be the right thread to pull.

— Takuya
GYOTAK on Midnight (Hua Hin / Pranburi, Thailand)