Skip to content

fix(dashpay): contested search row reflects vote state, not just document absence - #955

Merged
QuantumExplorer merged 1 commit into
developfrom
fix/marketplace-contested-search-row
Aug 10, 2026
Merged

fix(dashpay): contested search row reflects vote state, not just document absence#955
QuantumExplorer merged 1 commit into
developfrom
fix/marketplace-contested-search-row

Conversation

@QuantumExplorer

@QuantumExplorer QuantumExplorer commented Aug 10, 2026

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

Testnet QA on the username marketplace: after submitting a contested request for "greg" (visible under My Names → "In network vote"), searching "Greg" on Find Names still showed "Available — short names are decided by a network vote" with the green register affordance. The register row decided "available" purely from DPNS document absence — and a label mid-vote has no document until the vote resolves, so every contested name in an active vote (yours or anyone's) read as available.

What was done

The row now reflects the contest state, in precedence order:

  • Requested by this identity → hourglass, "Requested by you — the network vote is in progress". Answered synchronously from the SDK's contested-names cache plus the submission bookmark (covers an unsynced cache). RegisterNameSheet.alreadyRequested now reuses the same view-model helper (hasRequestedContest(for:)) instead of carrying a duplicate.
  • Someone else's active vote → "In a network vote — you can join as a contender". Filled best-effort by contestPrecheck after the search results land, guarded against stale queries; while unknown the row keeps the generic contested wording (never fabricates a state).
  • Locked by a past vote → "Locked by a network vote — nobody can register it".
  • Genuinely fresh → unchanged "Available" wording.

Tapping any state still opens RegisterNameSheet, whose existing callouts (already-requested / join-as-contender / locked, and the one-request-at-a-time limit) handle the rest.

How Has This Been Tested?

Clean dashpay arm64 simulator build; installed on the testnet QA simulator carrying a real in-flight contested request ("greg", vote ends today) — verification screenshots to follow on this PR. (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 clearer status indicators for contested username registrations, including active votes, locked names, available names, and previously requested names.
    • Registration options now display relevant icons, colors, and explanatory text.
    • Username searches now refresh contest availability and avoid unnecessary repeat checks.

…ment absence

After starting a contest for "greg", searching it still showed
"Available — short names are decided by a network vote": the register
row only checked for an existing DPNS document, and a mid-vote label
has none until the vote resolves.

The row now states the contest reality:
- requested by this identity → hourglass, "Requested by you — the
  network vote is in progress" (answered locally from the SDK cache +
  submission bookmark; RegisterNameSheet's alreadyRequested now reuses
  the same view-model helper instead of duplicating it)
- someone else's active vote → "In a network vote — you can join as a
  contender" (best-effort contestPrecheck after results land, staleness
  guarded)
- locked by a past vote → "Locked by a network vote — nobody can
  register it"
- genuinely fresh → unchanged "Available" wording

Tapping still opens RegisterNameSheet, whose callouts already handle
each state.

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

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Username search now performs asynchronous contest checks for registrable labels and reuses shared pending-request detection. Registration rows and the registration sheet display distinct contest states with matching English localization.

Changes

Username contest states

Layer / File(s) Summary
Contest precheck and request detection
DashWallet/Sources/UI/Explore Dash/UsernameMarketplaceScreen.swift
Search resets contest state, checks eligible labels asynchronously, ignores stale results, and centralizes pending-request detection.
Registration state presentation
DashWallet/Sources/UI/Explore Dash/UsernameMarketplaceScreen.swift, DashWallet/en.lproj/Localizable.strings
Registration rows and the registration sheet distinguish pending requests, active votes, locked labels, and available labels with updated icons, colors, subtitles, and localized text.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: jeanpierreroma

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant UsernameMarketplaceScreen
  participant UsernameMarketplaceViewModel
  participant ContestSDK

  User->>UsernameMarketplaceScreen: Enter username search
  UsernameMarketplaceScreen->>UsernameMarketplaceViewModel: Load matching labels
  UsernameMarketplaceViewModel->>ContestSDK: Check contest state
  ContestSDK-->>UsernameMarketplaceViewModel: Return contest result
  UsernameMarketplaceViewModel-->>UsernameMarketplaceScreen: Update registration state
  UsernameMarketplaceScreen-->>User: Show contest status and registration action
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 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 main change: showing contested username vote states in search rows.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/marketplace-contested-search-row

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

@QuantumExplorer
QuantumExplorer merged commit 52057ff into develop Aug 10, 2026
2 of 3 checks passed

@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/en.lproj/Localizable.strings`:
- Around line 755-760: Convert DashWallet/en.lproj/Localizable.strings to
UTF-16LE while preserving all localized keys and values, and ensure the file
remains managed by Xcode or BartyCrouch.

In `@DashWallet/Sources/UI/Explore` Dash/UsernameMarketplaceScreen.swift:
- Around line 559-573: Update the icon foreground color in the Button label
built by the username marketplace row to use the selected tint directly, so
contested available entries retain .dashGolden while regular available entries
remain .dashGreen.
🪄 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: f93590dc-6d67-4115-abc1-7bb3f995e821

📥 Commits

Reviewing files that changed from the base of the PR and between 7e6b393 and 59a0359.

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

Comment on lines +755 to +760
/* Username marketplace: search row for a contested label with an active vote by others */
"In a network vote — you can join as a contender" = "In a network vote — you can join as a contender";

/* Username marketplace: search row for a label a past vote locked */
"Locked by a network vote — nobody can register it" = "Locked by a network vote — nobody can register it";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

file -b --mime-encoding DashWallet/en.lproj/Localizable.strings

Repository: dashpay/dashwallet-ios

Length of output: 202


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

path = Path("DashWallet/en.lproj/Localizable.strings")
data = path.read_bytes()

print(f"path={path}")
print(f"size={len(data)}")
print(f"bom={data[:2].hex()}")
try:
    text = data.decode("utf-16-le")
    print("utf-16le=decodable")
    print(f"nul_ratio={data.count(0) / len(data):.3f}")
    for key in (
        "In a network vote — you can join as a contender",
        "Locked by a network vote — nobody can register it",
    ):
        print(f"{key!r}={key in text}")
except UnicodeDecodeError as exc:
    print(f"utf-16le=not_decodable:{exc}")
PY

printf '\nRelevant repository configuration:\n'
rg -n -i 'BartyCrouch|Localizable\.strings|UTF-16|utf-16' \
  --glob '!DashWallet/**/Localizable.strings' \
  .github . 2>/dev/null | head -80 || true

Repository: dashpay/dashwallet-ios

Length of output: 12569


Convert DashWallet/en.lproj/Localizable.strings to UTF-16LE. Keep it managed by Xcode or BartyCrouch.

🤖 Prompt for 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.

In `@DashWallet/en.lproj/Localizable.strings` around lines 755 - 760, Convert
DashWallet/en.lproj/Localizable.strings to UTF-16LE while preserving all
localized keys and values, and ensure the file remains managed by Xcode or
BartyCrouch.

Source: Coding guidelines

Comment on lines +559 to +573
icon = "plus.circle.fill"
tint = .dashGolden
subtitle = NSLocalizedString("Available — short names are decided by a network vote", comment: "Username marketplace: unregistered contested-eligible name row")
} else {
icon = "plus.circle.fill"
tint = .dashGreen
subtitle = NSLocalizedString("Available — register it on your identity", comment: "Username marketplace: unregistered name row")
}
return Button {
registerCandidate = RegisterCandidate(label: label)
} label: {
HStack(spacing: 10) {
Image(systemName: "plus.circle.fill")
Image(systemName: icon)
.font(.system(size: 26))
.foregroundColor(.dashGreen)
.foregroundColor(icon == "plus.circle.fill" ? .dashGreen : tint)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use the selected tint for the contested available icon.

Line 573 forces every "plus.circle.fill" icon to .dashGreen. The contested fresh case on Lines 559-561 selects .dashGolden, but the icon renders green while its subtitle renders gold.

Proposed fix
-                    .foregroundColor(icon == "plus.circle.fill" ? .dashGreen : tint)
+                    .foregroundColor(tint)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
icon = "plus.circle.fill"
tint = .dashGolden
subtitle = NSLocalizedString("Available — short names are decided by a network vote", comment: "Username marketplace: unregistered contested-eligible name row")
} else {
icon = "plus.circle.fill"
tint = .dashGreen
subtitle = NSLocalizedString("Available — register it on your identity", comment: "Username marketplace: unregistered name row")
}
return Button {
registerCandidate = RegisterCandidate(label: label)
} label: {
HStack(spacing: 10) {
Image(systemName: "plus.circle.fill")
Image(systemName: icon)
.font(.system(size: 26))
.foregroundColor(.dashGreen)
.foregroundColor(icon == "plus.circle.fill" ? .dashGreen : tint)
icon = "plus.circle.fill"
tint = .dashGolden
subtitle = NSLocalizedString("Available — short names are decided by a network vote", comment: "Username marketplace: unregistered contested-eligible name row")
} else {
icon = "plus.circle.fill"
tint = .dashGreen
subtitle = NSLocalizedString("Available — register it on your identity", comment: "Username marketplace: unregistered name row")
}
return Button {
registerCandidate = RegisterCandidate(label: label)
} label: {
HStack(spacing: 10) {
Image(systemName: icon)
.font(.system(size: 26))
.foregroundColor(tint)
🤖 Prompt for 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.

In `@DashWallet/Sources/UI/Explore` Dash/UsernameMarketplaceScreen.swift around
lines 559 - 573, Update the icon foreground color in the Button label built by
the username marketplace row to use the selected tint directly, so contested
available entries retain .dashGolden while regular available entries remain
.dashGreen.

@QuantumExplorer QuantumExplorer changed the title fix(dashpay): contested search row reflects vote state, not just document absence feat(dashpay): contested-name UX — truthful search row and live vote details Aug 10, 2026
@QuantumExplorer QuantumExplorer changed the title feat(dashpay): contested-name UX — truthful search row and live vote details fix(dashpay): contested search row reflects vote state, not just document absence Aug 10, 2026
QuantumExplorer added a commit that referenced this pull request Aug 10, 2026
…tplace reality (#965)

* feat(dashpay): create-username form reflects contested-vote and marketplace reality

Typing a contested-eligible name that is already mid-vote showed a green
"Username available" plus a generic "requires a masternode vote" —
nothing said a vote was running, when it ends, or whether it can still
be joined. Typing a name listed for sale showed a dead-end "taken".

The availability check now runs the same contestPrecheck the search row
uses (#955) and the form states each case:

- vote running, join window open: "voting ends around <deadline> —
  submitting joins the vote as a contender" (deadline from the
  current-contests listing, "Today 20:33"-style formatting)
- join window closed (poll end − 45 min testnet / − 1 week mainnet,
  mirroring rs-platform-version allow_other_contenders_time): red
  "new contenders can no longer join" and Continue disabled — a submit
  could only fail at broadcast
- vote ended, awaiting finalization: "Vote ended … being finalized"
- own pending request: unchanged "in voting" row, now also answered
  from the SDK contested-names cache when the bookmark is gone
- taken but listed in the marketplace: "For sale" callout with the
  price instead of a bare "taken"
- plainly taken: the contradictory "requires a masternode vote"
  callout no longer shows — an owned name will never go to a vote

Correctness and load fixes underneath:

- contestPrecheck and nameState normalize the label before querying;
  the vote-poll and marketplace indexes key on the normalized form, so
  "Greg" reported fresh while "greg" was mid-vote (also fixes the
  latent capitalized-input miss in the #955 marketplace row)
- the DPNS availability check runs once per typed label instead of on
  every balance/credits/readiness publish (constant during sync); a
  one-shot re-check fires at the contest's join-close and end
  boundaries, and each screen visit re-verifies once
- isContestedNameLocked is removed — the precheck subsumes it

Also: arriving at the form through the readiness interstitial (its
shielded checklist or the explicit "Use transparent balance instead"
escape) no longer re-teases Private registration.

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

* refactor(dashpay): address #965 review — VM-owned contest state, join-window submit guard

- Move activeContestHasEnded / activeContestJoinClosed /
  showContestedWarning from CreateUsernameView into the view model, so
  the protocol-derived join-window rule and callout policy live next to
  the submission gating (SwiftUI-first guideline).
- "Submit anyway" re-checks joinability at the moment of confirmation:
  the alert can sit open across the join-window boundary, and a
  submission the network would refuse is now replaced by a fresh
  availability answer instead of a broadcast failure.
- State the plainly-taken exception in the contested-warning comment.
- SwiftLint: drop redundant "= nil" optional initializers, use
  `case let .activeContest(...)` (pattern_matching_keywords).

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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