feat(dashpay): Browse tab — recent price changes and purchases across the network - #962
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 <[email protected]>
|
Warning Review limit reached
Next review available in: 32 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughAdded marketplace activity browsing for price changes and purchases. The service retrieves paginated history records, resolves current DPNS name state in batches, and filters invalid or unavailable records. The browse view supports caching, refresh, incremental loading, and localized states. ChangesMarketplace Activity Browse
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant UsernameMarketplaceScreen
participant UsernameMarketplaceService
participant SwiftDashSDK
User->>UsernameMarketplaceScreen: open Browse segment or select feed
UsernameMarketplaceScreen->>UsernameMarketplaceService: request a 25-event page
UsernameMarketplaceService->>SwiftDashSDK: query marketplace history
SwiftDashSDK-->>UsernameMarketplaceService: return valid history records
UsernameMarketplaceService->>SwiftDashSDK: batch query live DPNS documents
SwiftDashSDK-->>UsernameMarketplaceService: return current name state
UsernameMarketplaceService-->>UsernameMarketplaceScreen: return events with live state
UsernameMarketplaceScreen-->>User: show activity rows and sale status
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@DashWallet/Sources/UI/Explore` Dash/UsernameMarketplaceScreen.swift:
- Around line 117-147: Refactor scanBrowsePages into an async scan operation
that callers can await, and update the .refreshable handler to await it so
refresh remains active until scanning completes. Ensure reset invalidates or
cancels any in-progress scan before clearing browseCursor, browseForSale,
browseScannedCount, and browseExhausted; do not let the active-scan guard
prevent refresh from restarting the browse scan.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 5e2439cc-24da-49b4-bdc6-ecfd1002f99f
📒 Files selected for processing (3)
DashWallet/Sources/Infrastructure/SwiftDashSDK/UsernameMarketplaceService.swiftDashWallet/Sources/UI/Explore Dash/UsernameMarketplaceScreen.swiftDashWallet/en.lproj/Localizable.strings
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 <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@DashWallet/Sources/Infrastructure/SwiftDashSDK/UsernameMarketplaceService.swift`:
- Around line 118-145: Update listingEventsPage and scanBrowsePages to use a
collision-free pagination cursor containing both $createdAt and documentId,
applying a strict tie-breaker so rows sharing a timestamp are not skipped.
Return explicit backend completion metadata alongside parsed ListingEvent
values, and make scanBrowsePages continue or stop based on that metadata rather
than the compactMap result count.
- Around line 156-164: The document fetch handling around sdk.documentGet in
UsernameMarketplaceService must return nil only for the SDK’s confirmed
not-found/deletion error; rethrow network, decoding, authorization, and other
failures. Preserve the failed document ID before advancing the browse cursor so
subsequent scans can retry that document.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 4ed05c0e-6d25-4cff-a963-e07fee13955b
📒 Files selected for processing (3)
DashWallet/Sources/Infrastructure/SwiftDashSDK/UsernameMarketplaceService.swiftDashWallet/Sources/UI/Explore Dash/UsernameMarketplaceScreen.swiftDashWallet/en.lproj/Localizable.strings
🚧 Files skipped from review as they are similar to previous changes (2)
- DashWallet/en.lproj/Localizable.strings
- DashWallet/Sources/UI/Explore Dash/UsernameMarketplaceScreen.swift
… "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 <[email protected]>
…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 <[email protected]>
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 <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@DashWallet/Sources/Infrastructure/SwiftDashSDK/UsernameMarketplaceService.swift`:
- Around line 222-230: Update the live document mapping around LiveDomainName
creation to validate both the `$id` and `$ownerId` values with
`identifier32(_:)`, rather than accepting arbitrary-length decoded data. Use the
canonical base58 representation of the validated document identifier as the
`out` dictionary key, while preserving the existing label and price handling.
In `@DashWallet/Sources/UI/Explore` Dash/UsernameMarketplaceScreen.swift:
- Around line 589-595: Update the purchase-event subtitle in the browse-feed row
around browseFeed and MarketplaceEvent to include shortened buyerIdBase58 and
sellerIdBase58 values alongside the price and date. Add the required localized
format string, and ensure events missing either counterparty are excluded or
display an explicit unavailable value; leave the price-change subtitle
unchanged.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 996907d8-ea59-4d94-885a-b277be872a96
📒 Files selected for processing (3)
DashWallet/Sources/Infrastructure/SwiftDashSDK/UsernameMarketplaceService.swiftDashWallet/Sources/UI/Explore Dash/UsernameMarketplaceScreen.swiftDashWallet/en.lproj/Localizable.strings
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 <[email protected]>
…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 <[email protected]>
Issue being fixed or feature implemented
"Can we see the most expensive names for sale?" — until now the marketplace was search-driven only, because
$priceis not an indexable property on Dash Platform: there is no server-side "everything for sale ordered by price" query at any layer (re-verified against current rs-dpp/rs-drive —$priceappears only in the price-update transition, and DPNS v2 carries just theparentNameAndLabel+records.identityindices).What was done
A third segment — Find Names / My Names / Browse. After two iterations (alphabetical namespace scan, then a complete-listing-set reconstruction), Browse landed as what the history trail actually indexes — recency:
priceUpdateevents newest-first ("Listed for 0.8 DASH · Aug 5").purchaseevents with the price paid and both counterparties.$createdAtcursor pagination with Show more; pull-to-refresh restarts both feeds.documentId,sellerId) arrive base64 while system fields are base58; both accepted strictly as 32-byte identifiers.Server-side price ordering remains impossible at every layer (
$priceis not indexable; verified in rs-dpp/rs-drive) — thepurchase.byPriceindex could later power a "top sales" leaderboard if wanted.How Has This Been Tested?
Clean
dashpayarm64 simulator build; installed on the testnet QA simulator (which has real listed names, including Quantumtester2 at 1.4 DASH) — verification in the same QA session. (Unit-test target pre-existing broken.)Breaking Changes
None.
Checklist:
For repository code-owners and collaborators only
🤖 Generated with Claude Code
Summary by CodeRabbit