Skip to content

fix(wallet): restored internal transfers show their route and amount - #939

Merged
QuantumExplorer merged 4 commits into
developfrom
fix/restored-internal-transfer-routes
Aug 8, 2026
Merged

fix(wallet): restored internal transfers show their route and amount#939
QuantumExplorer merged 4 commits into
developfrom
fix/restored-internal-transfer-routes

Conversation

@QuantumExplorer

@QuantumExplorer QuantumExplorer commented Aug 8, 2026

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

On a restored (wipe & recover / second same-seed device) wallet, internal transfers lose their identity in the history:

  • Asset-lock funding txs render "Internal Transfer — 0 DASH / Not available". The route/amount labelling added in feat(ui): shielded-transfer identity and status on the tx details sheet #826/feat(ui): asset-lock transaction identity (transfers + identity fundings) #827 reads ShieldedTxLookup, which mirrors the SDK's PersistentAssetLock store — and those rows are live-recorded at execution and do not survive a restore. On a freshly restored mainnet wallet, two July asset locks (0.4 and 0.302 DASH) both showed as zero-amount "Internal Transfer".
  • Shielded Sent rows hide their destination. A shielded → shielded send's row pill and detail sheet said only "Sent / Type: Shielded" — the recipient address (which the SDK records as the 43-byte raw Orchard counterparty, live or OVK-recovered by the restore scan) was displayed nowhere, making an old send indistinguishable from an internal transfer.

Both were found on a restored wallet whose only shielded history row was a scan-derived Sent −0.01 with no visible destination.

What was done?

Asset-lock reconstruction (ShieldedTxLookup.refresh()): after building the snapshot from PersistentAssetLock rows, every persisted AssetLock transaction with no store row gets a reconstructed entry parsed from its raw bytes (consensus truth — parser shared with the raw-tx inspector, whose credit-output parsing was promoted into reusable RawTransactionInspector.assetLockCreditOutputs / p2pkhKeyHash helpers).

The destination is recovered exactly: every credit output pays a one-time address derived from a purpose-specific funding account (identity registration / top-up / invitation, Platform address top-up, shielded top-up — account types 2…7), and those pools survive a restore as PersistentCoreAddress rows. Matching the credit outputs against the active wallet's persisted funding-account addresses yields the exact FundingType (0…5), so a restored Transparent → Shielded funding renders the full existing Core → Shielded treatment ("Internal Transfer" with the shield route icon, From: Transparent balance / To: Shielded balance on the detail sheet), Platform top-ups render the Core → Platform treatment, and identity fundings render their named titles. Credit outputs that match no pool (or mix pools) fall back to a reconstructedUnknownFundingType sentinel: the row keeps its generic "Internal Transfer" title but still shows the real locked amount instead of 0.

Reconstructed entries carry statusRaw = -1 (outside both the pending window 1…3 and consumed 4), so they never render "Pending", never claim a completion status (lockStatusText maps -1 to no Status row), and never feed a recovery resume. Store-backed rows always win over reconstruction.

Shielded Sent destination (fetchShieldedActivity + ShieldedActivityItem): Sent entries decode their 43-byte counterparty to the DIP-0018 bech32m display form (0x10 type byte + raw address, same encoding as the Receive screen). The row pill shows the shortened address (matching the external unshield/withdrawal treatment) and the detail sheet gains From: Your Shielded balance / To: <copyable address> rows. A Sent row whose counterparty didn't decode keeps the current route-less presentation rather than claiming a destination.

What was deliberately NOT done (SDK follow-ups)

The restore scan currently collapses shielded history poorly: on the test wallet it reconstructed the entire multi-transaction history into a single Sent entry whose cmx list aggregates all nine actions across several transactions, attached one nullifier from a different transaction, recorded no fee, and stamped every note and the activity row with the scan-tip block height (411495 on one device, 412108 on the other, for the same on-chain history). Own-shield (Shield/ShieldFromAssetLock) entries were not reconstructed at all, so a restored wallet shows no "Transparent → Shielded" rows for past shields. That is rs-platform-wallet / swift-sdk scan-reconstruction territory and needs a cross-repo fix; this PR fixes what is honestly derivable app-side.

How Has This Been Tested?

Manual, on a restored mainnet wallet (fresh iPhone 16 Pro simulator, wallet recovered from seed — the exact state that exhibits both bugs):

  • The two July asset-lock rows show their real amounts (0.4 / 0.302 DASH) instead of "0"; the 0.302 lock's credit output matched the wallet's shielded top-up funding account (m/9'/5'/5'/5'/0) and renders the Core → Shielded route, the 0.4 lock matched the Platform address top-up account (m/9'/5'/5'/4'/0) and renders Core → Platform; no "Pending" pill on either.
  • The shielded Sent −0.01 row pill shows the shortened dash1… recipient; its detail sheet shows From/To with the full copyable address.
  • Regression: masternode registration, classic sends/receives, and the live-recorded transfer rows are unchanged.

Clean dashpay build verified. (The unit-test target is pre-existing broken; not run.)

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

    • Restores missing asset-lock transaction details and displays their parsed amounts.
    • Shows decoded shielded recipients for sent activity when available.
    • Adds clearer routes and destination details for shielded sends, withdrawals, and unshield operations.
  • Bug Fixes

    • Improves asset-lock parsing and safely handles malformed or ambiguous transaction data.
    • Displays decoded asset-lock outputs with address details or a script fallback.
    • Prevents errors when processing unusually large serialized transaction data.

After a wipe & recover, PersistentAssetLock rows don't survive, so
asset-lock funding txs rendered "Internal Transfer - 0 DASH"; shielded
Sent rows showed no destination at all. ShieldedTxLookup now
reconstructs missing locks from the persisted raw tx bytes (real locked
amount; full Core->Platform treatment when the credit outputs pay own
persisted Platform addresses; an explicit unknown-destination sentinel
otherwise), and shielded Sent entries decode their 43-byte Orchard
counterparty to the DIP-0018 bech32m form for the row pill and the
detail sheet's From/To rows.

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

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

Next review available in: 10 minutes

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

How can I continue?

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

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4901ef5a-c6c4-4752-941b-217b944b1a3e

📥 Commits

Reviewing files that changed from the base of the PR and between b42eb63 and f33bc33.

📒 Files selected for processing (1)
  • DashWallet/Sources/UI/Home/Views/HomeViewModel.swift
📝 Walkthrough

Walkthrough

The wallet reconstructs missing asset-lock records from raw transactions during restore, uses parsed amounts in transaction displays, and decodes shielded send counterparties for destination-aware activity details and routes.

Changes

Asset-lock restoration

Layer / File(s) Summary
Asset-lock payload parsing
DashWallet/Sources/Models/Transactions/Model/RawTransactionInspector.swift
Shared parsing validates asset-lock payloads, extracts credit outputs and P2PKH key hashes, and safely handles serialized lengths.
Restore-time asset-lock reconstruction
DashWallet/Sources/Infrastructure/SwiftDashSDK/PlatformAddressSyncCoordinator.swift
Refresh reconstructs missing asset-lock entries, derives locked values and output indexes, and assigns unknown funding and status sentinels when required.
Reconstructed amount display
DashWallet/Sources/Models/Transactions/Model/Transaction.swift
Transaction DASH and fiat calculations use reconstructed asset-lock amounts when persisted amounts are unavailable.

Shielded send display

Layer / File(s) Summary
Shielded counterparty decoding
DashWallet/Sources/UI/Home/Views/HomeViewModel.swift
Sent activity identifies wallet-owned transfers and decodes valid Orchard counterparties into network-specific DIP-0018 Bech32m addresses.
Shielded send details and routes
DashWallet/Sources/UI/Home/Views/ShieldedActivityHistory.swift
Sent entries show decoded destinations and provide destination routes only when decoding succeeds.

Estimated code review effort: 4 (Complex) | ~45 minutes

Suggested reviewers: llbartekll, jeanpierreroma

Sequence Diagram(s)

sequenceDiagram
  participant SnapshotRefresh
  participant PlatformAddressSyncCoordinator
  participant RawTransactionInspector
  participant ShieldedTxLookup
  SnapshotRefresh->>PlatformAddressSyncCoordinator: refresh snapshot
  PlatformAddressSyncCoordinator->>ShieldedTxLookup: find existing asset-lock entries
  PlatformAddressSyncCoordinator->>RawTransactionInspector: parse missing raw asset-lock transactions
  RawTransactionInspector-->>PlatformAddressSyncCoordinator: return validated outputs and outpoint index
  PlatformAddressSyncCoordinator->>ShieldedTxLookup: merge reconstructed records
Loading
sequenceDiagram
  participant ShieldedActivity
  participant HomeViewModel
  participant ShieldedActivityHistory
  ShieldedActivity->>HomeViewModel: load sent activity
  HomeViewModel->>HomeViewModel: decode Orchard counterparty and classify ownership
  HomeViewModel-->>ShieldedActivityHistory: provide destination and external status
  ShieldedActivityHistory->>ShieldedActivityHistory: render destination and route
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 summarizes the main user-visible fix: restored internal transfers now display their route and amount.
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/restored-internal-transfer-routes

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 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/Models/Transactions/Model/RawTransactionInspector.swift`:
- Around line 230-242: Update assetLockCreditOutputs(payload:) to validate
scriptLength can be represented as an Int before converting it, using
Int(exactly:) or an equivalent bound check. Return nil for oversized lengths and
only pass the validated Int to reader.readBytes.

In `@DashWallet/Sources/UI/Home/Views/HomeViewModel.swift`:
- Around line 1563-1575: Update the sent-item handling in fetchShieldedActivity
so isExternalDestination reflects wallet ownership rather than address decoding
success. Resolve the destination against persisted self-transfer/ownership data
or the active wallet’s Orchard-address lookup, while preserving the decoded
destinationAddress for display; mark only non-owned destinations as external.

In `@DashWallet/Sources/UI/Home/Views/ShieldedActivityHistory.swift`:
- Line 230: Update the route documentation near the .sent branch to state that
undecodable sent entries remain route-less when destinationAddress is absent,
replacing the incorrect “External address” fallback description.
🪄 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: 686a3df1-b131-4dad-b67c-dd59810d0740

📥 Commits

Reviewing files that changed from the base of the PR and between 572a98e and aa613c4.

📒 Files selected for processing (5)
  • DashWallet/Sources/Infrastructure/SwiftDashSDK/PlatformAddressSyncCoordinator.swift
  • DashWallet/Sources/Models/Transactions/Model/RawTransactionInspector.swift
  • DashWallet/Sources/Models/Transactions/Model/Transaction.swift
  • DashWallet/Sources/UI/Home/Views/HomeViewModel.swift
  • DashWallet/Sources/UI/Home/Views/ShieldedActivityHistory.swift

Comment thread DashWallet/Sources/UI/Home/Views/HomeViewModel.swift Outdated
Comment thread DashWallet/Sources/UI/Home/Views/ShieldedActivityHistory.swift Outdated
QuantumExplorer and others added 2 commits August 8, 2026 21:26
…ools

Every asset-lock credit output pays a one-time address derived from a
purpose-specific funding account (identity registration/top-up/
invitation, Platform address top-up, shielded top-up), and those
address pools survive a restore as PersistentCoreAddress rows. Match
the parsed credit outputs against them to recover the exact funding
type, so a restored Transparent->Shielded transfer renders the full
Core->Shielded treatment instead of a generic unknown-destination
"Internal Transfer".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- ByteReader gains a UInt64-length readBytes that rejects lengths above
  Int.max instead of trapping on the Int(_:) conversion; all CompactSize
  script/payload reads go through it.
- A shielded Sent destination is marked external only when provably not
  the wallet's own: an intra-wallet transfer leaves a Received row under
  the same entry id, and the wallet's default Orchard address is checked
  directly.
- internalMoveRoute doc: undecodable Sent counterparties stay route-less.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@DashWallet/Sources/UI/Home/Views/HomeViewModel.swift`:
- Around line 1442-1445: Update the ownership lookup for ownShieldedRaw43 to use
the walletId captured by hostHandles() instead of reading wallet.walletId from
the later SwiftDashSDKHost.shared.wallet access, while preserving the existing
manager lookup and shieldedDefaultAddress call.
🪄 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: ce5ab4d5-e57c-4ab1-9376-861eadaf007c

📥 Commits

Reviewing files that changed from the base of the PR and between cbf8065 and b42eb63.

📒 Files selected for processing (3)
  • DashWallet/Sources/Models/Transactions/Model/RawTransactionInspector.swift
  • DashWallet/Sources/UI/Home/Views/HomeViewModel.swift
  • DashWallet/Sources/UI/Home/Views/ShieldedActivityHistory.swift
🚧 Files skipped from review as they are similar to previous changes (2)
  • DashWallet/Sources/UI/Home/Views/ShieldedActivityHistory.swift
  • DashWallet/Sources/Models/Transactions/Model/RawTransactionInspector.swift

Comment thread DashWallet/Sources/UI/Home/Views/HomeViewModel.swift Outdated
The default-Orchard-address lookup re-read the host's active wallet on a
later main hop; a wallet switch between the two reads could compare
against the wrong wallet's address. Use the walletId the activity rows
were fetched with.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@QuantumExplorer
QuantumExplorer merged commit 7664bae into develop Aug 8, 2026
2 checks passed
QuantumExplorer added a commit that referenced this pull request Aug 9, 2026
…sset-lock reconstruction (#943)

* feat(shielded): chain-order the Date-unknown band; trust SDK asset-lock reconstruction

Consumes platform #4342 (SDK-side tracked-asset-lock reconstruction +
minNotePosition on scan-derived shielded entries):

- The trailing "Date unknown" history band (PR #940) now orders by exact
  on-chain sequence: ShieldedActivityItem carries the SDK's
  minNotePosition (smallest note commitment-tree position, nil when the
  row predates the field), TransactionListDataItem exposes it as
  chainOrderKey, and the pre-grouping sort tiebreaks equal dates by it
  descending — identical order on every device that restores the seed.

- The app-side restore fallback (PR #939's addReconstructedLocks raw-tx
  reparse) is reduced to a coverage diagnostic: SDK reconstruction now
  writes store-backed PersistentAssetLock rows for every historical
  funding tx (verified 9/9 on a restored testnet wallet — identity,
  platform, and shielded types all classify), and store rows always won
  over the fallback anyway. Any asset-lock tx with no row is logged as a
  reconstruction gap. The reconstructed* sentinels and
  Transaction.reconstructedLockAmountDuffs go with it.

- statusRaw 5 (RecoveredFromChain) is mapped explicitly: dedup prefers
  consumed (4) over recovered (5), and the tx detail sheet shows
  "Restored — completion unknown" instead of falling through. Note the
  SDK currently delivers reconstructed rows at 1/3, not 5 — the false-
  pending consequence and the enrichment gap are SDK-side, tracked for a
  platform follow-up.

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

* fix(dashpay): registration recovery includes RecoveredFromChain locks

With platform #4347, a registration asset lock stranded by an app kill
whose block chain-locks before relaunch surfaces as statusRaw 5
(RecoveredFromChain) instead of staying in the 0…3 window the recovery
lookup filtered on — the stranded payment would have stopped being
offered for recovery. Treat everything but the Consumed (4) tombstone
as unfinished: a 5-lock with no identity is a genuinely incomplete
registration, and the SDK resume path explicitly supports consuming it.
Completed registrations never resume from this lock — both call sites
probe the identity (local row, then the Platform slot, reconciling the
lock to Consumed) first.

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