feat(voting): derive keys through the platform wallet; rework node selection - #936
Conversation
|
Warning Review limit reached
Next review available in: 33 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 (6)
📝 WalkthroughWalkthroughThe PR replaces the all-nodes voting preference with persisted masternode selections. It adds node selection UI, contest-specific bulk vote planning, duplicate suppression, replacement confirmation, per-contest casting, and provider-derived signing-key diagnostics. ChangesVoting architecture
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant BulkVoteSheet
participant VotingViewModel
participant MasternodeVoteCaster
BulkVoteSheet->>VotingViewModel: request bulk vote plan
VotingViewModel-->>BulkVoteSheet: return plan and replacement metadata
BulkVoteSheet->>MasternodeVoteCaster: cast per-contest work items
MasternodeVoteCaster-->>BulkVoteSheet: return casting results
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: 6
🧹 Nitpick comments (6)
DashWallet/Sources/UI/DashPay/Voting/VotingNodeSelectionSheet.swift (1)
34-34: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winPut the attribute on its own line.
SwiftLint's
attributesrule requires an attribute with arguments to sit on the line above the declaration.CastVoteSheet.swiftlines 32-33 already use that layout.♻️ Proposed change
- `@Environment`(\.dismiss) private var dismiss + `@Environment`(\.dismiss) + private var dismiss🤖 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/VotingNodeSelectionSheet.swift` at line 34, Move the `@Environment`(\.dismiss) attribute in VotingNodeSelectionSheet to its own line above the dismiss property declaration, matching the layout used in CastVoteSheet.swift and satisfying SwiftLint's attributes rule.Source: Linters/SAST tools
DashWallet/Sources/Infrastructure/SwiftDashSDK/Voting/MasternodeVoterRegistry.swift (1)
204-207: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDistinguish the two "no provider-voting deriver" messages.
Line 170 and line 205 log identical text for two different failures. One is
MasternodeProviderKeyDeriver, the other isProviderKeyDeriver. A reader of the log cannot tell which construction failed.♻️ Proposed change
guard let keyDeriver = ProviderKeyDeriver(key: .voting) else { - Self.logger.error("🗳️ VOTING :: no provider-voting deriver available") + Self.logger.error("🗳️ VOTING :: no platform-wallet provider-voting key deriver available") return nil }🤖 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/MasternodeVoterRegistry.swift` around lines 204 - 207, Update the error log in the ProviderKeyDeriver guard within MasternodeVoterRegistry to identify that ProviderKeyDeriver construction failed, distinguishing it from the separate MasternodeProviderKeyDeriver failure log while preserving the existing return behavior.DashWallet/Sources/UI/DashPay/Voting/VotingViewModel.swift (2)
444-446: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winReplace the three-member tuple with a named type.
SwiftLint reports
large_tupleat both lines. The tuple(label:choice:nodes:)also crosses the module boundary intoMasternodeVoteCaster.castBulk. A small struct documents the contract and removes the warnings at both ends.♻️ Proposed change
+ /// One contest's share of a bulk run. + struct BulkWorkItem { + let label: String + let choice: VoteChoice + let nodes: [VoterNode] + } + struct BulkPlan { /// Per contest: the wire choice and the nodes that will actually cast. - let work: [(label: String, choice: VoteChoice, nodes: [VoterNode])] + let work: [BulkWorkItem]Then update
planBulkandMasternodeVoteCaster.castBulkto accept[BulkWorkItem].Also applies to: 469-469
🤖 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 444 - 446, Replace the three-member work tuple in BulkPlan with a named BulkWorkItem type containing label, choice, and nodes, then update planBulk and MasternodeVoteCaster.castBulk to use [BulkWorkItem] while preserving the existing data and behavior.Source: Linters/SAST tools
474-488: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win
planBulkruns one history query per selected contest.
history.votes(forContest:network:)is awaited inside the loop, so a selection of N contests produces N sequential database round trips before the confirmation sheet can render.VoteHistoryDAOalready exposes a network-wide aggregate; a single query filtered by network, grouped in memory by label, would collapse this to one round trip.The per-node deduplication at lines 482-488 also assumes several rows per (node, contest).
VoteHistoryDAO.recordupserts on(proTxHash, normalizedLabel, network), so only one row exists. Confirm the assumption before relying on it.🤖 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 474 - 488, The planBulk flow currently performs one sequential history query per selected contest; replace this with VoteHistoryDAO’s network-wide aggregate query, then group the returned records by normalizedLabel in memory and use each contest’s group. Remove the unnecessary per-node latestByNode deduplication because record upserts guarantee one row per proTxHash, normalizedLabel, and network, while preserving the existing resolved-choice filtering and vote processing.DashWallet/Sources/Infrastructure/SwiftDashSDK/Voting/MasternodeVoteCaster.swift (1)
270-272: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winReplace the three-member tuple with a named type.
SwiftLint reports
large_tupleat Line 271. The same tuple shape is repeated inVotingViewModel.planBulkand inBulkPlan.work. A smallstruct(for exampleBulkWorkItem) documents the contract at the call sites and removes the warning.♻️ Proposed named type
struct BulkWorkItem { let label: String let choice: VoteChoice let nodes: [VoterNode] }func castBulk( - _ work: [(label: String, choice: VoteChoice, nodes: [VoterNode])] + _ work: [BulkWorkItem] ) async throws -> [VoteCastReport] {🤖 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 270 - 272, Replace the repeated three-element work tuple with a named BulkWorkItem type containing label, choice, and nodes. Update castBulk, VotingViewModel.planBulk, BulkPlan.work, and all construction/access sites to use BulkWorkItem while preserving existing behavior.Source: Linters/SAST tools
DashWallet/Sources/UI/DashPay/Voting/BulkVoteSheet.swift (1)
216-230: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueShare one choice-name mapping instead of three copies.
VotingViewModelalready defines a private staticname(for choice: VoteChoice)with the same strings. This file adds two more copies. Move a single mapping toVotingViewModel.BulkChoiceandVoteChoice(for example, adisplayNameproperty on each type) and call it from both files. This prevents the labels from drifting apart later.Also note that the
VoteChoiceoverload maps every.towards(identityId:)to "Sole Requester". A stored history record can hold a.towardsvote that was cast when the contest had several contenders. In that case the replacement message names the wrong choice.🤖 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/BulkVoteSheet.swift` around lines 216 - 230, Centralize the choice labels by adding shared display-name mappings to both VotingViewModel.BulkChoice and VoteChoice, then replace the duplicate name(for:) helpers in BulkVoteSheet and VotingViewModel with those mappings. Ensure VoteChoice.towards(identityId:) derives its label from the associated identity or otherwise preserves the actual contender name instead of always returning “Sole Requester” for historical multi-contender votes.
🤖 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/Voting/MasternodeVoteCaster.swift`:
- Around line 273-276: The empty-batch handling must precede node validation: in
MasternodeVoteCaster.swift lines 273-276, update the guards so work.isEmpty
returns [] before work.contains checks nodes. In BulkVoteSheet.swift lines
311-322, record contests dropped by resolvedChoice in BulkPlan, include that
count in overlapMessage(for:), and skip castBulk(plan:) when plan.work is empty.
In BulkVoteSheet.swift lines 148-156, add a confirmed.hasWork guard inside the
Continue action so it returns without proceeding.
In
`@DashWallet/Sources/Infrastructure/SwiftDashSDK/Voting/MasternodeVoterRegistry.swift`:
- Around line 127-138: Update the logging in MasternodeVoterRegistry.swift at
lines 127-138 and 179-197: mark address, poolAddress, and derivationAddress as
private/redacted in the unmatched, matched, mismatch, and informational log
messages. Keep index and livePoolSummary() public, since they provide
non-sensitive diagnostic details.
In `@DashWallet/Sources/UI/DashPay/Voting/BulkVoteSheet.swift`:
- Around line 148-156: Update the Continue button action in BulkVoteSheet to
guard plan.hasWork and return immediately when no work remains, before clearing
pendingPlan or calling viewModel.castBulk. Keep the existing behavior unchanged
for valid plans.
In `@DashWallet/Sources/UI/DashPay/Voting/UsernameVotingScreen.swift`:
- Around line 259-268: Update UsernameVotingScreen.swift lines 259-268 to build
selectionSummary.chosen from VotingViewModel.effectiveSelectedNodeIDs,
preserving the existing empty, all-nodes, and name-list summaries. Update
CastVoteSheet.swift lines 114-115 to compare the sheet-local selectedNodeIDs
with candidateNodes.count, so its hint reflects the current sheet selection; no
other sites require changes.
In `@DashWallet/Sources/UI/DashPay/Voting/VotingViewModel.swift`:
- Around line 202-210: Update the selection-settling block around
effectiveSelectedNodeIDs so selectedNodeIDs is written back only when the
votable node list has successfully resolved, not when registry.votableNodes() is
temporarily empty during SDK, wallet, or masternode synchronization. Preserve
the stored selection while unresolved, and continue settling it once a resolved
node list is available.
- Around line 322-326: Update nodesForNextVote(on:) and its supporting
vote-history flow to use the hashes loaded for the requested normalizedLabel
rather than a shared set. Ensure loadVotedNodes(for:) completes before
filtering, or otherwise gate the result, so nodes already voted in that contest
are never returned.
---
Nitpick comments:
In
`@DashWallet/Sources/Infrastructure/SwiftDashSDK/Voting/MasternodeVoteCaster.swift`:
- Around line 270-272: Replace the repeated three-element work tuple with a
named BulkWorkItem type containing label, choice, and nodes. Update castBulk,
VotingViewModel.planBulk, BulkPlan.work, and all construction/access sites to
use BulkWorkItem while preserving existing behavior.
In
`@DashWallet/Sources/Infrastructure/SwiftDashSDK/Voting/MasternodeVoterRegistry.swift`:
- Around line 204-207: Update the error log in the ProviderKeyDeriver guard
within MasternodeVoterRegistry to identify that ProviderKeyDeriver construction
failed, distinguishing it from the separate MasternodeProviderKeyDeriver failure
log while preserving the existing return behavior.
In `@DashWallet/Sources/UI/DashPay/Voting/BulkVoteSheet.swift`:
- Around line 216-230: Centralize the choice labels by adding shared
display-name mappings to both VotingViewModel.BulkChoice and VoteChoice, then
replace the duplicate name(for:) helpers in BulkVoteSheet and VotingViewModel
with those mappings. Ensure VoteChoice.towards(identityId:) derives its label
from the associated identity or otherwise preserves the actual contender name
instead of always returning “Sole Requester” for historical multi-contender
votes.
In `@DashWallet/Sources/UI/DashPay/Voting/VotingNodeSelectionSheet.swift`:
- Line 34: Move the `@Environment`(\.dismiss) attribute in
VotingNodeSelectionSheet to its own line above the dismiss property declaration,
matching the layout used in CastVoteSheet.swift and satisfying SwiftLint's
attributes rule.
In `@DashWallet/Sources/UI/DashPay/Voting/VotingViewModel.swift`:
- Around line 444-446: Replace the three-member work tuple in BulkPlan with a
named BulkWorkItem type containing label, choice, and nodes, then update
planBulk and MasternodeVoteCaster.castBulk to use [BulkWorkItem] while
preserving the existing data and behavior.
- Around line 474-488: The planBulk flow currently performs one sequential
history query per selected contest; replace this with VoteHistoryDAO’s
network-wide aggregate query, then group the returned records by normalizedLabel
in memory and use each contest’s group. Remove the unnecessary per-node
latestByNode deduplication because record upserts guarantee one row per
proTxHash, normalizedLabel, and network, while preserving the existing
resolved-choice filtering and vote processing.
🪄 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: 187fdbda-3be2-4845-8d90-17e8127bc515
📒 Files selected for processing (10)
DashWallet.xcodeproj/project.pbxprojDashWallet/Sources/Infrastructure/SwiftDashSDK/Voting/MasternodeVoteCaster.swiftDashWallet/Sources/Infrastructure/SwiftDashSDK/Voting/MasternodeVoterRegistry.swiftDashWallet/Sources/Models/Voting/VotingPrefs.swiftDashWallet/Sources/UI/DashPay/Voting/BulkVoteSheet.swiftDashWallet/Sources/UI/DashPay/Voting/CastVoteSheet.swiftDashWallet/Sources/UI/DashPay/Voting/UsernameVotingScreen.swiftDashWallet/Sources/UI/DashPay/Voting/VotingNodeSelectionSheet.swiftDashWallet/Sources/UI/DashPay/Voting/VotingViewModel.swiftDashWallet/Sources/UI/Menu/Tools/Masternode Keys/DerivationPathKeys/Models/DerivationPathKeysModel.swift
These were the app's only Swift-side private-key derivation, and the only
keys sourced from the throwaway key-wallet rebuilt from the mnemonic.
Payments, identity/DPNS, and the BLS/Ed25519 provider families all derive
Rust-side from the running wallet already.
That outlier was broken. `Account.derivePrivateKeyWIF` took a `masterPath`
and pre-derived it, while the FFI applies the account's own path itself, so
every owner/voting key came from
m/9'/5'/3'/1'/9'/5'/3'/1'/index instead of m/9'/5'/3'/1'/index
Nothing failed locally — the keys were well-formed and deterministic — so
the Masternode Keys screen displayed wrong owner/voting private keys, and
masternode votes were rejected by Platform as having no voter identity,
because the voter identity is derived from the signing key's own hash160.
Routes all four families through `ProviderKeyDeriver` (platform-wallet
`providerKeyAtIndex`, requires platform#4338), which derives against the
running wallet and cross-checks the seed-derived private key against the
account xpub before returning. Drops the key-wallet half of
`MasternodeProviderKeyDeriver`; what remains is its live address-pool read,
which the owner/voting address join needs and which was always correct.
Also stops swallowing derivation errors. `key(at:)` used `try?`, so a
failure was indistinguishable from "no such key" — which is how a wrong
signing key stayed invisible locally and only showed up as an opaque
Platform rejection. Adds voting diagnostics that record the registered
address, the pool index it matched, and the key source, so the next
failure of this class names itself.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hing
Three changes to how votes are chosen and cast.
**Remembered node selection.** Replaces the "One node at a time / All nodes
at once" toggle with the nodes themselves: a "Voting with" row naming them
("Evonode 2", "Evonode 2, Evonode 3", "All 6 nodes") that opens a picker.
The choice is persisted and shared by the single-contest screen and the
bulk sheet, so the next contest opens with the same nodes ticked. A wallet
that has never voted defaults to ONE node — voting with several at once
publicly links those masternodes to each other, and that stays something
the user opts into rather than an accident of the first vote. A remembered
node that has since been revoked is dropped rather than left to disable
voting silently.
**Sole Requester.** Bulk selection now accepts only single-request
contests, which is what makes "award it to the only requester" well-defined
— the caster previously rejected `towards` as not bulkable because it names
a specific contender. Ineligible rows stay visible but disabled with the
reason, so the selection list does not silently differ from the browsing
list. `castBulk` now carries a per-contest choice and per-contest nodes;
the `choiceNotBulkable` error is removed rather than left describing a
constraint that no longer holds.
**Confirm before re-voting.** A bulk run now plans first: for each contest,
which selected nodes still need to vote there. A node that already cast the
SAME choice is skipped (re-sending spends a per-contest allowance to change
nothing); a node that cast a DIFFERENT choice is carried out, because
changing a vote is legitimate — Platform accepts several votes per
masternode per contest, and refusing would make a recorded vote more
permanent than Platform does. The prompt says which votes are replaced and
which are left alone, names the earlier choice only when they all agree,
and reads the latest record per node so a node that already changed its
vote is judged on its live choice.
**Stall on returning from a contest.** `.task` re-fires when the list
reappears, and `refresh()` re-queries every active contest over the network
— around 12s, which read as the screen hanging. The initial load is now
`refreshIfNeeded()`, keyed on a fetch having SUCCEEDED rather than having
been attempted, so a failed first load still retries. Pull-to-refresh still
forces a real reload, and casting already refreshes the contest it touched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
df3123d to
f6d6464
Compare
…h reporting Six findings from review on #936. **A remembered multi-node selection could be destroyed (Major).** `refresh()` settled the selection against `votableNodes`, which is empty while the SDK is starting or the masternode phase has not synced. The settle then computed an empty selection and PERSISTED it, so a user who chose several nodes silently dropped to one. Only settle once the node list has actually resolved. **An empty batch reported "Select at least one masternode" (Major).** `castBulk` checked `work.contains(where: nodes non-empty)` before the `work.isEmpty` early return, so a batch where planning dropped everything was reported as a missing node selection — sending the user to fix a selection that was fine. Guards reordered, and the sheet no longer calls the caster with an empty batch: it reports that the selected names can no longer take the vote. **Masternode addresses were written to the unified log in clear (Major).** They identify nodes the user operates and persist in sysdiagnose archives. Redacted to `.private`; the pool index and the pool summary (counts only) stay public, which is where the diagnostic value was. **Vote history was one shared set (Minor).** `nodesYetToVote(on:)` ignored its label and read a single set, so a screen evaluated before its `.task` completed could be answered with the previous contest's history — offering a node that already voted, which Platform rejects while still spending one of that masternode's per-contest votes. Keyed by contest, with an absent entry meaning "not loaded yet" rather than "nobody voted"; `hasLoadedVoteHistory` lets the control stay disabled until it is known. **Two screens described a selection the vote does not use (Minor).** The "Voting with" row read the raw persisted set while the cast resolves the effective one, so it could read "Select nodes" while a tap would vote with the fallback node. It now takes the effective selection, and CastVoteSheet's hint compares its own ticks against its own candidates. **`.disabled` on an alert button is unreliable on iOS 17 (Minor).** It can omit the button or fail to track state. Continue is now only offered when there is work, the action guards again rather than trusting the modifier, and the dismiss button reads "OK" when there is nothing to do. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#936 and #937 landed independently and each brought its own memoised wrapper over `ManagedPlatformWallet.providerKeyAtIndex` — byte-identical bodies, one for the ECDSA owner/voting family and one for BLS/Ed25519. Two caches to keep in step and two places to change. Collapse both into a file-private `ProviderKeyResolver`, now the single call site for all four families. `MasternodeProviderKeyDeriver` keeps its distinct job — it carries the address pool the owner/voting rows join against — and delegates derivation. Operator/evonode-operator have no address rows, so their wrapper shell goes away entirely and the model holds a resolver directly; `tenderdashNodeKeyBase64` moves onto the resolver, still guarded on the Ed25519 kind. Also drops three pieces of state left dead by #937, when derivation moved Rust-side: * `wallet` — written in init, never read since. It held a `Wallet` owned by the `WalletManager` that `derivationWallet()` builds per call and that this class never retained, so it was a reference into a graph released at the end of init. Unused, so harmless, but not worth keeping. * `key` — written, never read. * the bound `network` — it selected the coin type back when this class composed the DIP-3 path app-side. Now a boolean test, with a comment for why the check stays. No behaviour change. Same kinds, same `includePrivate: true`, same cache lifetime (one per deriver instance). The host-wallet guard now runs before the `derivationWallet()` guard rather than after, which cannot change the outcome: `derivationWallet()` returns nil unless that same wallet is present, so the guard only fires where the other would have. Verified: clean `dashpay` build, no warnings left in the file. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Two commits: a correctness fix to how masternode voting keys are derived, and a rework of how votes are chosen and cast.
1. Derive the VOTING key through the platform wallet
#937 landed the same fix for the Masternode Keys screen independently, so this commit is now only the part it did not cover:
MasternodeVoterRegistry, the signing path.The shared cause:
Account.derivePrivateKeyWIFasked callers for the account root path while the FFI applied the account's own path on top, so every owner/voting key came fromm/9'/5'/3'/1'/9'/5'/3'/1'/index. The keys were well-formed and deterministic, so nothing failed locally — masternode votes were simply rejected by Platform as having no voter identity, because the voter identity is derived from the signing key's own hash160.The voting path now resolves its key through the same Rust-side resolver, which owns the DIP-3 path and cross-checks the derived key against the account xpub before returning it. A key that does not match the node's registered voting address can no longer reach the signer.
Rebased onto #937, taking its version of
DerivationPathKeysModelwholesale. The diagnostics I had added there — comparing the live pool against the throwaway derivation wallet — are dropped: they existed to hunt this bug, and the Rust cross-check now verifies the same property at the source.2. Node selection, Sole Requester, and the stall
Remembered nodes. The "One node at a time / All nodes at once" toggle is replaced by the nodes themselves: a Voting with row naming them ("Evonode 2", "Evonode 2, Evonode 3", "All 6 nodes") that opens a picker. Persisted and shared between the single-contest screen and the bulk sheet, so the next contest opens with the same nodes ticked. A wallet that has never voted defaults to one node — voting with several at once publicly links those masternodes, and that stays opt-in rather than an accident of the first vote. A remembered node that has since been revoked is dropped rather than left to disable voting silently.
Sole Requester. Bulk selection now accepts only single-request contests, which is what makes "award it to the only requester" well-defined — the caster previously rejected
towardsas not bulkable because it names a specific contender. Ineligible rows stay visible but disabled with the reason, so the selection list doesn't silently differ from the browsing list.Confirm before re-voting. A bulk run plans first. A node that already cast the same choice is skipped (re-sending spends a per-contest allowance to change nothing); a node that cast a different choice proceeds, because changing a vote is legitimate — Platform accepts several votes per masternode per contest, and refusing would make a recorded vote more permanent than Platform does. The prompt says which votes are replaced and which are left alone, names the earlier choice only when they all agree, and reads the latest record per node so a node that already changed its vote is judged on its live choice.
Stall on leaving a contest.
.taskre-fires when the list reappears, andrefresh()re-queries every active contest — ~12s, which read as the screen hanging. NowrefreshIfNeeded(), keyed on a fetch having succeeded rather than been attempted, so a failed first load still retries. Pull-to-refresh still forces a reload; casting already refreshes the contest it touched.Dependencies
All merged into
v4.2-dev. This PR needs an xcframework built from it — that is the only manual step.providerKeyAtIndexfor the secp256k1 familiesderivePrivateKeyWIFthat caused the doubled pathdevelopdid not compile without itAlso rebased onto dashwallet-ios#937, which fixed the Masternode Keys screen half of the derivation bug.
Testing
dashpaybuilds clean and runs on an iPhone 16 Plus simulator against a mainnet wallet with 6 evonodes.Confirmed: votes now cast successfully — observed by the repo owner against real masternodes. That was the load-bearing unknown: the derivation fix was previously only proven arithmetically (the voter identity the app requests equals
SHA256(proTxHash ‖ hash160(registered voting address)), computed independently), with no successful cast observed.Still not verified:
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Improvements