From af431d98bf9f53fabacad26ca3634e680ca9aa4a Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 6 May 2026 05:43:01 +0700 Subject: [PATCH 1/2] fix(swift-example-app): show real platform balance on Send screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Send Dash sheet's "Platform: 0 DASH" row contradicted the wallet detail's "Platform Balance: 1.00572807 DASH" for the same wallet, and as a knock-on the FundSource picker never offered Platform as an option for shielded sends — `selectedSource` stayed pinned at the default Core, `(.orchard, .core)` doesn't map to a flow, and the Send button stayed disabled. Root cause: `SendTransactionView.platformBalance` summed only `wallet.identities.balance`. BLAST-synced platform-address credits (the Platform Payment accounts a faucet top-up lands in) live in `PersistentPlatformAddress` rows under the wallet id, not on the identity rows, so a wallet without a registered identity always reported 0. Mirror the same `addressBalances + identity-fallback` shape `WalletDetailView.platformBalance` already uses: BLAST-synced balance is canonical once `syncHeight`/`syncTimestamp` is set, identity sum is the pre-sync fallback. Add the matching `@Query` and `@Query` filters scoped to the wallet id / network at init time. After this the Send screen reports the same Platform balance the wallet detail does, the FundSource picker offers Platform for shielded destinations, and the flow detector resolves `(.orchard, .platform) → .platformToShielded` so the user can at least *select* the Platform→Shielded path. Actual send execution still hits the `"Shielded sending is being rebuilt"` placeholder until the spend signer pipeline lands. --- .../Core/Views/SendTransactionView.swift | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift index 90ea08db872..39f50a94160 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift @@ -13,9 +13,34 @@ struct SendTransactionView: View { @Environment(\.modelContext) private var modelContext + /// BLAST-synced platform-address balances for this wallet — + /// same source `WalletDetailView` reads to populate its + /// "Platform Balance" row. Without these the send screen used + /// to fall through to `wallet.identities.balance` only, which + /// is empty for wallets that hold credits at platform + /// addresses (e.g. faucet-funded Platform Payment accounts) + /// rather than at registered identities. + @Query private var addressBalances: [PersistentPlatformAddress] + + /// Persisted BLAST sync watermarks — used to distinguish + /// "BLAST hasn't synced yet, fall back to identities" from + /// "BLAST synced and there genuinely are no platform-address + /// credits". + @Query private var syncStates: [PersistentPlatformAddressesSyncState] + init(wallet: PersistentWallet) { self.wallet = wallet _viewModel = StateObject(wrappedValue: SendViewModel(network: wallet.network ?? .testnet)) + let walletId = wallet.walletId + let walletNetworkRaw = (wallet.network ?? .testnet).rawValue + _addressBalances = Query( + filter: #Predicate { $0.walletId == walletId } + ) + _syncStates = Query( + filter: #Predicate { + $0.networkRaw == walletNetworkRaw + } + ) } var body: some View { @@ -185,8 +210,19 @@ struct SendTransactionView: View { shieldedService.shieldedBalance } + /// Mirrors `WalletDetailView.platformBalance`: BLAST-synced + /// address balances are the canonical source once a sync has + /// landed; before that, fall back to summing identity credits + /// so a freshly-restored wallet still shows something + /// approximate. private var platformBalance: UInt64 { - wallet.identities.reduce(UInt64(0)) { $0 + UInt64(bitPattern: $1.balance) } + let blastBalance = addressBalances.reduce(UInt64(0)) { $0 + $1.balance } + let hasSynced = syncStates.first.map { $0.syncHeight > 0 || $0.syncTimestamp > 0 } + ?? false + if blastBalance > 0 || hasSynced { + return blastBalance + } + return wallet.identities.reduce(UInt64(0)) { $0 + UInt64(bitPattern: $1.balance) } } private func availableSources(coreBalance: UInt64) -> [FundSource] { From ad579a4489121e3fab15c0d77c0bc71f58cd0000 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 6 May 2026 05:51:41 +0700 Subject: [PATCH 2/2] fix(swift-example-app): format Platform/Shielded balances at the credits scale MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Send screen rendered Platform balance as 1,005.7280748 DASH for a wallet whose actual Platform Payment account holds 1.00572807 DASH — off by exactly 1000×. Same latent bug for Shielded balance (just masked because the row currently always reads 0). `BalanceInfoRow.formatBalance` (and the parent struct's helper used by the FundSource picker) divided every UInt64 amount by 1e8, treating it as L1 duffs. Platform credits and Orchard credits use a 1e11 scale, so anything fed through the shared formatter inflated the value 1000-fold. Add a `SendBalanceUnit` enum (`.duffs` / `.credits`) and thread it through `BalanceInfoRow`, the Send screen's `formatBalance(_:unit:)` helper, and a `unit(for source: FundSource)` selector. Core stays on `.duffs`; Platform and Shielded use `.credits`. Estimated-fee formatting is left at the default `.duffs` because the existing `SendFlow.estimatedFee` constants are encoded in duffs (a separate known issue out of scope here). After this, the Send sheet's Platform row and the FundSource picker both show the same number the wallet detail does. --- .../Core/Views/SendTransactionView.swift | 63 ++++++++++++++++--- 1 file changed, 54 insertions(+), 9 deletions(-) diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift index 39f50a94160..80ffe41816a 100644 --- a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift +++ b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift @@ -74,9 +74,24 @@ struct SendTransactionView: View { } VStack(alignment: .leading, spacing: 4) { - BalanceInfoRow(label: "Core:", amount: coreBalance, color: .green) - BalanceInfoRow(label: "Shielded:", amount: shieldedBalance, color: .purple) - BalanceInfoRow(label: "Platform:", amount: platformBalance, color: .blue) + BalanceInfoRow( + label: "Core:", + amount: coreBalance, + unit: .duffs, + color: .green + ) + BalanceInfoRow( + label: "Shielded:", + amount: shieldedBalance, + unit: .credits, + color: .purple + ) + BalanceInfoRow( + label: "Platform:", + amount: platformBalance, + unit: .credits, + color: .blue + ) } } @@ -96,7 +111,8 @@ struct SendTransactionView: View { .foregroundColor(.primary) Spacer() Text(formatBalance( - balance(for: source, coreBalance: coreBalance) + balance(for: source, coreBalance: coreBalance), + unit: unit(for: source) )) .font(.caption) .foregroundColor(.secondary) @@ -264,8 +280,13 @@ struct SendTransactionView: View { } } - private func formatBalance(_ amount: UInt64) -> String { - let dash = Double(amount) / 100_000_000.0 + /// Format a `UInt64` balance for display. + /// + /// `unit` controls the divisor — Core/duffs are 1e8 per DASH, + /// Platform/shielded credits are 1e11 per DASH. Mixing the two + /// would over-report Platform balances by 1000×. + private func formatBalance(_ amount: UInt64, unit: SendBalanceUnit = .duffs) -> String { + let dash = Double(amount) / unit.dashDivisor let formatter = NumberFormatter() formatter.minimumFractionDigits = 0 formatter.maximumFractionDigits = 8 @@ -277,6 +298,13 @@ struct SendTransactionView: View { } return String(format: "%.8f DASH", dash) } + + private func unit(for source: FundSource) -> SendBalanceUnit { + switch source { + case .core: return .duffs + case .platform, .shielded: return .credits + } + } } // MARK: - Subviews @@ -314,22 +342,39 @@ private struct AddressTypeBadge: View { } } +/// Whether a `UInt64` balance reads as L1 duffs (1 DASH = 1e8) or +/// Platform / shielded credits (1 DASH = 1e11). The two scales +/// differ by 1000× — formatting Platform credits as duffs over- +/// reports balances by exactly that factor. +enum SendBalanceUnit { + case duffs + case credits + + fileprivate var dashDivisor: Double { + switch self { + case .duffs: return 100_000_000.0 + case .credits: return 100_000_000_000.0 + } + } +} + private struct BalanceInfoRow: View { let label: String let amount: UInt64 + var unit: SendBalanceUnit = .duffs var color: Color = .primary var body: some View { HStack { Text(label).font(.caption).foregroundColor(.secondary) Spacer() - Text(formatBalance(amount)) + Text(formatBalance(amount, unit: unit)) .font(.caption).foregroundColor(amount > 0 ? color : .secondary) } } - private func formatBalance(_ amount: UInt64) -> String { - let dash = Double(amount) / 100_000_000.0 + private func formatBalance(_ amount: UInt64, unit: SendBalanceUnit) -> String { + let dash = Double(amount) / unit.dashDivisor let formatter = NumberFormatter() formatter.minimumFractionDigits = 0 formatter.maximumFractionDigits = 8