Skip to content

Marketplace Browse tab + scoped wallet-source fetches - #972

Merged
QuantumExplorer merged 8 commits into
developfrom
feat/marketplace-browse
Aug 10, 2026
Merged

Marketplace Browse tab + scoped wallet-source fetches#972
QuantumExplorer merged 8 commits into
developfrom
feat/marketplace-browse

Conversation

@QuantumExplorer

@QuantumExplorer QuantumExplorer commented Aug 10, 2026

Copy link
Copy Markdown
Member

What

Two bodies of work that landed on this branch:

Username-marketplace Browse tab (f4a74b11e0d641)

A Browse surface for names listed for sale across the network: recent-activity feed (price changes and purchases) driven by the document-history contract's listing trail, costing exactly 2 platform queries per page, with the price-changes feed filtered to names actually still for sale. Includes review-feedback fixes (cursor overlap, cancellable refresh, id validation, purchase counterparties) and the documentList orderBy ascending-BOOL fix.

Scoped wallet-source fetches (93c370a)

SwiftDashSDKWalletSource previously had one shape — materialize the ENTIRE wallet (TXO walk with two relationship faults per row, giant IN-refetch, per-tx CoinJoin classification traversals) — and every consumer paid it, including the Watch bridge (needs 100 rows), Coinbase metadata (needs stored hashes only), and swap matchers (need a time window). On a CoinJoin-heavy wallet one snapshot cost multiple seconds of SwiftData CPU; diagnosed at 700% CPU / 4.6GB during a mainnet recovery sync.

  • New scoped API: fetchRecent(limit:) / fetchRecent(firstSeenSince:) (firstSeen-index scan, membership as SQL EXISTS over the indexed PersistentTxo.walletId denorm + the involvedAccounts join, fetchLimit in the store, logged full-pass fallback if predicate translation ever regresses) and fetch(txids:) (unique-index point lookups).
  • Full pass restructured: one prefetched TXO scan yields membership + set-wise CoinJoin classification; no per-row faults. Scoped paths use a (txid, lastUpdated)-stamped classification cache seeded by full passes.
  • Callers migrated: Watch snapshot (newest 100 + cheap hasActiveWallet), CoinbaseMetadataProvider (stored-hash lookups + single-flight refresh), Coinbase pending-receive resolver and swap matchers (time-ranged), swap outbound-hash check (point lookup). Watch context sends coalesced to 15s.

Why

The Browse tab is the marketplace feature itself; the perf commit removes the SwiftData meltdown that made large (CoinJoin-heavy / recovered) wallets peg the CPU on every Watch context send, Coinbase refresh, and balance tick.

Verification

  • Clean dashpay arm64 simulator build; testnet smoke on the QA sim (restored-wallet state, relaunched cleanly).
  • Perf measured in-process on a synthetic 7,007-tx / 12,223-TXO CoinJoin-heavy mainnet store: Watch payload build 0.47–0.68s (was multi-second-plus), 8-txid metadata resolve 11ms, scoped SQL predicate translated with zero fallbacks, CoinJoin classification exactly matches the previous per-row rules (3,500/3,500 vs SQL ground truth), 526MB footprint.

Reviewer notes

  • The scoped EXISTS predicate shape is already shipped by TransactionObserver.scan (CrowdNode); the new code adds the involvedAccounts leg for payload-only membership (e.g. ProRegTx), verified translating on iOS 18.
  • Mixing classification is now evaluated from the wallet's OWN TXO roles (matches DashSync's per-wallet-account grouping); a cross-wallet tx no longer classifies from another on-device wallet's stake.
  • Follow-ups deliberately out of scope (tracked separately): paging the home list's full reload (still O(wallet), ~8.6s at 7k txs), and CrowdNode's doubled 6.5s restore scan at startup.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a Browse experience for username marketplace activity, including recent price changes and purchases.
    • Added pagination, pull-to-refresh, loading and empty states, sale-status details, and navigation to username details.
    • Added support for displaying historical marketplace events and current ownership information.
  • Bug Fixes

    • Improved transaction matching for swaps and pending Coinbase transfers.
    • Reduced duplicate wallet synchronization and metadata refresh operations.
  • Performance

    • Limited transaction searches to relevant time ranges and IDs for faster wallet activity loading.
    • Improved handling of CoinJoin transaction classification and marketplace data retrieval.

QuantumExplorer and others added 8 commits August 10, 2026 20:43
… by price

Third marketplace segment (Find Names / My Names / Browse): an
alphabetical scan over ALL DPNS names via the SDK's empty-prefix
searchDpnsMarketplace with its documentId cursor, keeping the listed
ones and sorting client-side (highest price first by default, menu
toggle for lowest).

Honesty by construction: $price is not an indexable property on Dash
Platform — there is no server-side "everything for sale ordered by
price" at any layer — so the sort is over what the scan has covered,
and the coverage line says exactly that ("500 names scanned · 12 for
sale", "All N names scanned" once exhausted). Each pass fetches 5
pages of 100; "Scan more names" continues, pull-to-refresh restarts
so listings re-read fresh. Rows reuse the search row (seller-clarity
line included) and open the standard detail sheet with the Buy flow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The alphabetical namespace scan was the wrong primitive: the
document-history system contract (platform #4348) records a
priceUpdate event for EVERY listing, indexed by
[dataContractId, $createdAt]. Browse now walks that trail newest-first
— every listed name necessarily has an event, so exhausting the trail
yields the complete current listing set at a cost proportional to
listing activity, not namespace size.

Each event's domain document resolves to its LIVE marketplace state
before it can appear (events say nothing about later re-prices,
delists, or purchases; the recorded event price is deliberately never
displayed), deduped so a many-times-relisted name costs one check.
Coverage line now counts listings checked; sort unchanged (client-side
high/low toggle — $price itself is still not indexable, so ordering
remains local).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… "desc"

The listing-trail query failed at runtime: "Invalid order by JSON:
invalid type: string \"desc\", expected a boolean". The FFI's order-by
tuples are [field, ascending-bool] — [["$createdAt",false]] for newest
first.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…urchases

Price sorting is not buildable server-side ($price is not indexable
anywhere), so stop approximating it. What the document-history trail
DOES index is recency — so Browse now shows exactly that, newest
first, in two feeds:

- Price changes: priceUpdate events ("Listed for 0.8 DASH · Aug 5").
- Purchases: purchase events with the price paid ("Sold for 0.002
  DASH · Aug 3"); buyer/seller ride along for the detail sheet.

Event price and time render as historical facts; the trailing badge is
the name's LIVE state (current For-sale price, or "Not for sale now"),
resolved per name with a per-refresh cache, so a stale listing can't
read as an offer. Cursor pagination ($createdAt) with Show more;
pull-to-refresh restarts both feeds. Also fixes identifier decoding
for this query path: custom identifier properties (documentId,
sellerId) arrive base64 while system fields are base58 — accepted
strictly as 32-byte identifiers either way.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A 25-event page resolved names one at a time — documentGet + nameState
per name, ~51 serialized round trips per page. The events already
carry the domain documentId and DPNS's primary index supports an "in"
clause, so all live states now come back in ONE batched documentList
(the domain document itself carries the full live state a row claims:
label, owner, current $price). Page cost: one events query + one
batched $id lookup, verified against live testnet via evo-sdk.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The live batch read now FILTERS instead of badging: events whose name
was since delisted or sold are dropped, and each for-sale name renders
once (its newest event — whose price is by consensus the live price).
A pass keeps paging (bounded, 4 pages) when filtering leaves a page
empty. Purchases stay a history feed. Still exactly 2 platform queries
per page — the batch read is what makes the for-sale filter possible
at all, since the append-only trail can't testify about the present
and $price isn't indexable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…fresh, id validation, purchase counterparties

- Pagination pages with "<=" and dedupes on the history row's own $id:
  a strict "<" cursor dropped the rest of a timestamp group at a page
  boundary (several events can share one block's $createdAt).
  Exhaustion reads the RAW page size; a full page of only-seen rows
  (cursor unable to advance) stops rather than spins.
- Pull-to-refresh awaits the restarted load (.refreshable spinner stays
  honest) and a reset cancels the in-flight task instead of bouncing
  off the busy guard; a generation counter keeps the cancelled task's
  cleanup from clearing the replacement's loading flag.
- liveDomainNames validates $id and $ownerId as exact 32-byte
  identifiers and keys the result by the canonical base58 form.
- Purchase rows show both counterparties ("seller → buyer" short ids);
  events missing either fall back to the price-and-date form.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ializing the whole wallet

SwiftDashSDKWalletSource previously had one shape — materialize every
wallet transaction (TXO walk with two relationship faults per row, giant
IN-refetch, per-tx CoinJoin classification traversals) — and every
consumer paid it: the Watch context send needs 100 rows, Coinbase
metadata needs the stored hashes, swap matchers need a time window. On a
CoinJoin-heavy wallet one snapshot cost multiple seconds of SwiftData
CPU (diagnosed at 700% CPU / 4.6GB during a mainnet recovery).

New scoped API (all wallet-scoped in SQL, verified index-backed):
- fetchRecent(limit:) / fetchRecent(firstSeenSince:) — firstSeen-index
  scan with EXISTS membership over the PersistentTxo.walletId denorm
  plus the involvedAccounts join, fetchLimit pushed to the store, and a
  logged fallback to the full pass if predicate translation ever breaks.
- fetch(txids:) — point lookups on the unique txid index.

Full pass restructured: one prefetched TXO scan (walletTxRollup) yields
the membership union AND set-wise CoinJoin classification — no per-row
faults; the display fetch prefetches outputs/inputs for the wrap.
Per-row classification for scoped paths is cached by (txid, lastUpdated)
and seeded by the full pass.

Callers migrated: Watch snapshot (newest 100 + cheap hasActiveWallet),
CoinbaseMetadataProvider (stored-hash lookups + single-flight refresh),
Coinbase pending-receive resolver (time-ranged), SwapOrderMetadataProvider
(outbound-hash point lookup + one shared ranged matcher fetch),
SwapTrackingService (ranged). Watch context sends are coalesced 15s.

Measured on a 7,007-tx / 12,223-TXO CoinJoin-heavy wallet (QA sim):
Watch payload build 0.47-0.68s (was multi-second-plus), 8-txid metadata
resolve 11ms, full pass 8.6s with classification exactly matching the
old rules (3,500/3,500 ground truth), scoped predicate translated with
zero fallbacks, 526MB footprint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c4d90e24-3d2b-4bf2-be7a-da70ce265880

📥 Commits

Reviewing files that changed from the base of the PR and between 4ac2e0b and 93c370a.

📒 Files selected for processing (10)
  • DashWallet/Sources/AppleWatch/DWPhoneWCSessionManager.m
  • DashWallet/Sources/Infrastructure/SwiftDashSDK/UsernameMarketplaceService.swift
  • DashWallet/Sources/Models/Coinbase/Coinbase.swift
  • DashWallet/Sources/Models/Swap/SwapBuyTransactionMatcher.swift
  • DashWallet/Sources/Models/Swap/SwapTrackingService.swift
  • DashWallet/Sources/UI/Explore Dash/UsernameMarketplaceScreen.swift
  • DashWallet/Sources/UI/Home/Tx Metadata/CoinbaseMetadataProvider.swift
  • DashWallet/Sources/UI/Home/Tx Metadata/SwapOrderMetadataProvider.swift
  • DashWallet/Sources/UI/Home/Views/HomeViewModel.swift
  • DashWallet/en.lproj/Localizable.strings

📝 Walkthrough

Walkthrough

The PR adds username marketplace activity feeds, reconciles historical events with live DPNS state, coalesces Watch and metadata refreshes, and replaces several full-wallet transaction scans with bounded or targeted queries.

Changes

Marketplace activity browsing

Layer / File(s) Summary
Marketplace history and live state
DashWallet/Sources/Infrastructure/SwiftDashSDK/UsernameMarketplaceService.swift
The service adds paginated listing and purchase history queries, event mapping, identifier validation, and batched live DPNS document resolution.
Browse feeds and presentation
DashWallet/Sources/UI/Explore Dash/UsernameMarketplaceScreen.swift, DashWallet/en.lproj/Localizable.strings
The marketplace screen adds price-change and purchase feeds, pagination, refresh, filtering, live sale status, detail navigation, and localized labels.

Bounded wallet transaction access

Layer / File(s) Summary
Scoped wallet queries and classification
DashWallet/Sources/UI/Home/Views/HomeViewModel.swift
Wallet access adds recent, cutoff-based, and txid-based queries, wallet membership checks, rollup processing, fallback handling, and cached CoinJoin classification.
Coinbase and swap transaction consumers
DashWallet/Sources/Models/Coinbase/Coinbase.swift, DashWallet/Sources/Models/Swap/SwapBuyTransactionMatcher.swift, DashWallet/Sources/Models/Swap/SwapTrackingService.swift
Coinbase receive resolution and swap tracking use bounded transaction queries. Swap cutoff calculation includes matcher slack and date skew allowances.
Shared swap metadata matching
DashWallet/Sources/UI/Home/Tx Metadata/SwapOrderMetadataProvider.swift
Swap metadata matching uses one shared recent transaction pool and direct outbound transaction lookup with validation.
Coalesced Coinbase metadata refresh
DashWallet/Sources/UI/Home/Tx Metadata/CoinbaseMetadataProvider.swift
Refresh requests are serialized with one trailing rerun. Metadata resolution queries only transactions matching stored Coinbase hashes.

Watch context updates

Layer / File(s) Summary
Delayed application-context scheduling
DashWallet/Sources/AppleWatch/DWPhoneWCSessionManager.m
Balance and sync events now schedule one delayed application-context send. Duplicate requests during the pending interval are ignored.

Estimated code review effort: 5 (Critical) | ~90+ minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant UsernameMarketplaceScreen
  participant UsernameMarketplaceService
  participant DPNSHistory
  participant DPNSDocuments
  User->>UsernameMarketplaceScreen: Select Browse feed
  UsernameMarketplaceScreen->>UsernameMarketplaceService: Load page with cursor
  UsernameMarketplaceService->>DPNSHistory: Query historical events
  DPNSHistory-->>UsernameMarketplaceService: Events and next cursor
  UsernameMarketplaceService->>DPNSDocuments: Resolve live document state
  DPNSDocuments-->>UsernameMarketplaceService: Current ownership and sale state
  UsernameMarketplaceService-->>UsernameMarketplaceScreen: Feed rows
  UsernameMarketplaceScreen-->>User: Render activity and pagination
Loading

Possibly related PRs

Suggested reviewers: romchornyi

✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/marketplace-browse

Comment @coderabbitai help to get the list of available commands.

@QuantumExplorer
QuantumExplorer merged commit 3888fc2 into develop Aug 10, 2026
0 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant