diff --git a/Bitkit/Resources/Localization/en.lproj/Localizable.strings b/Bitkit/Resources/Localization/en.lproj/Localizable.strings index 1c86bcd8..84df6832 100644 --- a/Bitkit/Resources/Localization/en.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/en.lproj/Localizable.strings @@ -1054,6 +1054,8 @@ "wallet__activity_output" = "{count, plural, one {OUTPUT} other {OUTPUTS (#)}}"; "wallet__activity_boosted_cpfp" = "BOOSTED TRANSACTION {num} (CPFP)"; "wallet__activity_boosted_rbf" = "BOOSTED TRANSACTION {num} (RBF)"; +"wallet__activity_boost_fee" = "Boost Fee"; +"wallet__activity_boost_fee_description" = "Boosted incoming transaction"; "wallet__activity_explorer" = "Open Block Explorer"; "wallet__activity_successful" = "Successful"; "wallet__activity_invoice_note" = "Invoice note"; diff --git a/Bitkit/Services/CoreService.swift b/Bitkit/Services/CoreService.swift index 53afed50..6cf0f424 100644 --- a/Bitkit/Services/CoreService.swift +++ b/Bitkit/Services/CoreService.swift @@ -137,6 +137,15 @@ class ActivityService { return doesExistMap } + func isCpfpChildTransaction(txId: String) async -> Bool { + guard await getTxIdsInBoostTxIds().contains(txId), + let activity = try? await getOnchainActivityByTxId(txid: txId) + else { + return false + } + return activity.doesExist + } + init(coreService: CoreService) { self.coreService = coreService } diff --git a/Bitkit/Views/Wallets/Activity/ActivityIcon.swift b/Bitkit/Views/Wallets/Activity/ActivityIcon.swift index 75a0620a..5a1bec36 100644 --- a/Bitkit/Views/Wallets/Activity/ActivityIcon.swift +++ b/Bitkit/Views/Wallets/Activity/ActivityIcon.swift @@ -10,9 +10,11 @@ struct ActivityIcon: View { let isBoosted: Bool let isTransfer: Bool let doesExist: Bool + let isCpfpChild: Bool - init(activity: Activity, size: CGFloat = 32) { + init(activity: Activity, size: CGFloat = 32, isCpfpChild: Bool = false) { self.size = size + self.isCpfpChild = isCpfpChild switch activity { case let .lightning(ln): isLightning = true @@ -65,7 +67,7 @@ struct ActivityIcon: View { backgroundColor: .red16, size: size ) - } else if isBoosted && !(confirmed ?? false) { + } else if isCpfpChild || (isBoosted && !(confirmed ?? false)) { CircularIcon( icon: "timer-alt", iconColor: .yellow, diff --git a/Bitkit/Views/Wallets/Activity/ActivityItemView.swift b/Bitkit/Views/Wallets/Activity/ActivityItemView.swift index c18ffc2d..4b6234ba 100644 --- a/Bitkit/Views/Wallets/Activity/ActivityItemView.swift +++ b/Bitkit/Views/Wallets/Activity/ActivityItemView.swift @@ -14,6 +14,7 @@ struct ActivityItemView: View { @EnvironmentObject var channelDetails: ChannelDetailsViewModel @StateObject private var viewModel: ActivityItemViewModel @State private var boostTxDoesExist: [String: Bool] = [:] // Maps boostTxId -> doesExist + @State private var isCpfpChild: Bool = false init(item: Activity) { self.item = item @@ -100,6 +101,10 @@ struct ActivityItemView: View { : t("wallet__activity_transfer_savings_done") } + if isCpfpChild { + return t("wallet__activity_boost_fee") + } + return isSent ? t("wallet__activity_bitcoin_sent") : t("wallet__activity_bitcoin_received") @@ -112,9 +117,11 @@ struct ActivityItemView: View { private var shouldDisableBoostButton: Bool { switch viewModel.activity { case .lightning: - // Lightning transactions can never be boosted return true case let .onchain(activity): + if isCpfpChild { + return true + } if !activity.doesExist { return true } @@ -186,7 +193,7 @@ struct ActivityItemView: View { HStack(alignment: .bottom) { MoneyStack(sats: amount, prefix: amountPrefix, showSymbol: false) Spacer() - ActivityIcon(activity: viewModel.activity, size: 48) + ActivityIcon(activity: viewModel.activity, size: 48, isCpfpChild: isCpfpChild) } .padding(.bottom, 16) @@ -221,6 +228,11 @@ struct ActivityItemView: View { } } .task { + // Check if this is a CPFP child transaction + if case let .onchain(activity) = viewModel.activity { + isCpfpChild = await CoreService.shared.activity.isCpfpChildTransaction(txId: activity.txId) + } + // Load boostTxIds doesExist status to determine RBF vs CPFP if case let .onchain(activity) = viewModel.activity, !activity.boostTxIds.isEmpty diff --git a/Bitkit/Views/Wallets/Activity/ActivityRowOnchain.swift b/Bitkit/Views/Wallets/Activity/ActivityRowOnchain.swift index b09c8027..f3f97daa 100644 --- a/Bitkit/Views/Wallets/Activity/ActivityRowOnchain.swift +++ b/Bitkit/Views/Wallets/Activity/ActivityRowOnchain.swift @@ -5,12 +5,15 @@ private struct ActivityStatus: View { let txType: PaymentType let confirmed: Bool let isTransfer: Bool + let isCpfpChild: Bool var body: some View { if isTransfer { BodyMSBText(t("wallet__activity_transfer")) } else { - if txType == .sent { + if isCpfpChild { + BodyMSBText(t("wallet__activity_boost_fee")) + } else if txType == .sent { BodyMSBText(t("wallet__activity_sent")) } else { BodyMSBText(t("wallet__activity_received")) @@ -22,6 +25,7 @@ private struct ActivityStatus: View { struct ActivityRowOnchain: View { let item: OnchainActivity let feeEstimates: FeeRates? + @State private var isCpfpChild: Bool = false private var amountPrefix: String { return item.txType == .sent ? "-" : "+" @@ -53,6 +57,10 @@ struct ActivityRowOnchain: View { return t("wallet__activity_removed") } + if isCpfpChild { + return t("wallet__activity_boost_fee_description") + } + if item.isTransfer { switch item.txType { case .sent: @@ -75,16 +83,22 @@ struct ActivityRowOnchain: View { var body: some View { HStack(spacing: 16) { - ActivityIcon(activity: .onchain(item), size: 40) + ActivityIcon(activity: .onchain(item), size: 40, isCpfpChild: isCpfpChild) VStack(alignment: .leading, spacing: 2) { - ActivityStatus(txType: item.txType, confirmed: item.confirmed, isTransfer: item.isTransfer) + ActivityStatus(txType: item.txType, confirmed: item.confirmed, isTransfer: item.isTransfer, isCpfpChild: isCpfpChild) + .lineLimit(1) CaptionBText(description) + .lineLimit(1) } + .fixedSize(horizontal: false, vertical: true) Spacer() MoneyCell(sats: amount, prefix: amountPrefix) } + .task { + isCpfpChild = await CoreService.shared.activity.isCpfpChildTransaction(txId: item.txId) + } } }