Marketplace Browse tab + scoped wallet-source fetches - #972
Merged
Conversation
… 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>
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (10)
📝 WalkthroughWalkthroughThe 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. ChangesMarketplace activity browsing
Bounded wallet transaction access
Watch context updates
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
Possibly related PRs
Suggested reviewers: ✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
5 tasks
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.
What
Two bodies of work that landed on this branch:
Username-marketplace Browse tab (
f4a74b1…1e0d641)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
documentListorderBy ascending-BOOL fix.Scoped wallet-source fetches (
93c370a)SwiftDashSDKWalletSourcepreviously 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.fetchRecent(limit:)/fetchRecent(firstSeenSince:)(firstSeen-index scan, membership as SQL EXISTS over the indexedPersistentTxo.walletIddenorm + theinvolvedAccountsjoin,fetchLimitin the store, logged full-pass fallback if predicate translation ever regresses) andfetch(txids:)(unique-index point lookups).(txid, lastUpdated)-stamped classification cache seeded by full passes.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
dashpayarm64 simulator build; testnet smoke on the QA sim (restored-wallet state, relaunched cleanly).Reviewer notes
TransactionObserver.scan(CrowdNode); the new code adds theinvolvedAccountsleg for payload-only membership (e.g. ProRegTx), verified translating on iOS 18.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Performance