Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import SwiftDashSDK
/// - `.platformToShielded` → `performShield(amountCredits:)`
/// - `.shieldedToCore` → `performWithdraw(amountCredits:)`
/// - `.shieldedToPlatform` → `performUnshield(amountCredits:)`
/// - `.coreToPlatform` → `performFundPlatform(amountDuffs:)`
/// - `.coreToPlatform` → `performFundPlatform(recipientAmountDuffs:)`
/// - `.platformToCore` → `performPlatformWithdrawAll()` (full balance)
struct InternalTransferConfirmSheet: View {

Expand All @@ -30,6 +30,21 @@ struct InternalTransferConfirmSheet: View {
let amountDuffsUnsigned: UInt64
let creditsAmount: UInt64
let fiatText: String
/// Resolved "Network fee" row value (credits), computed by
/// `InternalTransferViewModel.confirmNetworkFeeCredits` and frozen into
/// the submission — fee math is banned inside View structs. `nil`
/// renders as "—".
var networkFeeCredits: UInt64? = nil
/// Resolved "Total" row value (duffs) — what actually leaves the source
/// balance (`InternalTransferViewModel.confirmTotalDuffs`). `nil`
/// renders as "—".
var totalDuffs: Int64? = nil
/// Resolved "Fee reserve" row value (credits) — the FULL Core→Platform
/// funding reserve the fee is taken from; whatever the network doesn't
/// use is credited to the Platform balance
/// (`InternalTransferViewModel.confirmFeeReserveCredits`). `nil` hides
/// the row.
var feeReserveCredits: UInt64? = nil
/// Preflighted `AddressCreditWithdrawalTransition` fee — only meaningful
/// for `.platformToCore` (the fee headroom / netting basis).
var withdrawalFeeCredits: UInt64? = nil
Expand Down Expand Up @@ -210,62 +225,44 @@ struct InternalTransferConfirmSheet: View {
/// Platform credits per DASH (1e11).
private static let creditsPerDash: Decimal = 100_000_000_000

/// Flat fee estimate (credits) for the active route, computed offline by
/// the SDK against the latest protocol version (so it matches the fee the
/// SDK will charge). `nil` if the estimate is unavailable → the row
/// shows "—".
private var networkFeeCredits: UInt64? {
switch route {
case .coreToShielded:
// The lock charges the fee rounded UP to a whole duff — display
// that, so Amount + Network fee equals Total exactly.
return CoreToShieldedAmountPolicy.currentPoolFeeDuffs.map { $0 * 1000 }
case .platformToShielded:
// Shield (Type 15): base shielded fee. Real metered storage is
// extra and only knowable on-chain, so this is a lower bound.
return try? PlatformWalletManager.estimateShieldedFee(kind: .transfer, numActions: 2)
case .shieldedToCore:
return try? PlatformWalletManager.estimateShieldedFee(kind: .withdrawal, numActions: 2)
case .shieldedToPlatform:
return try? PlatformWalletManager.estimateShieldedFee(kind: .unshield, numActions: 2)
case .coreToPlatform:
// Address-funding asset lock: the required processing balance
// (the same 50k-duff base the Rust side reserves for address
// funding). The funding ST's metered fee is extra and only
// knowable on-chain, so this is a lower bound.
return CoreToShieldedAmountPolicy.assetLockBaseCostCredits
case .platformToCore:
// The exact transition fee the preflight already netted out of
// the payout amount.
return withdrawalFeeCredits
}
}

/// Network-fee estimate as fiat (e.g. "~ $0.08"), or "—" if unavailable.
/// Display-only: the value is resolved by the ViewModel and frozen into
/// the submission.
private var networkFeeString: String {
guard let credits = networkFeeCredits else { return "—" }
let dash = Decimal(credits) / Self.creditsPerDash
return "~ " + CurrencyExchanger.shared.fiatAmountString(for: dash)
}

/// What actually leaves the source balance. Core→Shielded charges the
/// pool fee on top of the amount (the executed lock value); every other
/// route's total is the amount itself. "—" when the fee estimate is
/// unavailable — `canContinue` fails closed before that can be confirmed,
/// but the row must never show the un-inflated number.
/// "Total" as DASH text — what actually leaves the source balance,
/// resolved by the ViewModel. "—" when unavailable — `canContinue`
/// fails closed before that can be confirmed, but the row must never
/// show an un-inflated number.
private var totalString: String {
guard route == .coreToShielded else {
return dashDuffs.formattedDashAmount
}
guard let poolFeeCredits = CoreToShieldedAmountPolicy.poolFeeCredits,
let lockDuffs = CoreToShieldedAmountPolicy.lockValueDuffs(
forAmountDuffs: amountDuffsUnsigned,
poolFeeCredits: poolFeeCredits),
let signedLockDuffs = Int64(exactly: lockDuffs)
else { return "—" }
return signedLockDuffs.formattedDashAmount
totalDuffs?.formattedDashAmount ?? "—"
}

/// "Fee reserve" as DASH text — the FULL funding reserve the lock
/// carries on top of the amount (Amount + Fee reserve = Total). The
/// network's fee is taken out of it and the unused part is credited to
/// the Platform balance. `nil` hides the row (non-topup routes).
private var feeReserveString: String? {
guard let credits = feeReserveCredits,
let duffs = Int64(exactly: credits / 1000)
else { return nil }
return duffs.formattedDashAmount
}

/// The fee row's label: the Core→Platform fee is an informational
/// estimate (the reserve, not the estimate, sizes the lock), so name
/// it honestly; other routes keep the plain "Network fee".
private var feeRowLabel: String {
route == .coreToPlatform
? NSLocalizedString(
"Estimated Platform fee",
comment: "Informational estimate of the Platform fee taken out of the fee reserve")
: NSLocalizedString("Network fee", comment: "")
}

// MARK: - Summary card

Expand All @@ -280,8 +277,16 @@ struct InternalTransferConfirmSheet: View {
value: toLabel)
divider
summaryRow(
label: NSLocalizedString("Network fee", comment: ""),
label: feeRowLabel,
value: networkFeeString)
if let feeReserveString {
divider
summaryRow(
label: NSLocalizedString(
"Fee reserve",
comment: "The Core→Platform funding reserve the fee is taken from; the unused part is credited to the Platform balance"),
value: feeReserveString)
}
divider
summaryRow(
label: NSLocalizedString("Total", comment: ""),
Expand Down Expand Up @@ -417,7 +422,7 @@ struct InternalTransferConfirmSheet: View {
comment: "")
case .coreToPlatform:
return NSLocalizedString(
"These funds move to your Platform balance and are ready to spend as soon as the transfer completes.",
"These funds move to your Platform balance and are ready to spend as soon as the transfer completes. The Platform fee is taken out of the fee reserve — whatever the network doesn't use is credited to your Platform balance too.",
comment: "")
case .platformToCore:
return isFullPlatformWithdrawal
Expand Down Expand Up @@ -478,7 +483,7 @@ struct InternalTransferConfirmSheet: View {
amountCredits: creditsAmount,
sweepAll: isFullShieldedSweep)
case .coreToPlatform:
await coordinator.performFundPlatform(amountDuffs: amountDuffsUnsigned)
await coordinator.performFundPlatform(recipientAmountDuffs: amountDuffsUnsigned)
case .platformToCore:
await coordinator.performPlatformWithdraw(
amountCredits: creditsAmount,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ private struct InternalTransferConfirmation: Identifiable {
let amountDuffsUnsigned: UInt64
let creditsAmount: UInt64
let fiatText: String
let networkFeeCredits: UInt64?
let totalDuffs: Int64?
let feeReserveCredits: UInt64?
let withdrawalFeeCredits: UInt64?
let isFullPlatformWithdrawal: Bool
let isFullShieldedSweep: Bool
Expand Down Expand Up @@ -113,6 +116,9 @@ struct InternalTransferScreen: View {
amountDuffsUnsigned: submission.amountDuffsUnsigned,
creditsAmount: submission.creditsAmount,
fiatText: submission.fiatText,
networkFeeCredits: submission.networkFeeCredits,
totalDuffs: submission.totalDuffs,
feeReserveCredits: submission.feeReserveCredits,
withdrawalFeeCredits: submission.withdrawalFeeCredits,
isFullPlatformWithdrawal: submission.isFullPlatformWithdrawal,
isFullShieldedSweep: submission.isFullShieldedSweep,
Expand Down Expand Up @@ -187,6 +193,9 @@ struct InternalTransferScreen: View {
amountDuffsUnsigned: viewModel.dashDuffsUnsigned,
creditsAmount: viewModel.creditsPreview,
fiatText: viewModel.fiatAmountString,
networkFeeCredits: viewModel.confirmNetworkFeeCredits,
totalDuffs: viewModel.confirmTotalDuffs,
feeReserveCredits: viewModel.confirmFeeReserveCredits,
withdrawalFeeCredits: viewModel.withdrawalPreflight?.estimatedFee,
isFullPlatformWithdrawal: viewModel.isFullPlatformWithdrawal,
isFullShieldedSweep: viewModel.isFullShieldedSweep,
Expand Down
Loading
Loading