diff --git a/android/app/src/main/java/org/bitcoinppl/cove/flows/SendFlow/SendFlowManager.kt b/android/app/src/main/java/org/bitcoinppl/cove/flows/SendFlow/SendFlowManager.kt index 9a0c47c54..35af83ea0 100644 --- a/android/app/src/main/java/org/bitcoinppl/cove/flows/SendFlow/SendFlowManager.kt +++ b/android/app/src/main/java/org/bitcoinppl/cove/flows/SendFlow/SendFlowManager.kt @@ -251,6 +251,10 @@ class SendFlowManager( is SendFlowManagerReconcileMessage.RefreshPresenters -> { refreshPresenters() } + + is SendFlowManagerReconcileMessage.FeeBumpConfirmDetails -> { + // fee bump confirmation screen wired in follow-up + } } } diff --git a/android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt b/android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt index d322a3731..76de47bfb 100644 --- a/android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt +++ b/android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt @@ -1768,6 +1768,8 @@ internal object IntegrityCheckingUniffiLib { ): Short external fun uniffi_cove_checksum_method_transactiondetails_block_number_fmt( ): Short + external fun uniffi_cove_checksum_method_transactiondetails_can_rbf_bump( + ): Short external fun uniffi_cove_checksum_method_transactiondetails_confirmation_date_time( ): Short external fun uniffi_cove_checksum_method_transactiondetails_fee_fiat_fmt( @@ -1936,6 +1938,8 @@ internal object IntegrityCheckingUniffiLib { ): Short external fun uniffi_cove_checksum_constructor_transactiondetails_preview_new_with_label( ): Short + external fun uniffi_cove_checksum_constructor_transactiondetails_preview_pending_rbf( + ): Short external fun uniffi_cove_checksum_constructor_transactiondetails_preview_pending_received( ): Short external fun uniffi_cove_checksum_constructor_transactiondetails_preview_pending_sent( @@ -2940,6 +2944,8 @@ internal object UniffiLib { ): Long external fun uniffi_cove_fn_constructor_transactiondetails_preview_new_with_label(`label`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, ): Long + external fun uniffi_cove_fn_constructor_transactiondetails_preview_pending_rbf(uniffi_out_err: UniffiRustCallStatus, + ): Long external fun uniffi_cove_fn_constructor_transactiondetails_preview_pending_received(uniffi_out_err: UniffiRustCallStatus, ): Long external fun uniffi_cove_fn_constructor_transactiondetails_preview_pending_sent(uniffi_out_err: UniffiRustCallStatus, @@ -2962,6 +2968,8 @@ internal object UniffiLib { ): RustBuffer.ByValue external fun uniffi_cove_fn_method_transactiondetails_block_number_fmt(`ptr`: Long,uniffi_out_err: UniffiRustCallStatus, ): RustBuffer.ByValue + external fun uniffi_cove_fn_method_transactiondetails_can_rbf_bump(`ptr`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Byte external fun uniffi_cove_fn_method_transactiondetails_confirmation_date_time(`ptr`: Long,uniffi_out_err: UniffiRustCallStatus, ): RustBuffer.ByValue external fun uniffi_cove_fn_method_transactiondetails_fee_fiat_fmt(`ptr`: Long, @@ -4607,6 +4615,9 @@ private fun uniffiCheckApiChecksums(lib: IntegrityCheckingUniffiLib) { if (lib.uniffi_cove_checksum_method_transactiondetails_block_number_fmt() != 8381.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } + if (lib.uniffi_cove_checksum_method_transactiondetails_can_rbf_bump() != 5315.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } if (lib.uniffi_cove_checksum_method_transactiondetails_confirmation_date_time() != 59432.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } @@ -4859,6 +4870,9 @@ private fun uniffiCheckApiChecksums(lib: IntegrityCheckingUniffiLib) { if (lib.uniffi_cove_checksum_constructor_transactiondetails_preview_new_with_label() != 51427.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } + if (lib.uniffi_cove_checksum_constructor_transactiondetails_preview_pending_rbf() != 25399.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } if (lib.uniffi_cove_checksum_constructor_transactiondetails_preview_pending_received() != 18117.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } @@ -23265,6 +23279,14 @@ public interface TransactionDetailsInterface { fun `blockNumberFmt`(): kotlin.String? + /** + * Whether this transaction can currently be fee-bumped via RBF. + * + * Requires the transaction to be outgoing, unconfirmed, and signaling opt-in RBF. + * Use this to gate the "Speed Up" action in the UI. + */ + fun `canRbfBump`(): kotlin.Boolean + fun `confirmationDateTime`(): kotlin.String? suspend fun `feeFiatFmt`(): kotlin.String @@ -23546,6 +23568,25 @@ open class TransactionDetails: Disposable, AutoCloseable, TransactionDetailsInte } + + /** + * Whether this transaction can currently be fee-bumped via RBF. + * + * Requires the transaction to be outgoing, unconfirmed, and signaling opt-in RBF. + * Use this to gate the "Speed Up" action in the UI. + */override fun `canRbfBump`(): kotlin.Boolean { + return FfiConverterBoolean.lift( + callWithHandle { + uniffiRustCall() { _status -> + UniffiLib.uniffi_cove_fn_method_transactiondetails_can_rbf_bump( + it, + _status) +} + } + ) + } + + override fun `confirmationDateTime`(): kotlin.String? { return FfiConverterOptionalString.lift( callWithHandle { @@ -23841,6 +23882,17 @@ open class TransactionDetails: Disposable, AutoCloseable, TransactionDetailsInte } + fun `previewPendingRbf`(): TransactionDetails { + return FfiConverterTypeTransactionDetails.lift( + uniffiRustCall() { _status -> + UniffiLib.uniffi_cove_fn_constructor_transactiondetails_preview_pending_rbf( + + _status) +} + ) + } + + fun `previewPendingReceived`(): TransactionDetails { return FfiConverterTypeTransactionDetails.lift( uniffiRustCall() { _status -> @@ -44199,6 +44251,16 @@ sealed class SendFlowManagerAction: Disposable { object FinalizeAndGoToNextScreen : SendFlowManagerAction() + data class ConfirmFeeBump( + val `txid`: org.bitcoinppl.cove_core.types.TxId, + val `feeRate`: org.bitcoinppl.cove_core.types.FeeRate) : SendFlowManagerAction() + + { + + + companion object + } + @Suppress("UNNECESSARY_SAFE_CALL") // codegen is much simpler if we unconditionally emit safe calls here @@ -44338,6 +44400,14 @@ sealed class SendFlowManagerAction: Disposable { } is SendFlowManagerAction.FinalizeAndGoToNextScreen -> {// Nothing to destroy } + is SendFlowManagerAction.ConfirmFeeBump -> { + + Disposable.destroy( + this.`txid`, + this.`feeRate` + ) + + } }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } } @@ -44416,6 +44486,10 @@ public object FfiConverterTypeSendFlowManagerAction : FfiConverterRustBuffer SendFlowManagerAction.FinalizeAndGoToNextScreen + 23 -> SendFlowManagerAction.ConfirmFeeBump( + FfiConverterTypeTxId.read(buf), + FfiConverterTypeFeeRate.read(buf), + ) else -> throw RuntimeException("invalid enum value, something is very wrong!!") } } @@ -44575,6 +44649,14 @@ public object FfiConverterTypeSendFlowManagerAction : FfiConverterRustBuffer { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + + FfiConverterTypeTxId.allocationSize(value.`txid`) + + FfiConverterTypeFeeRate.allocationSize(value.`feeRate`) + ) + } } override fun write(value: SendFlowManagerAction, buf: ByteBuffer) { @@ -44689,6 +44771,12 @@ public object FfiConverterTypeSendFlowManagerAction : FfiConverterRustBuffer { + buf.putInt(23) + FfiConverterTypeTxId.write(value.`txid`, buf) + FfiConverterTypeFeeRate.write(value.`feeRate`, buf) + Unit + } }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } } } @@ -44807,6 +44895,15 @@ sealed class SendFlowManagerReconcileMessage: Disposable { object ClearAlert : SendFlowManagerReconcileMessage() + data class FeeBumpConfirmDetails( + val v1: org.bitcoinppl.cove_core.types.ConfirmDetails) : SendFlowManagerReconcileMessage() + + { + + + companion object + } + @Suppress("UNNECESSARY_SAFE_CALL") // codegen is much simpler if we unconditionally emit safe calls here @@ -44895,6 +44992,13 @@ sealed class SendFlowManagerReconcileMessage: Disposable { } is SendFlowManagerReconcileMessage.ClearAlert -> {// Nothing to destroy } + is SendFlowManagerReconcileMessage.FeeBumpConfirmDetails -> { + + Disposable.destroy( + this.v1 + ) + + } }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } } @@ -44948,6 +45052,9 @@ public object FfiConverterTypeSendFlowManagerReconcileMessage : FfiConverterRust FfiConverterTypeSendFlowAlertState.read(buf), ) 14 -> SendFlowManagerReconcileMessage.ClearAlert + 15 -> SendFlowManagerReconcileMessage.FeeBumpConfirmDetails( + FfiConverterTypeConfirmDetails.read(buf), + ) else -> throw RuntimeException("invalid enum value, something is very wrong!!") } } @@ -45048,6 +45155,13 @@ public object FfiConverterTypeSendFlowManagerReconcileMessage : FfiConverterRust 4UL ) } + is SendFlowManagerReconcileMessage.FeeBumpConfirmDetails -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + + FfiConverterTypeConfirmDetails.allocationSize(value.v1) + ) + } } override fun write(value: SendFlowManagerReconcileMessage, buf: ByteBuffer) { @@ -45119,6 +45233,11 @@ public object FfiConverterTypeSendFlowManagerReconcileMessage : FfiConverterRust buf.putInt(14) Unit } + is SendFlowManagerReconcileMessage.FeeBumpConfirmDetails -> { + buf.putInt(15) + FfiConverterTypeConfirmDetails.write(value.v1, buf) + Unit + } }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } } } diff --git a/ios/Cove/Flows/SendFlow/SendFlowManager.swift b/ios/Cove/Flows/SendFlow/SendFlowManager.swift index 1f849bc4b..36551ada4 100644 --- a/ios/Cove/Flows/SendFlow/SendFlowManager.swift +++ b/ios/Cove/Flows/SendFlow/SendFlowManager.swift @@ -170,6 +170,9 @@ extension WeakReconciler: SendFlowManagerReconciler where Reconciler == SendFlow case .refreshPresenters: self.refreshPresenters() + + case .feeBumpConfirmDetails: + break } } diff --git a/ios/CoveCore/Sources/CoveCore/generated/cove.swift b/ios/CoveCore/Sources/CoveCore/generated/cove.swift index e0019bf64..d590aedae 100644 --- a/ios/CoveCore/Sources/CoveCore/generated/cove.swift +++ b/ios/CoveCore/Sources/CoveCore/generated/cove.swift @@ -10369,6 +10369,14 @@ public protocol TransactionDetailsProtocol: AnyObject, Sendable { func blockNumberFmt() -> String? + /** + * Whether this transaction can currently be fee-bumped via RBF. + * + * Requires the transaction to be outgoing, unconfirmed, and signaling opt-in RBF. + * Use this to gate the "Speed Up" action in the UI. + */ + func canRbfBump() -> Bool + func confirmationDateTime() -> String? func feeFiatFmt() async throws -> String @@ -10494,6 +10502,13 @@ public static func previewNewWithLabel(label: String = "bike payment") -> Transa }) } +public static func previewPendingRbf() -> TransactionDetails { + return try! FfiConverterTypeTransactionDetails_lift(try! rustCall() { + uniffi_cove_fn_constructor_transactiondetails_preview_pending_rbf($0 + ) +}) +} + public static func previewPendingReceived() -> TransactionDetails { return try! FfiConverterTypeTransactionDetails_lift(try! rustCall() { uniffi_cove_fn_constructor_transactiondetails_preview_pending_received($0 @@ -10599,6 +10614,20 @@ open func blockNumberFmt() -> String? { self.uniffiCloneHandle(),$0 ) }) +} + + /** + * Whether this transaction can currently be fee-bumped via RBF. + * + * Requires the transaction to be outgoing, unconfirmed, and signaling opt-in RBF. + * Use this to gate the "Speed Up" action in the UI. + */ +open func canRbfBump() -> Bool { + return try! FfiConverterBool.lift(try! rustCall() { + uniffi_cove_fn_method_transactiondetails_can_rbf_bump( + self.uniffiCloneHandle(),$0 + ) +}) } open func confirmationDateTime() -> String? { @@ -27078,6 +27107,8 @@ public enum SendFlowManagerAction { case changeFeeRateOptions(FeeRateOptionsWithTotalFee ) case finalizeAndGoToNextScreen + case confirmFeeBump(txid: TxId, feeRate: FeeRate + ) @@ -27160,6 +27191,9 @@ public struct FfiConverterTypeSendFlowManagerAction: FfiConverterRustBuffer { case 22: return .finalizeAndGoToNextScreen + case 23: return .confirmFeeBump(txid: try FfiConverterTypeTxId.read(from: &buf), feeRate: try FfiConverterTypeFeeRate.read(from: &buf) + ) + default: throw UniffiInternalError.unexpectedEnumCase } } @@ -27277,6 +27311,12 @@ public struct FfiConverterTypeSendFlowManagerAction: FfiConverterRustBuffer { case .finalizeAndGoToNextScreen: writeInt(&buf, Int32(22)) + + case let .confirmFeeBump(txid,feeRate): + writeInt(&buf, Int32(23)) + FfiConverterTypeTxId.write(txid, into: &buf) + FfiConverterTypeFeeRate.write(feeRate, into: &buf) + } } } @@ -27326,6 +27366,8 @@ public enum SendFlowManagerReconcileMessage { case setAlert(SendFlowAlertState ) case clearAlert + case feeBumpConfirmDetails(ConfirmDetails + ) @@ -27386,6 +27428,9 @@ public struct FfiConverterTypeSendFlowManagerReconcileMessage: FfiConverterRustB case 14: return .clearAlert + case 15: return .feeBumpConfirmDetails(try FfiConverterTypeConfirmDetails.read(from: &buf) + ) + default: throw UniffiInternalError.unexpectedEnumCase } } @@ -27460,6 +27505,11 @@ public struct FfiConverterTypeSendFlowManagerReconcileMessage: FfiConverterRustB case .clearAlert: writeInt(&buf, Int32(14)) + + case let .feeBumpConfirmDetails(v1): + writeInt(&buf, Int32(15)) + FfiConverterTypeConfirmDetails.write(v1, into: &buf) + } } } @@ -37068,6 +37118,9 @@ private let initializationResult: InitializationResult = { if (uniffi_cove_checksum_method_transactiondetails_block_number_fmt() != 8381) { return InitializationResult.apiChecksumMismatch } + if (uniffi_cove_checksum_method_transactiondetails_can_rbf_bump() != 5315) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_cove_checksum_method_transactiondetails_confirmation_date_time() != 59432) { return InitializationResult.apiChecksumMismatch } @@ -37320,6 +37373,9 @@ private let initializationResult: InitializationResult = { if (uniffi_cove_checksum_constructor_transactiondetails_preview_new_with_label() != 51427) { return InitializationResult.apiChecksumMismatch } + if (uniffi_cove_checksum_constructor_transactiondetails_preview_pending_rbf() != 25399) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_cove_checksum_constructor_transactiondetails_preview_pending_received() != 18117) { return InitializationResult.apiChecksumMismatch } diff --git a/rust/src/manager/send_flow_manager.rs b/rust/src/manager/send_flow_manager.rs index d80ad0c7b..0de47f515 100644 --- a/rust/src/manager/send_flow_manager.rs +++ b/rust/src/manager/send_flow_manager.rs @@ -16,7 +16,7 @@ use crate::{ fee_client::FEE_CLIENT, fiat::client::PriceResponse, router::RouteFactory, - transaction::FeeRate, + transaction::{FeeRate, TxId}, wallet::{ Address, balance::Balance, @@ -33,6 +33,7 @@ use cove_types::{ WalletId, address::AddressWithNetwork, amount::Amount, + confirm::ConfirmDetails, fees::{FeeRateOptionWithTotalFee, FeeRateOptions, FeeRateOptionsWithTotalFee, FeeSpeed}, psbt::Psbt, unit::BitcoinUnit, @@ -111,6 +112,9 @@ pub enum SendFlowManagerReconcileMessage { // side effects SetAlert(SendFlowAlertState), ClearAlert, + + // fee bump + FeeBumpConfirmDetails(Arc), } #[derive(Debug, Clone, PartialEq, uniffi::Enum)] @@ -152,6 +156,9 @@ pub enum SendFlowManagerAction { ChangeFeeRateOptions(Arc), FinalizeAndGoToNextScreen, + + // fee bump: replace a pending RBF transaction with a higher fee rate + ConfirmFeeBump { txid: Arc, fee_rate: Arc }, } impl RustSendFlowManager { @@ -670,6 +677,10 @@ impl RustSendFlowManager { Action::NotifyCoinControlEnteredAmountChanged(amount, is_focused) => { self.handle_coin_control_entered_amount_changed(amount, is_focused); } + + Action::ConfirmFeeBump { txid, fee_rate } => { + self.confirm_fee_bump(*txid, *fee_rate); + } } } } @@ -1645,6 +1656,57 @@ impl RustSendFlowManager { } } +/// MARK: fee bump +impl RustSendFlowManager { + fn confirm_fee_bump(self: &Arc, txid: TxId, fee_rate: FeeRate) { + let me = self.clone(); + let manager = self.wallet_manager.clone(); + let (wallet_type, wallet_id) = { + let state = self.state.lock(); + (state.metadata.wallet_type, state.metadata.id.clone()) + }; + + if matches!(wallet_type, WalletType::WatchOnly) { + return self.send_alert(SendFlowError::UnableToBuildTxn("watch only".to_string())); + } + + cove_tokio::task::spawn(async move { + let raw_txid = *txid; + + let confirm_details = manager.confirm_fee_bump_txn(raw_txid, fee_rate).await; + + let details = match confirm_details { + Ok(details) => Arc::new(details), + Err(error) => { + let error = SendFlowError::UnableToBuildTxn(error.to_string()); + return me.send_alert_async(error).await; + } + }; + + // save unsigned transaction for hardware wallets before routing + if matches!(wallet_type, WalletType::Cold | WalletType::XpubOnly) + && let Err(e) = manager.save_unsigned_transaction(details.clone()) + { + let error = SendFlowError::UnableToSaveUnsignedTransaction(e.to_string()); + return me.send_alert_async(error).await; + } + + me.reconciler.send_async(Message::FeeBumpConfirmDetails(details.clone())).await; + + let next_route = match wallet_type { + WalletType::Hot => RouteFactory::new().send_confirm(wallet_id, details, None, None), + WalletType::Cold | WalletType::XpubOnly => { + RouteFactory::new().send_hardware_export(wallet_id, details) + } + WalletType::WatchOnly => unreachable!("rejected above"), + }; + + let mut deferred = DeferredDispatch::::new(); + deferred.queue(AppAction::PushRoute(next_route)); + }); + } +} + /// MARK: helper method impls impl RustSendFlowManager { fn set_and_send_entering_btc_amount( diff --git a/rust/src/manager/wallet_manager.rs b/rust/src/manager/wallet_manager.rs index 1eea3b7a1..3ef1fe03b 100644 --- a/rust/src/manager/wallet_manager.rs +++ b/rust/src/manager/wallet_manager.rs @@ -1355,6 +1355,21 @@ impl RustWalletManager { let details = call!(self.actor.get_confirm_details(psbt, fee_rate)).await.unwrap()?; Ok(details) } + + pub async fn confirm_fee_bump_txn( + &self, + txid: bitcoin::Txid, + fee_rate: FeeRate, + ) -> Result { + debug!("confirm_fee_bump_txn txid: {txid} fee_rate: {:?}", fee_rate.sat_per_vb()); + let actor = self.actor.clone(); + let fee_rate = fee_rate.into(); + + let psbt = call!(actor.build_fee_bump_tx(txid, fee_rate)).await.unwrap()?; + let details = call!(self.actor.get_confirm_details(psbt, fee_rate)).await.unwrap()?; + + Ok(details) + } } #[uniffi::export] diff --git a/rust/src/manager/wallet_manager/actor.rs b/rust/src/manager/wallet_manager/actor.rs index 5d1f85ecb..1b0294444 100644 --- a/rust/src/manager/wallet_manager/actor.rs +++ b/rust/src/manager/wallet_manager/actor.rs @@ -289,6 +289,23 @@ impl WalletActor { Ok(psbt) } + /// Build a replacement transaction for an unconfirmed RBF-signaling transaction. + /// + /// Reuses the original inputs and outputs; caller supplies a higher fee rate. + /// BDK validates that `txid` is unconfirmed and signals opt-in RBF (BIP 125). + #[into_actor_result] + pub async fn build_fee_bump_tx( + &mut self, + txid: Txid, + fee_rate: impl Into, + ) -> Result { + debug!("build_fee_bump_tx: {txid}"); + let mut builder = self.wallet.bdk.build_fee_bump(txid).map_err_str(Error::BuildTxError)?; + builder.fee_rate(fee_rate.into()); + let psbt = builder.finish().map_err_str(Error::BuildTxError)?; + Ok(psbt) + } + // cancel a transaction, reset the address & change address index pub async fn cancel_txn(&mut self, txn: BdkTransaction) { self.wallet.bdk.cancel_tx(&txn); diff --git a/rust/src/transaction/transaction_details.rs b/rust/src/transaction/transaction_details.rs index 4ba1ed88d..2623dc129 100644 --- a/rust/src/transaction/transaction_details.rs +++ b/rust/src/transaction/transaction_details.rs @@ -350,6 +350,15 @@ impl TransactionDetails { self.is_rbf_signaling } + /// Whether this transaction can currently be fee-bumped via RBF. + /// + /// Requires the transaction to be outgoing, unconfirmed, and signaling opt-in RBF. + /// Use this to gate the "Speed Up" action in the UI. + #[uniffi::method] + pub fn can_rbf_bump(&self) -> bool { + self.is_sent() && self.is_rbf_signaling && !self.is_confirmed() + } + #[uniffi::method] pub fn confirmation_date_time(&self) -> Option { let confirm_time = match &self.pending_or_confirmed { @@ -517,6 +526,13 @@ impl TransactionDetails { me } + #[uniffi::constructor] + pub fn preview_pending_rbf() -> Self { + let mut me = Self::preview_pending_sent(); + me.is_rbf_signaling = true; + me + } + #[uniffi::constructor(default(label = "bike payment"))] pub fn preview_new_with_label(label: String) -> Self { let mut me = Self::preview_new_confirmed(); @@ -609,4 +625,23 @@ mod tests { assert!(!TransactionDetails::preview_new_confirmed().is_rbf_signaling); assert!(!TransactionDetails::preview_pending_sent().is_rbf_signaling); } + + #[test] + fn can_rbf_bump_requires_pending_and_rbf_signaling() { + let rbf_pending = TransactionDetails::preview_pending_rbf(); + assert!(rbf_pending.can_rbf_bump()); + } + + #[test] + fn can_rbf_bump_false_when_confirmed() { + let mut confirmed = TransactionDetails::preview_new_confirmed(); + confirmed.is_rbf_signaling = true; + assert!(!confirmed.can_rbf_bump()); + } + + #[test] + fn can_rbf_bump_false_when_pending_without_rbf() { + let pending_no_rbf = TransactionDetails::preview_pending_sent(); + assert!(!pending_no_rbf.can_rbf_bump()); + } }