feat(liquid): port Wasabi wallet to Liquid - #1
Draft
Abdullah1738 wants to merge 303 commits into
Draft
Abdullah1738 wants to merge 303 commits into
Abdullah1738 wants to merge 303 commits into
Conversation
The Liquid wallet home rendered off-Wasabi: bare underlined Send/Receive hyperlinks docked bottom-center, raw unstyled balance rows, no use of ContentArea.TopContent or the balance tile. Restyle LiquidWalletView to the Fluent pattern: ContentArea StretchTopContent, Send/Receive as function buttons (wallet_action icons) top-right, and a BALANCE TileControl (CopyableItem + PrivacyContentControl) mirroring the pegged row. Add a presentation-only PeggedBalanceRow projection + tests. Multi-asset rows and the History surface are unchanged.
…ition race Opening a Liquid wallet could fail once with 'node generation changed during the acquisition' when a testnet block landed mid-acquisition (a transient race), and a failed open then left the session reporting 'already open or opening' until restart. OpenWalletAsync is now a thin resilience wrapper over OpenWalletCoreAsync: RunWithTransientGenerationRetryAsync retries only the exact transient acquisition message (InvalidOperationException / ElementsRpcException), bounded to 2 extra attempts with 150ms backoff; all other rejections surface unretried and the generation fence stays fail-closed. The failed-open reservation release was already in the provider's catch; a regression test pins it. 8 new facts.
- Replay payload v3 carries InternalIndexHighWater in authenticated plaintext; legacy v1/v2 import internal high-water as 0 - Thread the value through replay open result, persistence handoff, load/save results, refresh state capture, state owner, and the refresh save fence - Generic and fenced saves carry both high-waters forward and reject either moving backward on a readable current state - Add LiquidWalletInternalIndexAllocator: generation-fenced, durable branch-1 index reservation (no reuse after reopen, gaps allowed), exhaustion at 0x7fffffff, no process-local state, no key derivation, address generation, send, native, RPC, or UI behavior
…surplus - New public LiquidWalletUiChangeDestination carries the wallet-owned branch-1 confidential change address as a public-safe value - Send execution scope reserves one durable branch-1 index lazily via LiquidWalletInternalIndexAllocator, derives the branch-1 spend script from the descriptor account xpub, blinds it with SLIP-77, and builds the confidential address with the receive-time factory; cached so both facade calls of one send observe the same address - Facade CreateDestinationBatch appends one change destination per asset whose selected total exceeds destination-plus-fee; when no change destination is supplied the batch is the byte-identical one-destination batch, preserving every pre-existing fail-closed exception ordering - Executor threads the reserved address into both facade calls; exact plan validator balances per asset. No native/FFI/RPC/validator/plan/ wire/refresh/CoinJoin changes. Unit/direct-executor evidence only
- Replace the free-text 64-hex asset-id TextBox with a ComboBox bound from the wallet's balance snapshot (pegged asset first, then issued in canonical order), each option showing asset id hex and atomic-unit balance with an L-BTC marker on the pegged row - SelectedAsset drives Recipient.AssetIdHex (the property the plan/sign path already consumes); default selection is the pegged asset, empty balance set leaves the dropdown empty with no fabricated asset - Host LiquidSendRecipientView inside LiquidSendView, removing the duplicated inline recipient TextBoxes - Dispatcher-deferred reseed keeps the selection stable across balance refreshes (a bound ComboBox clears SelectedItem when ItemsSource is replaced); empty initial option set avoids the null-initial OAPH race - Headless AvaloniaFact evidence renders the real send view and asserts the binding, default selection, selection-driven AssetIdHex, and reseed-on-refresh behavior. Presentation-only; no facade/plan/sign/ RPC/broadcast change
- Add LiquidAmountDisplay helper: the pegged asset (L-BTC, protocol-fixed
1e8 precision) renders as the L-BTC decimal form with Wasabi's
conventional fixed eight-fraction-digit grouping; an issued asset (no
known precision, no metadata) stays as the raw atomic-unit count — never
scaled. One shared helper, used by every display surface
- Wallet home balance tile + per-asset rows now bind BalanceDisplayText;
fixes the prior mislabel that printed the raw atomic integer with a
literal ' L-BTC' suffix
- Send asset-picker options and the built spend plan (explicit fee,
destinations, selected totals) render the pegged-aware display via new
LiquidSpendPlan{Destination,AssetAmount,Item}ViewModel wrappers
- Additive IsPeggedAsset flag on the spend-plan destination projection so
the presentation layer can pick its display convention; frozen
property-set test updated for the additive surface
- History asset-change rows already formatted the pegged amount as the
signed L-BTC decimal via the core DisplayAmount projection
- Headless AvaloniaFact evidence asserts the exact pegged decimal string
and the issued atomic-unit string for the balance row and the spend-plan
asset-amount wrapper. Presentation-only; no facade/plan/sign/RPC/
broadcast change
…lay payload Add a durable, authenticated receive-label store keyed by the external (branch-0) receive derivation index. Labels persist in the sealed replay payload as a new payload version 4; legacy v1/v2/v3 payloads open and import an empty label map. The map is canonically ordered by ascending index, duplicate-rejected, bounded (entry count, per-label and aggregate UTF-8), and byte-exact under the fixed-time canonicality re-check. SetLabels is generation-fenced and fail-closed: it loads the current state, applies the set (removing the entry when empty), and saves under the exact captured generation+1, rejecting a concurrent generation change. High-waters, output state, and confirmations are untouched. The published receive material rebinds NextReceiveLabels from the committed state's durable label map. No native/RPC/send/sign/CoinJoin change; labels carry no key material.
Thread the published NextReceiveLabels into the Fluent model (read path) and expose a narrow public SetNextReceiveLabelsAsync command on the application client that resolves the open session, reads the current next-receive index, and invokes the generation-fenced receive-label command service (write path). The Receive view shows the existing durable label joined as Wasabi's comma-separated convention and saves the edited set (empty clears) through the durable v4 store, not a process-local dictionary. Fail-closed: command rejections surface as-is. SaveLabel uses the main-thread output scheduler so the CanExecute completion reaches the bound button on the UI thread. No v4 payload/replay/fence/allocator/command-service core change; no native, RPC, send/sign, CoinJoin, or address-derivation change; labels carry no key material.
…end flow Two additive facade projections into the send flow. Change attribution: the spend-plan destination projection gains an additive IsWalletOwnedChange flag, true exactly when the destination's canonical address text ordinal-matches the change address the facade was given for that plan (attribution of already-composed change only; never alters composition). A 'change' tag shows on the matching row. No flag is fabricated when no change destination is supplied. Visual coin control: the raw free-text outpoint field is replaced by a checkable list of the wallet's selectable outputs. Every row defaults to selected, preserving the landed empty-field semantics (an empty selection meant every spendable outpoint funded the plan). Checked rows drive the exact selected- outpoint hex set the plan/sign path consumes. Never fabricates outputs, never admits non-wallet outpoints, never bypasses exact-plan validation. No native/RPC/send-sign/CoinJoin/address-derivation/exact-plan change.
…t asset Each wallet-home balance row gains a Send action that navigates to the send flow with that row's asset pre-selected in the asset picker. The recipient holds the wanted asset id and consumes it in the deferred reseed once the matching option arrives (the initial option emission is scheduler-deferred); the held selection then survives later refreshes. The default reseed semantics for the plain top-level Send path (pegged-first) are unchanged, and the picker never fabricates an option for an empty balance set. Presentation-only: the affordance only chooses which asset the picker holds. It does not pre-fill amount or address, bypass validation, or touch the plan builder, change composition, signing, RPC, native, CoinJoin, address derivation, or exact-plan validation.
The two stalling-stream idle-timeout tests raced a short idle timeout against a short total-request timeout (75ms vs 500ms; 100ms vs 300ms). Under parallel CPU load the total timeout could win the race and surface the wrong failure kind/message, making both tests timing-flaky (they passed only in isolation, which is why the suite excluded one). The stream never yields data, so the idle timeout is the intended winner. Widen the margin — keep the idle timeout short but push the total-request timeout to 10s — so the idle timer always fires first regardless of scheduler jitter. The assertions (Timeout kind + 'idle timeout' message) are unchanged; the test intent is fully preserved. The full Liquid filter now runs with no exclusion: 1209/1209 Debug + 1209/1209 Release (the previously excluded test now passes in the parallel run).
…ck generation fence Surfaced by live testnet QA: against the official 23.3.3 node (which lacks the fork-only getnodegeneration RPC), every wallet refresh threw 'node generation changed during the acquisition'. The fallback generation observation pinned ChainstateRevision to 0, so any ~1-minute Liquid block landing between the before/after observations read as 'same revision, different tip' and threw. The fallback observation's ChainstateRevision now proxies the observed block height, so the existing fences tolerate forward-only tip movement (a new block advances the height and therefore the revision) while still rejecting a height rollback (the revision regresses) and a same-height tip identity change (the revision is unchanged but the best-block hash differs). The all-zero startup-id sentinel is retained: restart detection is genuinely unavailable without getnodegeneration, and the doc comments say so rather than overclaiming. The getnodegeneration path is unchanged. The deterministic every-refresh false positive is gone; the residual mid-refresh throw is now the rare, retryable transient race the refresh path already handles. Mutation evidence: restoring the constant revision 0 turns the new forward-progress test RED with the exact live error; rollback and same-height identity-change cases stay fail-closed even under the probe.
Surfaced by live testnet QA: every wallet OPEN consumed one external receive index and durably advanced the high-water (Allocate did allocatedIndex = highWater; highWater+1 and persisted it). So a label set on a receive address was durable in the store but never shown again — NextReceiveLabels always pointed at a fresh unlabeled address after reopen, and the address pool burned on every app start. Stock Wasabi advances the receive index only on explicit issuance (getnewaddress / NextReceiveAddress), never on open. AllocateWithFirstOpenInitialization now PEEKS: it loads the persisted state and returns the next-receive allocation at the CURRENT external-index high-water with no +1 and no save, so the next-receive address is deterministic across opens that observe no intervening issuance or received funds. The genuinely-absent genesis case still seals LiquidWalletState.Empty at generation 0 and presents index 0 without advancing. Allocate (durable issuance) is unchanged — there is no production caller that issues addresses beyond open, so no new public issue API is added. The index-exhaustion fence is still applied on the peek path; all fail-closed surfaces (absent-file genesis, corrupt-frame, wrong-key, orphaned .new/.old, generation fence) are preserved exactly. Mutation evidence: restoring the open-path Allocate turns the new stable-across-opens test RED (Expected 1, Actual 2) and the genesis test RED.
…ence trips
Live testnet QA surfaced this: the refresh-observation generation fence requires
an exact before/after chain view across its multi-RPC acquisition (correctly — a
block landing mid-acquisition could hide a wallet transaction seen only at the
new tip). But the acquisition ran exactly once and the transient propagated, and
on a ~1-minute-block chain the ~60s candidate fetch window makes a mid-window
block nearly certain, so refresh was structurally unusable on a live chain.
The refresh command service now treats that specific fence trip
("node generation changed during the …") as a retry signal, not a hard failure:
it re-acquires the whole observation (candidate re-discovery + raw fetch at the
new tip, internally consistent per attempt) up to 6 attempts with 400ms
cancellation-aware backoff. Only ElementsRpcException carries the retryable
message; rollback/restart/inconsistency fences and generic RPC failures remain
fatal on the first attempt. The fence itself, accepted-send semantics, the
commit path, and the session/active-wallet fences are unchanged and run once.
Also fixed the review-surfaced disposal leak on the same path: the
final-observation fence block moved inside the try whose catch disposes fetched
raw transactions, so a final-fence trip (or a final status-fetch RPC failure)
now zeroes those buffers instead of abandoning them.
Mutation evidence: removing the message predicate (catching all
ElementsRpcException) turns the non-retryable test red (6 calls instead of 1).
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.
Long-running integration branch for the Wasabi-on-Liquid port.
Current scope:
Explicitly excluded from this branch until separately audited prerequisites are satisfied:
This draft is an integration and CI surface. It does not claim release or production readiness.