Skip to content

fix(dashpay): make the voting screens escapable and vote one node at a time - #934

Merged
QuantumExplorer merged 1 commit into
developfrom
claude/voting-privacy-and-navigation
Aug 7, 2026
Merged

fix(dashpay): make the voting screens escapable and vote one node at a time#934
QuantumExplorer merged 1 commit into
developfrom
claude/voting-privacy-and-navigation

Conversation

@QuantumExplorer

@QuantumExplorer QuantumExplorer commented Aug 7, 2026

Copy link
Copy Markdown
Member

Three problems found on device.

1. You could not leave the contest screen

GovernanceMenuScreen hides its own navigation bar, and showVoting() pushed a bare UIHostingController. BaseNavigationController.willShow only restores the bar for a NavigationBarDisplayable, so a non-conforming controller inherits whatever the previous screen left — hidden.

No bar meant no back button and no way out. It also silently dropped .navigationTitle and the entire .toolbar — sort and "Vote on several" never rendered at all — because neither applies outside a navigation container.

Fixed the way showMasternodes directly above already does it: its own NavigationStack plus a back button wired explicitly to pop the UIKit stack. (That function even carries a comment explaining why it's needed; voting should have followed it.)

2. Voting cast every node's vote at once — a privacy loss

Broadcasting several MasternodeVote transitions for the same poll in quick succession lets an observer group those masternodes as one operator, and the user cannot undo that after the fact.

Voting is now one node per tap by default. Each tap casts with the next node that hasn't voted on this contest, so how many of your nodes get linked together is a decision rather than a side effect.

The mode is configurable where the node count is shown:

One node at a time (default) "Each tap votes with one more node, so you choose how many to link together."
All nodes at once "Every node votes together. Faster, but it publicly links your masternodes to each other."

Persisted in VotingPrefs, so it isn't re-chosen every launch. The control only appears with more than one node, where the modes actually differ.

3. Votes cast are now visible

  • Detail screen shows "2 of 5 nodes voted"
  • The button reads "Vote ×3" when a tap will use more than one node
  • Nodes that already voted are dropped from the picker, with a footer saying how many
  • Once every node has voted, the buttons give way to an explanation of how to change a vote

Why this needed persistence

It has to survive a relaunch, or the count silently resets to zero and lies. New masternode_vote_history table + VoteHistoryDAO, keyed per network so a testnet and a mainnet contest sharing a label never merge. Only accepted votes are recorded, so the count reflects what Platform took, not what was attempted. Cleared on wallet wipe.

Platform can't answer this cheaply: getContestedResourceVotersForIdentity is per contender, keyed by voter identity, and can't see abstain or lock voters at all — so answering "which of my nodes voted here" from the network would cost a query per contender and still be incomplete.

Testing

Clean dashpay build. ⚠️ Runtime verification is still outstanding — in particular the one-node-per-tap flow and the persisted count across a relaunch want a testnet pass with a real multi-node wallet.

Note develop currently needs the unmerged dashpay/platform#4332 to compile at all (invitationProspectiveIdentityId); built here against a local merge of v4.2-dev + that PR.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added vote progress tracking for username contests, including cast and remaining node counts.
    • Added options to vote with one node or all available nodes, with privacy guidance.
    • Previously voted nodes are excluded from future vote actions.
    • Voting controls now disable when all eligible nodes have voted or a contest closes.
    • Added localized status messages, vote totals, and pluralized node-count text.
  • Bug Fixes

    • Vote history now persists across sessions and networks, improving voting state accuracy.

…a time

Three problems found on device.

**You could not leave the contest screen.** `GovernanceMenuScreen` hides its
own navigation bar, and `showVoting()` pushed a bare `UIHostingController`.
`BaseNavigationController.willShow` only restores the bar for a
`NavigationBarDisplayable`, so a non-conforming controller inherits whatever the
previous screen left — hidden. No bar meant no back button and no way out, and
it silently dropped `.navigationTitle` and the whole `.toolbar` too (sort and
"Vote on several" never rendered), because neither applies outside a navigation
container.

Fixed the way `showMasternodes` directly above already does it: its own
`NavigationStack` plus a back button wired explicitly to pop the UIKit stack.

**Voting cast every node's vote at once, which is a privacy loss.** Broadcasting
several `MasternodeVote` transitions for one poll in quick succession lets an
observer group those masternodes as a single operator — and the user cannot
undo that afterwards. Voting is now **one node per tap by default**: each tap
casts with the next node that has not voted on this contest, so how many nodes
you link together is a choice rather than a side effect.

The mode is configurable where the node count is shown, as the user asked —
"One node at a time" (default) or "All nodes at once" — with a line explaining
the trade-off, persisted in `VotingPrefs` so it survives relaunch. The control
only appears with more than one node, where the two modes actually differ.

**How many votes you have cast is now visible.** The detail screen shows "2 of
5 nodes voted", the vote button reads "Vote ×3" when a tap will use more than
one node, nodes that already voted are dropped from the picker with a note
saying so, and once every node has voted the buttons give way to an explanation
of how to change a vote instead.

That needs to survive a relaunch, so votes are recorded in a new
`masternode_vote_history` table (`VoteHistoryDAO`), keyed per network so a
testnet and a mainnet contest sharing a label never merge. Only accepted votes
are recorded, so the count reflects what Platform took, not what was attempted.
Platform cannot answer this cheaply — `getContestedResourceVotersForIdentity`
is per contender, keyed by voter identity, and cannot see abstain or lock
voters at all. Cleared on wallet wipe.

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

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The wallet now persists successful masternode votes by contest and network. Voting screens exclude nodes that already voted, support one-node or all-node modes, show vote progress, and provide privacy guidance.

Changes

Voting history and node selection

Layer / File(s) Summary
Vote history storage
DashWallet/Sources/Infrastructure/Database/Migrations.bundle/..., DashWallet/Sources/Models/Voting/VoteHistoryDAO.swift, DashWallet.xcodeproj/project.pbxproj, DashWallet/Sources/Application/App.swift
Adds the masternode_vote_history table and SQLite-backed DAO. The project includes the DAO in both targets. Cleanup removes persisted history.
Successful vote recording and preferences
DashWallet/Sources/Infrastructure/SwiftDashSDK/Voting/MasternodeVoteCaster.swift, DashWallet/Sources/Models/Voting/VotingPrefs.swift
Successful casts persist network-scoped records. A persisted preference selects one-node or all-node voting.
History-aware voting orchestration
DashWallet/Sources/UI/DashPay/Voting/VotingViewModel.swift
The view model loads vote history, counts voted nodes, selects remaining nodes, and refreshes state after casting.
Voting controls and navigation
DashWallet/Sources/UI/DashPay/Voting/*, DashWallet/Sources/UI/Menu/Governance/GovernanceMenuScreen.swift, DashWallet/en.lproj/Localizable.strings*
The UI filters voted nodes, displays progress and privacy guidance, adds voting-mode controls, updates vote titles, and supports explicit navigation closure.

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

Sequence Diagram(s)

sequenceDiagram
  participant Voter
  participant VotingScreen
  participant VotingViewModel
  participant VoteHistoryDAOImpl
  participant MasternodeVoteCaster
  Voter->>VotingScreen: Open contest
  VotingScreen->>VotingViewModel: Load vote history
  VotingViewModel->>VoteHistoryDAOImpl: Query contest votes
  VoteHistoryDAOImpl-->>VotingViewModel: Return voted nodes
  VotingScreen->>VotingViewModel: Submit selected vote
  VotingViewModel->>MasternodeVoteCaster: Cast vote
  MasternodeVoteCaster->>VoteHistoryDAOImpl: Store successful vote
  VotingViewModel->>VoteHistoryDAOImpl: Reload contest history
  VoteHistoryDAOImpl-->>VotingScreen: Updated vote progress
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.78% 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 summarizes the two primary changes: escapable voting screens and one-node-at-a-time voting.
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 claude/voting-privacy-and-navigation

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

@QuantumExplorer
QuantumExplorer merged commit 82501ae into develop Aug 7, 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: 8

🧹 Nitpick comments (1)
DashWallet/Sources/UI/Menu/Governance/GovernanceMenuScreen.swift (1)

134-142: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use a wrapper that keeps the navigation bar visible.

UIHostingController globally conforms to NavigationBarDisplayable with isNavigationBarHidden == true. BaseNavigationController therefore hides the bar, which can hide the NavigationStack title, search field, and toolbar. Push a thin UIViewController wrapper that embeds the hosting controller and reports isNavigationBarHidden == false.

🤖 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/Menu/Governance/GovernanceMenuScreen.swift` around
lines 134 - 142, Wrap the UIHostingController containing UsernameVotingScreen in
a thin UIViewController that embeds it as a child and conforms to
NavigationBarDisplayable with isNavigationBarHidden set to false. Push this
wrapper instead of hosting directly in the vc.pushViewController flow,
preserving the NavigationStack’s title, search field, and toolbar.

Source: Learnings

🤖 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 4803-4809: Add the four missing voting localization keys—“%d of %d
nodes voted”, “Vote ×%d”, “%d of your nodes already voted here and are not
listed.”, and “Cast %u votes”—to the Localizable.strings entries using
BartyCrouch or Xcode-aware localization tooling, preserving the existing
key/value format.

In
`@DashWallet/Sources/Infrastructure/Database/Migrations.bundle/20260808030000_add_masternode_vote_history.sql`:
- Line 12: Rename the migration file containing the masternode_vote_history
CREATE TABLE statement to use its actual creation timestamp on or before August
7, 2026, rather than the future 20260808030000 timestamp. Preserve the migration
name suffix and ensure the updated timestamp maintains correct ordering with
other migrations.

In
`@DashWallet/Sources/Infrastructure/SwiftDashSDK/Voting/MasternodeVoteCaster.swift`:
- Around line 233-241: Update MasternodeVoteCaster’s successful Platform-vote
path to propagate the history record result or throw on failure, while retaining
retryable local-history handling after the vote succeeds; update App.swift
cleanup at lines 106-109 to propagate or throw the delete result so failed
history deletion cannot complete silently.

In `@DashWallet/Sources/Models/Voting/VoteHistoryDAO.swift`:
- Around line 82-86: Update VoteHistoryDAO.record to propagate database write
failures instead of logging and returning normally. Make
MasternodeVoteCaster.castAuthenticated handle the propagated failure by retrying
the durable vote-history write or reporting a recoverable persistence error
after Platform accepts the vote.
- Around line 139-141: Update VoteHistoryDAO.deleteAll() to be actor-isolated
and async instead of nonisolated and synchronous. In the wallet-wipe flow, wait
for active vote-recording tasks to finish or cancel them before awaiting
deleteAll(), ensuring no asynchronous record operation can recreate history
after deletion.

In `@DashWallet/Sources/UI/DashPay/Voting/VotingViewModel.swift`:
- Around line 185-187: Update castBulk to reload castCountsByContest via
history.voteCountsByContest(network:) after caster.castBulk succeeds, matching
the existing refresh() behavior so node vote totals reflect the bulk cast
immediately.
- Around line 185-187: Separate node progress from vote-attempt history in the
voting flow around voteCountsByContest: count distinct proTxHash values for
progress, while tracking attempts per proTxHash independently. Do not
permanently exclude nodes merely because they have prior history; after every
node has voted once, keep eligible nodes available for replacement votes until
the Platform limit is reached.
- Around line 261-269: The loadVotedNodes flow must distinguish loading and
failed history reads from a successfully empty history. Add label-scoped loading
and failure state around VoteHistoryDAO.votes, set loading before querying, mark
success only after a successful read, and preserve failure separately rather
than treating [] as valid history; update the displayed contest’s node-selection
and CastVoteSheet casting guards to remain disabled until that contest’s history
has loaded successfully.

---

Nitpick comments:
In `@DashWallet/Sources/UI/Menu/Governance/GovernanceMenuScreen.swift`:
- Around line 134-142: Wrap the UIHostingController containing
UsernameVotingScreen in a thin UIViewController that embeds it as a child and
conforms to NavigationBarDisplayable with isNavigationBarHidden set to false.
Push this wrapper instead of hosting directly in the vc.pushViewController flow,
preserving the NavigationStack’s title, search field, and toolbar.
🪄 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: 6c843b54-a38c-4581-9e84-cd242c582d12

📥 Commits

Reviewing files that changed from the base of the PR and between ce28ee4 and 30b2f5f.

📒 Files selected for processing (13)
  • DashWallet.xcodeproj/project.pbxproj
  • DashWallet/Sources/Application/App.swift
  • DashWallet/Sources/Infrastructure/Database/Migrations.bundle/20260808030000_add_masternode_vote_history.sql
  • DashWallet/Sources/Infrastructure/SwiftDashSDK/Voting/MasternodeVoteCaster.swift
  • DashWallet/Sources/Models/Voting/VoteHistoryDAO.swift
  • DashWallet/Sources/Models/Voting/VotingPrefs.swift
  • DashWallet/Sources/UI/DashPay/Voting/CastVoteSheet.swift
  • DashWallet/Sources/UI/DashPay/Voting/ContestDetailScreen.swift
  • DashWallet/Sources/UI/DashPay/Voting/UsernameVotingScreen.swift
  • DashWallet/Sources/UI/DashPay/Voting/VotingViewModel.swift
  • DashWallet/Sources/UI/Menu/Governance/GovernanceMenuScreen.swift
  • DashWallet/en.lproj/Localizable.strings
  • DashWallet/en.lproj/Localizable.stringsdict

Comment on lines +4803 to +4809
"Your votes" = "Your votes";
"All of your masternodes have voted on this username. To change a vote, vote again from the contender you now prefer." = "All of your masternodes have voted on this username. To change a vote, vote again from the contender you now prefer.";
"One node at a time" = "One node at a time";
"All nodes at once" = "All nodes at once";
"Every node votes together. Faster, but it publicly links your masternodes to each other." = "Every node votes together. Faster, but it publicly links your masternodes to each other.";
"Each tap votes with one more node, so you choose how many to link together." = "Each tap votes with one more node, so you choose how many to link together.";
"Selecting fewer nodes reveals less about which masternodes you run." = "Selecting fewer nodes reveals less about which masternodes you run.";

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

Add the remaining voting localization keys.

The new UI also uses these keys, but they are absent from this file:

  • "%d of %d nodes voted"
  • "Vote ×%d"
  • "%d of your nodes already voted here and are not listed."
  • "Cast %u votes"

Add them with BartyCrouch or Xcode-aware localization tooling. Otherwise, localized builds show English fallback text for these controls.

As per coding guidelines, use BartyCrouch or Xcode-aware tooling for Localizable.strings updates.

🤖 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 4803 - 4809, Add the
four missing voting localization keys—“%d of %d nodes voted”, “Vote ×%d”, “%d of
your nodes already voted here and are not listed.”, and “Cast %u votes”—to the
Localizable.strings entries using BartyCrouch or Xcode-aware localization
tooling, preserving the existing key/value format.

Source: Coding guidelines

--
-- `proTxHash` is stored in raw wire byte order, matching PlatformMasternode.
-- `castAt` is milliseconds since epoch.
CREATE TABLE masternode_vote_history (

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Use a non-future migration timestamp.

The filename 20260808030000_add_masternode_vote_history.sql encodes August 8, 2026. The review date is August 7, 2026. A migration created later on August 7 can sort before this migration in a timestamp-ordered runner. Rename this file with its actual creation timestamp before merge.

🤖 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/Infrastructure/Database/Migrations.bundle/20260808030000_add_masternode_vote_history.sql`
at line 12, Rename the migration file containing the masternode_vote_history
CREATE TABLE statement to use its actual creation timestamp on or before August
7, 2026, rather than the future 20260808030000 timestamp. Preserve the migration
name suffix and ensure the updated timestamp maintains correct ordering with
other migrations.

Source: Coding guidelines

Comment on lines +233 to +241
// Recorded only on success, so the count the UI shows is
// votes Platform accepted — not votes attempted.
await Self.history.record(
CastVoteRecord(
proTxHash: node.proTxHash,
normalizedLabel: normalizedLabel,
choice: choice,
castAt: Date()),
network: Self.networkKey)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Expose vote-history database failures to both callers.

The DAO swallows SQLite failures, so callers cannot guarantee that accepted votes are persisted or that wallet cleanup removed old history.

  • DashWallet/Sources/Infrastructure/SwiftDashSDK/Voting/MasternodeVoteCaster.swift#L233-L241: return or throw the record result and provide retryable local-history handling after a successful Platform vote.
  • DashWallet/Sources/Application/App.swift#L106-L109: return or throw the delete result and prevent cleanup from completing silently when deletion fails.
📍 Affects 2 files
  • DashWallet/Sources/Infrastructure/SwiftDashSDK/Voting/MasternodeVoteCaster.swift#L233-L241 (this comment)
  • DashWallet/Sources/Application/App.swift#L106-L109
🤖 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/Infrastructure/SwiftDashSDK/Voting/MasternodeVoteCaster.swift`
around lines 233 - 241, Update MasternodeVoteCaster’s successful Platform-vote
path to propagate the history record result or throw on failure, while retaining
retryable local-history handling after the vote succeeds; update App.swift
cleanup at lines 106-109 to propagate or throw the delete result so failed
history deletion cannot complete silently.

Comment on lines +82 to +86
do {
try db.run(query, bindings)
} catch {
DWLogger.log("VoteHistoryDAO: record failed: \(error)")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Do not hide vote-history write failures.

record logs the database error and returns normally. MasternodeVoteCaster.castAuthenticated records the vote after Platform accepts it. If this write fails, the next refresh or app launch reoffers a node that already voted.

Make record return or throw the persistence failure. The caller must retry the durable write or report a recoverable persistence failure.

🤖 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/Models/Voting/VoteHistoryDAO.swift` around lines 82 - 86,
Update VoteHistoryDAO.record to propagate database write failures instead of
logging and returning normally. Make MasternodeVoteCaster.castAuthenticated
handle the propagated failure by retrying the durable vote-history write or
reporting a recoverable persistence error after Platform accepts the vote.

Comment on lines +139 to +141
nonisolated func deleteAll() {
do {
try db.run("DELETE FROM masternode_vote_history")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate wallet-wipe deletion callers and DAO access sites.
ast-grep outline DashWallet/Sources/Models/Voting/VoteHistoryDAO.swift --items all --view expanded
rg -n -C 6 --glob '*.swift' 'VoteHistoryDAOImpl\.shared|VoteHistoryDAO|\.deleteAll\s*\(' DashWallet/Sources

Repository: dashpay/dashwallet-ios

Length of output: 12952


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- App.cleanUp context and callers ---'
sed -n '70,130p' DashWallet/Sources/Application/App.swift
rg -n -C 8 --glob '*.swift' 'cleanUp\s*\(|\.cleanUp\s*\(' DashWallet/Sources

printf '%s\n' '--- Vote caster write path ---'
sed -n '100,260p' DashWallet/Sources/Infrastructure/SwiftDashSDK/Voting/MasternodeVoteCaster.swift
rg -n -C 8 --glob '*.swift' 'history\.record|record\s*\(.*CastVoteRecord|castVote|cast\(' DashWallet/Sources/Infrastructure/SwiftDashSDK/Voting DashWallet/Sources/UI/DashPay/Voting

printf '%s\n' '--- DAO and database connection definitions ---'
sed -n '1,155p' DashWallet/Sources/Models/Voting/VoteHistoryDAO.swift
rg -n -C 8 --glob '*.swift' 'class DatabaseConnection|struct DatabaseConnection|static let shared.*DatabaseConnection|Connection\(' DashWallet/Sources

Repository: dashpay/dashwallet-ios

Length of output: 43142


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- Wallet wiper structure and call sites ---'
ast-grep outline DashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKWalletWiper.swift --items all --view expanded
sed -n '1,210p' DashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKWalletWiper.swift
rg -n -C 10 --glob '*.swift' 'SwiftDashSDKWalletWiper|wipeWallet|deleteWallet|removeWallet|wallet.*wipe|wipe.*wallet' DashWallet/Sources

printf '%s\n' '--- Vote-cast task and lifecycle coordination ---'
rg -n -C 8 --glob '*.swift' 'MasternodeVoteCaster|VotingViewModel|Task\s*\{|Task\.|cancel\s*\(|isCasting|cleanUp\s*\(' DashWallet/Sources/Infrastructure/SwiftDashSDK DashWallet/Sources/UI/DashPay/Voting DashWallet/Sources/Application

Repository: dashpay/dashwallet-ios

Length of output: 50379


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- SwiftDashSDKWalletWiper.swift ---'
wc -l DashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKWalletWiper.swift
sed -n '1,190p' DashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKWalletWiper.swift

printf '%s\n' '--- Exact App.cleanUp callers ---'
rg -n -C 12 --glob '*.swift' 'App(\.shared)?\.cleanUp|waitForPendingWipe|deleteWalletFromSDK' DashWallet/Sources/Infrastructure/SwiftDashSDK DashWallet/Sources/Application DashWallet/Sources/UI/Setup DashWallet/Sources/UI/Menu

printf '%s\n' '--- Vote task ownership ---'
rg -n -C 5 --glob '*.swift' 'Task\s*\{|Task\.|isCasting|MasternodeVoteCaster' DashWallet/Sources/UI/DashPay/Voting DashWallet/Sources/Infrastructure/SwiftDashSDK/Voting

Repository: dashpay/dashwallet-ios

Length of output: 50378


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

dao = Path("DashWallet/Sources/Models/Voting/VoteHistoryDAO.swift").read_text()
caster = Path("DashWallet/Sources/Infrastructure/SwiftDashSDK/Voting/MasternodeVoteCaster.swift").read_text()
app = Path("DashWallet/Sources/Application/App.swift").read_text()
wiper = Path("DashWallet/Sources/Infrastructure/SwiftDashSDK/SwiftDashSDKWalletWiper.swift").read_text()
sheet = Path("DashWallet/Sources/UI/DashPay/Voting/CastVoteSheet.swift").read_text()
bulk = Path("DashWallet/Sources/UI/DashPay/Voting/BulkVoteSheet.swift").read_text()

checks = {
    "deleteAll is nonisolated and synchronous": "nonisolated func deleteAll()" in dao,
    "record is async": "func record(_ record: CastVoteRecord, network: String) async" in dao,
    "successful SDK cast precedes record": (
        "try await sdk.castContestedResourceVote(" in caster
        and "await Self.history.record(" in caster
        and caster.index("try await sdk.castContestedResourceVote(")
            < caster.index("await Self.history.record(")
    ),
    "cleanup calls synchronous deleteAll": "VoteHistoryDAOImpl.shared.deleteAll()" in app,
    "cleanup is inside main sync": (
        "DispatchQueue.main.sync {" in wiper
        and wiper.index("DispatchQueue.main.sync {")
            < wiper.index("App.shared.cleanUp()")
    ),
    "single-vote UI starts unretained Task": "Task {" in sheet and "await viewModel.cast(" in sheet,
    "bulk-vote UI starts unretained Task": "Task { await viewModel.castBulk" in bulk,
}

for name, passed in checks.items():
    print(f"{'PASS' if passed else 'FAIL'}: {name}")
if not all(checks.values()):
    raise SystemExit(1)
PY

Repository: dashpay/dashwallet-ios

Length of output: 433


Coordinate wallet-wipe cleanup with vote recording.

A successful vote records history asynchronously after the SDK call returns. Wallet wipe can delete history before record runs.

Make deleteAll() actor-isolated and asynchronous. Await it after active vote tasks finish or cancel.

🤖 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/Models/Voting/VoteHistoryDAO.swift` around lines 139 -
141, Update VoteHistoryDAO.deleteAll() to be actor-isolated and async instead of
nonisolated and synchronous. In the wallet-wipe flow, wait for active
vote-recording tasks to finish or cancel them before awaiting deleteAll(),
ensuring no asynchronous record operation can recreate history after deletion.

Comment on lines +185 to +187
castCountsByContest = await history.voteCountsByContest(
network: MasternodeVoteCaster.networkKey)

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

Refresh progress after a bulk cast.

castCountsByContest updates only in refresh(). castBulk refreshes contest tallies but does not reload history counts. The list can therefore show stale “n of m nodes voted” values until the user performs a full refresh.

Reload voteCountsByContest(network:) after caster.castBulk succeeds.

🤖 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/DashPay/Voting/VotingViewModel.swift` around lines 185
- 187, Update castBulk to reload castCountsByContest via
history.voteCountsByContest(network:) after caster.castBulk succeeds, matching
the existing refresh() behavior so node vote totals reflect the bulk cast
immediately.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Separate node progress from vote-attempt history.

Line 268 counts vote records, not unique nodes. The DAO count query also uses COUNT(*). A replacement vote can make progress display as more than the wallet's node count.

Lines 257-259 then permanently exclude every node with prior history. When all nodes have one vote, this returns no nodes and disables the flow that the UI says can change a vote.

Count distinct proTxHash values for node progress. Track attempts per proTxHash separately. When all nodes have voted once, offer eligible nodes for replacement votes until they reach the Platform limit.

Also applies to: 256-280

🤖 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/DashPay/Voting/VotingViewModel.swift` around lines 185
- 187, Separate node progress from vote-attempt history in the voting flow
around voteCountsByContest: count distinct proTxHash values for progress, while
tracking attempts per proTxHash independently. Do not permanently exclude nodes
merely because they have prior history; after every node has voted once, keep
eligible nodes available for replacement votes until the Platform limit is
reached.

Comment on lines +261 to +269
/// Load which of our nodes already voted on one contest. Called when its
/// detail screen opens, so the vote button knows what is left.
func loadVotedNodes(for normalizedLabel: String) async {
let records = await history.votes(
forContest: normalizedLabel,
network: MasternodeVoteCaster.networkKey)
votedProTxHashesForOpenContest = Set(records.map(\.proTxHash))
castCountsByContest[normalizedLabel] = records.count
}

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 | 🟠 Major | ⚡ Quick win

Do not treat unknown history as an empty history.

votedProTxHashesForOpenContest starts empty while this asynchronous query runs. The detail screen can therefore open CastVoteSheet with all nodes selectable before this method completes.

VoteHistoryDAO.votes(forContest:network:) also returns [] when its database query fails. This method then enables the same duplicate-vote path after a history-read failure.

Add label-scoped loading and failure state. Disable node selection and casting until history for the displayed contest has loaded successfully.

🤖 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/DashPay/Voting/VotingViewModel.swift` around lines 261
- 269, The loadVotedNodes flow must distinguish loading and failed history reads
from a successfully empty history. Add label-scoped loading and failure state
around VoteHistoryDAO.votes, set loading before querying, mark success only
after a successful read, and preserve failure separately rather than treating []
as valid history; update the displayed contest’s node-selection and
CastVoteSheet casting guards to remain disabled until that contest’s history has
loaded successfully.

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