MidnightWallet
Embedded wallet that handles transaction balancing and submission on-device.
Implements TransactionBalancer so it can be plugged into com.midnight.kuira.core.compact.MidnightConfig as a drop-in replacement for the remote DAppConnectorClient.
Uses DustSyncManager for session-scoped caching. Handles error 170 (InvalidDustSpendProof) by auto-retrying with a fresh dust sync.
Properties
Same "disable = stop uploading" gate for the app-state blob (#246). Set false by disableAppStateCloudBackup; the host persists the preference and re-applies it after each SDK (re)build — WITHOUT it a disable wouldn't stick (the next refresh would re-upload the deleted blob). Default true.
Host-registered snapshot of app state to back up automatically on each refresh (≤2 KB; e.g. Kicks match witnesses). The host sets this once after obtaining the wallet — the SDK can't inject it (the provider lives above this layer). The upload is silent (seed-keyed) + digest-guarded, so it's a no-op when the snapshot is unchanged. Null → nothing to back up.
Observable cloud-backup status (dust + app-state lanes), updated by the backup methods so the UI can show "syncing / up to date / needs consent / failed" instead of a silent log. Identity recovery (the passkey) is sourced separately at the app layer.
User opt-out for dust cloud backup. When false, backupDustToCloud (and therefore refresh) skip the Drive upload entirely — that's the "disable" semantics: we simply stop uploading. The host persists the preference and re-applies it after each SDK (re)build. Default true.
Live NIGHT-UTXO list (as the registration-filter JSON) wired by MidnightSdk. Lets balance/balanceFlow compute dustRegistered from true per-UTXO registration state — "are all current NIGHT UTXOs generating dust?" — instead of the dust>0 heuristic (#265). Null → fall back to the heuristic (e.g. before the SDK wires it).
Per-transaction inbound NIGHT receipts (#284), classified by UTXO-set provenance in the indexer subscription: a genuine receipt is a transaction that created NIGHT to us without spending any of our UTXOs. Our own sends (which return change to us) are NEVER receipts, so a consumer wiring this to a notification will never tell the user they "received" their own change. Each event carries the exact amount received IN THAT transaction — not a balance delta. Cold by default (emptyFlow) when no subscription is wired (e.g. tests).
Observable wallet-sync status (#235). EVERY heavy sync this wallet runs publishes here — syncDust, the proactive proactiveDustResync, the shielded + dust refresh, and the forceFullSync genesis rebuild — so a background observer (the foreground-service Live-Update notification) can render progress for whichever phase is in flight without driving the sync itself. The in-app WalletSyncIndicator can also collect this instead of the per-call lambda.
SUPPLIER of the wallet-level backup prefs carried INSIDE the app-state blob (see WalletBackupPrefs) — the SDK can't derive them (the committed toggle is host UI state). A supplier, not a pushed field, on purpose: it's read at UPLOAD time, so a pref change (e.g. the restore gate committing the toggle ON, or mirroring an opt-out) is always reflected in the next blob without the host re-pushing a snapshot at every mutation site — a pushed snapshot went stale and clobbered restored cloud truth. The host sets this once per build. Null supplier / null result → prefs marked absent.
Functions
Back up host app state (≤2 KB; e.g. Kicks match witnesses) to the cloud, encrypted under a seed-derived key — no passkey/biometric, so it's safe to call automatically. No-op when no coordinator is wired. The coordinator hash-guards redundant uploads, so this is cheap to call after every meaningful state change. Never throws to the caller.
Snapshot the current dust checkpoint and hand it to the cloud backup coordinator (e.g. Google Drive) for cross-device recovery. No-op when no coordinator is wired or there's no checkpoint yet. The coordinator hash-guards redundant uploads, so this is cheap to call after every sync.
Current snapshot of every balance the SDK tracks for this wallet — unshielded NIGHT, shielded NIGHT, DUST, and the registration flag.
Observable balance — emits a fresh WalletBalance whenever the wallet's unshielded OR shielded NIGHT changes. Both are driven by background indexer subscriptions, so externally-received funds (an airdrop, an incoming shielded or unshielded transfer) surface automatically without a manual refresh. Each emission also re-reads dust.
Release resources. Suspends because closing the dust state must be serialized against an in-flight balance: balanceAgainst reads the shared DustLocalState's nullifiers under balanceMutex, so closing it out from under a running balance throws "DustLocalState has been closed" (regression — every other dust-close path already holds this mutex; the teardown one added later did not). Under localnet the balance is instant so the window never opened; on PreProd it spans seconds of dust sync, so a teardown mid-deploy raced the close. Acquiring the mutex here makes the teardown WAIT for the balance instead of yanking its state.
Delete the cloud app-state blob (#246 — true backup disable). Clears the remote blob + local upload digest via the coordinator; no-op when none is wired. Recovery is unaffected (the seed is passkey-derived) — this only drops the convenience app-state copy.
Disable dust cloud backup and DELETE the cloud copy (#246 — true backup disable). Stops future uploads (dustBackupEnabled = false) and clears the remote blob + local upload digests via the coordinator. No-op when no coordinator is wired. The wallet itself is unaffected — recovery is the passkey; the cloud blob is only a dust-sync-speed checkpoint.
Fetch the previously backed-up host app state, or null if none / no coordinator / unavailable. Decryption is seed-derived (no biometric), so a freshly-signed-in device recovers its app state silently.
Fetch the full restored app state: wallet-level WalletBackupPrefs (when the blob carries the SDK envelope) + the host payload. fetchAppState is the host-payload shortcut; this variant exists for the bootstrap restore flow, which needs the prefs to decide restore-over-genesis. One Block Store read serves both.
Force-rebuild the unshielded UTXO cache from genesis to self-heal a stale NIGHT balance.
NIGHT UTXOs (from nightUtxosJson) that are NOT yet generating dust — i.e. still need a registration. Matched natively against the current synced dust state's backing-nights, so it's correct regardless of the cache-layer tokens. A null/absent dust state means nothing is registered yet, so all are returned. Drives MidnightSdk.registerForDustGeneration's per-UTXO registration loop.