Skip to content

feat(dashpay): Browse tab — recent price changes and purchases across the network - #962

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

feat(dashpay): Browse tab — recent price changes and purchases across the network#962
QuantumExplorer merged 7 commits into
developfrom
feat/marketplace-browse

Conversation

@QuantumExplorer

@QuantumExplorer QuantumExplorer commented Aug 10, 2026

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

"Can we see the most expensive names for sale?" — until now the marketplace was search-driven only, because $price is 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 — $price appears only in the price-update transition, and DPNS v2 carries just the parentNameAndLabel + records.identity indices).

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:

  • Price changes: priceUpdate events newest-first ("Listed for 0.8 DASH · Aug 5").
  • Purchases: purchase events with the price paid and both counterparties.
  • Event price/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 — a stale listing can never read as an offer.
  • $createdAt cursor pagination with Show more; pull-to-refresh restarts both feeds.
  • Includes the identifier-encoding fix this query path needed: custom identifier properties (documentId, sellerId) arrive base64 while system fields are base58; both accepted strictly as 32-byte identifiers.
  • Queries verified against live testnet data via evo-sdk (real priceUpdate/purchase rows returned by the exact where/orderBy the app uses).

Server-side price ordering remains impossible at every layer ($price is not indexable; verified in rs-dpp/rs-drive) — the purchase.byPrice index could later power a "top sales" leaderboard if wanted.

How Has This Been Tested?

Clean dashpay arm64 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:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have made corresponding changes to the documentation

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added a Browse view for username marketplace activity.
    • Added separate feeds for recent price changes and purchases.
    • Added paginated “Show more” loading, automatic loading, and pull-to-refresh.
    • Added historical event details with current sale-status badges and live availability filtering.
    • Added loading, empty, and error states for marketplace activity.
  • Localization
    • Added English labels and messages for browsing, activity feeds, sale details, loading, and empty states.

… 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]>
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@QuantumExplorer, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 32 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8f67adaf-c3b7-4637-a2db-6341ba1878ee

📥 Commits

Reviewing files that changed from the base of the PR and between 8dae116 and 1e0d641.

📒 Files selected for processing (3)
  • DashWallet/Sources/Infrastructure/SwiftDashSDK/UsernameMarketplaceService.swift
  • DashWallet/Sources/UI/Explore Dash/UsernameMarketplaceScreen.swift
  • DashWallet/en.lproj/Localizable.strings
📝 Walkthrough

Walkthrough

Added 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.

Changes

Marketplace Activity Browse

Layer / File(s) Summary
Marketplace event retrieval
DashWallet/Sources/Infrastructure/SwiftDashSDK/UsernameMarketplaceService.swift
Defines MarketplaceEvent and retrieves newest-first priceUpdate and purchase records. The service supports cursor pagination, multiple response shapes, base64 and base58 identifiers, and malformed-record filtering.
Live DPNS state resolution
DashWallet/Sources/Infrastructure/SwiftDashSDK/UsernameMarketplaceService.swift
Adds batched live DPNS document lookup by $id. The lookup returns current labels, owners, and sale prices and omits incomplete documents.
Browse feeds and presentation
DashWallet/Sources/UI/Explore Dash/UsernameMarketplaceScreen.swift, DashWallet/en.lproj/Localizable.strings
Adds separate price-change and purchase feeds with 25-event pagination, cursors, exhaustion tracking, caching, refresh, loading and empty states, event details, sale-status badges, navigation, and localized strings.

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
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the new Browse tab and its recent price-change and purchase feeds.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 6c1b762 and f4a74b1.

📒 Files selected for processing (3)
  • DashWallet/Sources/Infrastructure/SwiftDashSDK/UsernameMarketplaceService.swift
  • DashWallet/Sources/UI/Explore Dash/UsernameMarketplaceScreen.swift
  • DashWallet/en.lproj/Localizable.strings

Comment thread DashWallet/Sources/UI/Explore Dash/UsernameMarketplaceScreen.swift Outdated
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]>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between f4a74b1 and 15efdff.

📒 Files selected for processing (3)
  • DashWallet/Sources/Infrastructure/SwiftDashSDK/UsernameMarketplaceService.swift
  • DashWallet/Sources/UI/Explore Dash/UsernameMarketplaceScreen.swift
  • DashWallet/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

Comment thread DashWallet/Sources/Infrastructure/SwiftDashSDK/UsernameMarketplaceService.swift Outdated
Comment thread DashWallet/Sources/Infrastructure/SwiftDashSDK/UsernameMarketplaceService.swift Outdated
QuantumExplorer and others added 2 commits August 10, 2026 21:04
… "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]>
@QuantumExplorer QuantumExplorer changed the title feat(dashpay): Browse tab — names for sale across the network, sorted by price feat(dashpay): Browse tab — recent price changes and purchases across the network Aug 10, 2026
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]>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 15efdff and 0d888e7.

📒 Files selected for processing (3)
  • DashWallet/Sources/Infrastructure/SwiftDashSDK/UsernameMarketplaceService.swift
  • DashWallet/Sources/UI/Explore Dash/UsernameMarketplaceScreen.swift
  • DashWallet/en.lproj/Localizable.strings

Comment thread DashWallet/Sources/Infrastructure/SwiftDashSDK/UsernameMarketplaceService.swift Outdated
Comment thread DashWallet/Sources/UI/Explore Dash/UsernameMarketplaceScreen.swift Outdated
QuantumExplorer and others added 2 commits August 10, 2026 21:54
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]>
@QuantumExplorer
QuantumExplorer merged commit 424ee13 into develop Aug 10, 2026
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