0xMiden / 0xMiden/web-sdk

MidenClient missing surface forces consumers onto a second WasmWebClient (dual-instance store → synced notes not found)

Open
#169 2 comments 0 reactions 1 assignee Claimed by @WiktorStarczewski View on GitHub
Dominant language
TypeScript
Stars
1
Forks
21
Avg merge
12h 14m
Merged PRs (30d)
41

Description

## Summary

`MidenClient` doesn't expose enough surface to cover some advanced flows, which forces consumers (e.g. `@openzeppelin/miden-multisig-client`) to drop down to the low-level `WasmWebClient`. Because there's no supported way to borrow the `WasmWebClient` that a `MidenClient` already owns, consumers **reconstruct a second `WasmWebClient` keyed to the same store identifier**. Two live WASM clients over one IndexedDB database don't share in-memory state, so writes made through one client aren't reliably visible through the other.

The user-visible symptom (reported against the multisig wallet): **public notes sent to a private multisig account are synced but then not found**. The note is fetched + persisted by `midenClient.sync()` (client A), but read back through the multisig-client's separate raw client (client B), which doesn't see it. The same flow works in pure Rust because there's a single client/store.

## Root cause

In `@openzeppelin/miden-multisig-client`'s `raw-client.ts`:

```ts
const rawClient = WasmWebClient.createClient(
rpcUrl, undefined, undefined,
client.storeIdentifier(), // same IndexedDB *name*, brand-new instance
);
```

This is a reasonable workaround, not the consumer's fault: the multisig-client genuinely needs low-level operations, and the SDK gives it no way to reuse `MidenClient`'s underlying client. `getRawMidenClient` already reuses a `WasmWebClient` when handed one directly — but apps hand it a high-level `MidenClient`, which has no accessor to surrender its raw client, so it always hits the reconstruct path.

## Why consumers reach for the raw client

Audit of every raw `WasmWebClient` call the multisig-client makes vs. the `MidenClient` surface:

| Raw call | `MidenClient` equivalent | Status |
|---|---|---|
| `getAccount` | `accounts.get` | already covered |
| `newAccount(acc, overwrite)` | `accounts.insert({account, overwrite})` | already covered |
| `syncState` | `sync` | already covered |
| `getInputNote` | `notes.get` | already covered |
| `compileTxScript` | `compile.txScript` | already covered |
| `executeForSummary(acc, request)` | `transactions.preview({operation:"custom", account, request})` | already covered |
| `getConsumableNotes` | `notes.listAvailable` | **gap**: drops `noteConsumability()` (the `consumableAfterBlock` filter) |
| `createCodeBuilder().linkModule().compileAccountComponentCode()` | `compile.component({code, slots})` | **gap**: no dependency-module linking |

So most calls are already covered — the consumer just hasn't migrated — but two real gaps remain.

## Gaps to close on `MidenClient`

1. **`compile.component` can't link dependency modules.** `CompileComponentOptions` had no `libraries`, unlike `compile.txScript`/`compile.noteScript`. The multisig auth component imports guardian + multisig MASM libraries, so it was forced onto a raw code builder. Note the correctness constraint: a component's compiled MAST determines the account's code commitment (baked into the account ID), so the linking path must produce a **byte-identical** component — i.e. static source linking via `linkModule`, not `buildLibrary` + `linkStaticLibrary`.

2. **`notes.listAvailable` drops consumability metadata.** `get_consumable_notes` returns notes including block-locked ones (`NoteConsumptionStatus::ConsumableAfter`). `listAvailable` maps to `inputNoteRecord()`, discarding `noteConsumability()`, so callers can't distinguish consumable-now from time-locked notes.

## Proposed fix

- Add `libraries?: CompileTxScriptLibrary[]` to `CompileComponentOptions`; `compile.component` static-links each via `builder.linkModule(namespace, code)` (identical MAST by construction).
- Add `notes.listConsumable({ account? })` returning `ConsumableNoteRecord[]` with consumability preserved.

After these, the multisig-client can run entirely on a `MidenClient` (or a single shared `WasmWebClient`) — one instance per store — which removes the dual-instance bug at the source.

A follow-up worth considering: a supported accessor for `MidenClient`'s underlying `WasmWebClient` (or guidance/guardrails against constructing a second client over an existing store id), so consumers don't reconstruct one by store identifier.

## Verification (done locally)

Implemented both gaps + tests. Built the napi module and confirmed `compile.component({ libraries })` produces a component digest **identical** to the raw `createCodeBuilder().linkModule()` path (`EQUAL: true`), so existing accounts are unaffected. Unit tests (compiler + notes): 48 passing; node integration `compile.component` group: 4 passing.

Contributor guide

Open the contributing guide

Research direction

The issue is in the TypeScript SDK, focusing on the MidenClient and WasmWebClient classes. Look at the compile.component method in the compiler module and the notes.listAvailable method. The goal is to add library linking support to component compilation and expose consumability metadata for notes. Check existing tests for these modules to understand the current behavior and add new tests for the changes. The fix must ensure byte-identical component compilation to not break existing accounts.

Written by the indexing model from the issue text.

Assessment

Tech stack
nodejs, typescript, wasm
Domain
api, backend, databases
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.