Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<PersistentPlatformAddress> { $0.walletId == walletId }
)
_syncStates = Query(
filter: #Predicate<PersistentPlatformAddressesSyncState> {
$0.networkRaw == walletNetworkRaw
}
)
}

var body: some View {
Expand Down Expand Up @@ -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
)
}
}

Expand All @@ -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)
Expand Down Expand Up @@ -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] {
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading