diff --git a/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift b/packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/SendTransactionView.swift index 90ea08db872..80ffe41816a 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 { @@ -49,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 + ) } } @@ -71,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) @@ -185,8 +226,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] { @@ -228,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 @@ -241,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 @@ -278,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