fix(dashpay): contested search row reflects vote state, not just document absence - #955
Conversation
…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>
📝 WalkthroughWalkthroughUsername 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. ChangesUsername contest states
Estimated code review effort: 3 (Moderate) | ~20 minutes Suggested reviewers: 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
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
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/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
📒 Files selected for processing (2)
DashWallet/Sources/UI/Explore Dash/UsernameMarketplaceScreen.swiftDashWallet/en.lproj/Localizable.strings
| /* 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"; | ||
|
|
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file -b --mime-encoding DashWallet/en.lproj/Localizable.stringsRepository: 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 || trueRepository: 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
| 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) |
There was a problem hiding this comment.
🎯 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.
| 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.
…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>
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:
RegisterNameSheet.alreadyRequestednow reuses the same view-model helper (hasRequestedContest(for:)) instead of carrying a duplicate.contestPrecheckafter the search results land, guarded against stale queries; while unknown the row keeps the generic contested wording (never fabricates a state).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
dashpayarm64 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:
For repository code-owners and collaborators only
🤖 Generated with Claude Code
Summary by CodeRabbit