diff --git a/android/app/src/main/java/org/bitcoinppl/cove/flows/SelectedWalletFlow/TransactionDetails/TransactionDetailsScreen.kt b/android/app/src/main/java/org/bitcoinppl/cove/flows/SelectedWalletFlow/TransactionDetails/TransactionDetailsScreen.kt index 85bf78df8..f284558ff 100644 --- a/android/app/src/main/java/org/bitcoinppl/cove/flows/SelectedWalletFlow/TransactionDetails/TransactionDetailsScreen.kt +++ b/android/app/src/main/java/org/bitcoinppl/cove/flows/SelectedWalletFlow/TransactionDetails/TransactionDetailsScreen.kt @@ -3,10 +3,12 @@ package org.bitcoinppl.cove.flows.SelectedWalletFlow.TransactionDetails import android.content.Intent import android.net.Uri import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkVertically +import androidx.compose.animation.with import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -78,10 +80,13 @@ import org.bitcoinppl.cove.views.BalanceAutoSizeText import org.bitcoinppl.cove.views.ImageButton import org.bitcoinppl.cove_core.HeaderIconPresenter import org.bitcoinppl.cove_core.TransactionDetails +import org.bitcoinppl.cove_core.TransactionLockState import org.bitcoinppl.cove_core.TransactionState import org.bitcoinppl.cove_core.WalletManagerAction import org.bitcoinppl.cove_core.types.FfiColorScheme import org.bitcoinppl.cove_core.types.TransactionDirection +import androidx.compose.material.icons.filled.Lock +import androidx.compose.material.icons.filled.LockOpen private const val INITIAL_DELAY_MS = 2000L private const val FREQUENT_POLL_INTERVAL_MS = 30000L @@ -93,7 +98,7 @@ private const val CONFIRMATIONS_THRESHOLD = 3 * Transaction details screen - now using manager-based pattern * Ported from iOS TransactionDetailsView.swift */ -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class) @Composable fun TransactionDetailsScreen( app: AppManager, @@ -125,11 +130,18 @@ fun TransactionDetailsScreen( // get current color scheme (respects in-app theme toggle) val isDark = !MaterialTheme.colorScheme.isLight + // state for recovery lock and UTXO lock + var lockState by remember { mutableStateOf(TransactionLockState.NONE) } + var isToggling by remember { mutableStateOf(false) } + // immediately fetch fresh transaction details on screen load LaunchedEffect(manager, txId) { try { val freshDetails = manager.rust.transactionDetails(txId = txId) manager.updateTransactionDetailsCache(txId, freshDetails) + + // also fetch lock state + lockState = manager.rust.transactionLockState(txId = txId) } catch (e: Exception) { android.util.Log.e("TransactionDetails", "error fetching fresh details", e) } @@ -337,6 +349,64 @@ fun TransactionDetailsScreen( ) } }, + actions = { + if (lockState != TransactionLockState.NONE) { + IconButton( + onClick = { + if (isToggling) return@IconButton + isToggling = true + scope.launch { + try { + manager.rust.toggleTransactionLock(txId = txId) + // refresh state after toggle + val newState = manager.rust.transactionLockState(txId = txId) + lockState = newState + + val message = when (newState) { + TransactionLockState.LOCKED -> "Transaction outputs locked" + TransactionLockState.UNLOCKED -> "Transaction outputs unlocked" + TransactionLockState.MIXED -> "Remaining outputs locked" + else -> null + } + if (message != null) { + snackbarHostState.showSnackbar(message) + } + } catch (e: Exception) { + android.util.Log.e("TransactionDetails", "error toggling lock", e) + snackbarHostState.showSnackbar("Failed to update lock state") + } finally { + isToggling = false + } + } + }, + enabled = !isToggling + ) { + androidx.compose.animation.AnimatedContent( + targetState = lockState, + transitionSpec = { + (fadeIn() + androidx.compose.animation.scaleIn()) + .with(fadeOut() + androidx.compose.animation.scaleOut()) + }, + label = "lock_state_anim" + ) { state -> + val icon = when (state) { + TransactionLockState.LOCKED -> Icons.Default.Lock + TransactionLockState.MIXED -> Icons.Default.Lock + else -> Icons.Default.LockOpen + } + Icon( + imageVector = icon, + contentDescription = when (state) { + TransactionLockState.LOCKED -> "Unlock all outputs" + TransactionLockState.MIXED -> "Lock remaining outputs" + else -> "Lock all outputs" + }, + tint = if (state == TransactionLockState.MIXED) CoveColor.WarningOrange else fg + ) + } + } + } + } ) }, snackbarHost = { SnackbarHost(snackbarHostState) }, @@ -356,12 +426,14 @@ fun TransactionDetailsScreen( val freshDetails = manager.rust.transactionDetails(txId = txId) manager.updateTransactionDetailsCache(txId, freshDetails) - // also update confirmations + // also update confirmations and lock state val blockNumber = freshDetails.blockNumber() if (blockNumber != null) { val confirmations = manager.rust.numberOfConfirmations(blockHeight = blockNumber) numberOfConfirmations = confirmations.toInt() } + + lockState = manager.rust.transactionLockState(txId = txId) } catch (e: Exception) { android.util.Log.e("TransactionDetails", "error refreshing details", e) } 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..c280bc96b 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 @@ -1622,8 +1622,12 @@ internal object IntegrityCheckingUniffiLib { ): Short external fun uniffi_cove_checksum_method_rustwalletmanager_switch_to_different_wallet_address_type( ): Short + external fun uniffi_cove_checksum_method_rustwalletmanager_toggle_transaction_lock( + ): Short external fun uniffi_cove_checksum_method_rustwalletmanager_transaction_details( ): Short + external fun uniffi_cove_checksum_method_rustwalletmanager_transaction_lock_state( + ): Short external fun uniffi_cove_checksum_method_rustwalletmanager_validate_metadata( ): Short external fun uniffi_cove_checksum_method_rustwalletmanager_wallet_metadata( @@ -2712,8 +2716,12 @@ internal object UniffiLib { ): Long external fun uniffi_cove_fn_method_rustwalletmanager_switch_to_different_wallet_address_type(`ptr`: Long,`walletAddressType`: RustBuffer.ByValue, ): Long + external fun uniffi_cove_fn_method_rustwalletmanager_toggle_transaction_lock(`ptr`: Long,`txId`: Long, + ): Long external fun uniffi_cove_fn_method_rustwalletmanager_transaction_details(`ptr`: Long,`txId`: Long, ): Long + external fun uniffi_cove_fn_method_rustwalletmanager_transaction_lock_state(`ptr`: Long,`txId`: Long, + ): Long external fun uniffi_cove_fn_method_rustwalletmanager_validate_metadata(`ptr`: Long,uniffi_out_err: UniffiRustCallStatus, ): Unit external fun uniffi_cove_fn_method_rustwalletmanager_wallet_metadata(`ptr`: Long,uniffi_out_err: UniffiRustCallStatus, @@ -4388,9 +4396,15 @@ private fun uniffiCheckApiChecksums(lib: IntegrityCheckingUniffiLib) { if (lib.uniffi_cove_checksum_method_rustwalletmanager_switch_to_different_wallet_address_type() != 64255.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } + if (lib.uniffi_cove_checksum_method_rustwalletmanager_toggle_transaction_lock() != 65256.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } if (lib.uniffi_cove_checksum_method_rustwalletmanager_transaction_details() != 35364.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } + if (lib.uniffi_cove_checksum_method_rustwalletmanager_transaction_lock_state() != 4851.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } if (lib.uniffi_cove_checksum_method_rustwalletmanager_validate_metadata() != 36684.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } @@ -20768,8 +20782,12 @@ public interface RustWalletManagerInterface { suspend fun `switchToDifferentWalletAddressType`(`walletAddressType`: WalletAddressType) + suspend fun `toggleTransactionLock`(`txId`: TxId) + suspend fun `transactionDetails`(`txId`: TxId): TransactionDetails + suspend fun `transactionLockState`(`txId`: TxId): TransactionLockState + fun `validateMetadata`() fun `walletMetadata`(): WalletMetadata @@ -21861,6 +21879,28 @@ open class RustWalletManager: Disposable, AutoCloseable, RustWalletManagerInterf } + @Throws(WalletManagerException::class) + @Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE") + override suspend fun `toggleTransactionLock`(`txId`: TxId) { + return uniffiRustCallAsync( + callWithHandle { uniffiHandle -> + UniffiLib.uniffi_cove_fn_method_rustwalletmanager_toggle_transaction_lock( + uniffiHandle, + FfiConverterTypeTxId.lower(`txId`), + ) + }, + { future, callback, continuation -> UniffiLib.ffi_cove_rust_future_poll_void(future, callback, continuation) }, + { future, continuation -> UniffiLib.ffi_cove_rust_future_complete_void(future, continuation) }, + { future -> UniffiLib.ffi_cove_rust_future_free_void(future) }, + // lift function + { Unit }, + + // Error FFI converter + WalletManagerException.ErrorHandler, + ) + } + + @Throws(WalletManagerException::class) @Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE") override suspend fun `transactionDetails`(`txId`: TxId) : TransactionDetails { @@ -21881,6 +21921,27 @@ open class RustWalletManager: Disposable, AutoCloseable, RustWalletManagerInterf ) } + + @Throws(WalletManagerException::class) + @Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE") + override suspend fun `transactionLockState`(`txId`: TxId) : TransactionLockState { + return uniffiRustCallAsync( + callWithHandle { uniffiHandle -> + UniffiLib.uniffi_cove_fn_method_rustwalletmanager_transaction_lock_state( + uniffiHandle, + FfiConverterTypeTxId.lower(`txId`), + ) + }, + { future, callback, continuation -> UniffiLib.ffi_cove_rust_future_poll_rust_buffer(future, callback, continuation) }, + { future, continuation -> UniffiLib.ffi_cove_rust_future_complete_rust_buffer(future, continuation) }, + { future -> UniffiLib.ffi_cove_rust_future_free_rust_buffer(future) }, + // lift function + { FfiConverterTypeTransactionLockState.lift(it) }, + // Error FFI converter + WalletManagerException.ErrorHandler, + ) + } + override fun `validateMetadata`() = callWithHandle { @@ -47640,6 +47701,42 @@ public object FfiConverterTypeTransactionDetailError : FfiConverterRustBuffer { + override fun read(buf: ByteBuffer) = try { + TransactionLockState.values()[buf.getInt() - 1] + } catch (e: IndexOutOfBoundsException) { + throw RuntimeException("invalid enum value, something is very wrong!!", e) + } + + override fun allocationSize(value: TransactionLockState) = 4UL + + override fun write(value: TransactionLockState, buf: ByteBuffer) { + buf.putInt(value.ordinal + 1) + } +} + + + + + + enum class TransactionState { PENDING, diff --git a/ios/CoveCore/Sources/CoveCore/generated/cove.swift b/ios/CoveCore/Sources/CoveCore/generated/cove.swift index e0019bf64..4d38fd838 100644 --- a/ios/CoveCore/Sources/CoveCore/generated/cove.swift +++ b/ios/CoveCore/Sources/CoveCore/generated/cove.swift @@ -8827,8 +8827,12 @@ public protocol RustWalletManagerProtocol: AnyObject, Sendable { func switchToDifferentWalletAddressType(walletAddressType: WalletAddressType) async throws + func toggleTransactionLock(txId: TxId) async throws + func transactionDetails(txId: TxId) async throws -> TransactionDetails + func transactionLockState(txId: TxId) async throws -> TransactionLockState + func validateMetadata() func walletMetadata() -> WalletMetadata @@ -9684,6 +9688,23 @@ open func switchToDifferentWalletAddressType(walletAddressType: WalletAddressTyp ) } +open func toggleTransactionLock(txId: TxId)async throws { + return + try await uniffiRustCallAsync( + rustFutureFunc: { + uniffi_cove_fn_method_rustwalletmanager_toggle_transaction_lock( + self.uniffiCloneHandle(), + FfiConverterTypeTxId_lower(txId) + ) + }, + pollFunc: ffi_cove_rust_future_poll_void, + completeFunc: ffi_cove_rust_future_complete_void, + freeFunc: ffi_cove_rust_future_free_void, + liftFunc: { $0 }, + errorHandler: FfiConverterTypeWalletManagerError_lift + ) +} + open func transactionDetails(txId: TxId)async throws -> TransactionDetails { return try await uniffiRustCallAsync( @@ -9701,6 +9722,23 @@ open func transactionDetails(txId: TxId)async throws -> TransactionDetails { ) } +open func transactionLockState(txId: TxId)async throws -> TransactionLockState { + return + try await uniffiRustCallAsync( + rustFutureFunc: { + uniffi_cove_fn_method_rustwalletmanager_transaction_lock_state( + self.uniffiCloneHandle(), + FfiConverterTypeTxId_lower(txId) + ) + }, + pollFunc: ffi_cove_rust_future_poll_rust_buffer, + completeFunc: ffi_cove_rust_future_complete_rust_buffer, + freeFunc: ffi_cove_rust_future_free_rust_buffer, + liftFunc: FfiConverterTypeTransactionLockState_lift, + errorHandler: FfiConverterTypeWalletManagerError_lift + ) +} + open func validateMetadata() {try! rustCall() { uniffi_cove_fn_method_rustwalletmanager_validate_metadata( self.uniffiCloneHandle(),$0 @@ -29256,6 +29294,86 @@ public func FfiConverterTypeTransactionDetailError_lower(_ value: TransactionDet +public enum TransactionLockState: Equatable, Hashable { + + case unlocked + case locked + case mixed + case none + + + + + +} + +#if compiler(>=6) +extension TransactionLockState: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeTransactionLockState: FfiConverterRustBuffer { + typealias SwiftType = TransactionLockState + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> TransactionLockState { + let variant: Int32 = try readInt(&buf) + switch variant { + + case 1: return .unlocked + + case 2: return .locked + + case 3: return .mixed + + case 4: return .none + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: TransactionLockState, into buf: inout [UInt8]) { + switch value { + + + case .unlocked: + writeInt(&buf, Int32(1)) + + + case .locked: + writeInt(&buf, Int32(2)) + + + case .mixed: + writeInt(&buf, Int32(3)) + + + case .none: + writeInt(&buf, Int32(4)) + + } + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeTransactionLockState_lift(_ buf: RustBuffer) throws -> TransactionLockState { + return try FfiConverterTypeTransactionLockState.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeTransactionLockState_lower(_ value: TransactionLockState) -> RustBuffer { + return FfiConverterTypeTransactionLockState.lower(value) +} + + + + public enum TransactionState: Equatable, Hashable { case pending @@ -36849,9 +36967,15 @@ private let initializationResult: InitializationResult = { if (uniffi_cove_checksum_method_rustwalletmanager_switch_to_different_wallet_address_type() != 64255) { return InitializationResult.apiChecksumMismatch } + if (uniffi_cove_checksum_method_rustwalletmanager_toggle_transaction_lock() != 65256) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_cove_checksum_method_rustwalletmanager_transaction_details() != 35364) { return InitializationResult.apiChecksumMismatch } + if (uniffi_cove_checksum_method_rustwalletmanager_transaction_lock_state() != 4851) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_cove_checksum_method_rustwalletmanager_validate_metadata() != 36684) { return InitializationResult.apiChecksumMismatch } diff --git a/rust/src/database/wallet_data/label.rs b/rust/src/database/wallet_data/label.rs index a7f3e22dc..1b0bb9643 100644 --- a/rust/src/database/wallet_data/label.rs +++ b/rust/src/database/wallet_data/label.rs @@ -197,6 +197,30 @@ impl LabelsTable { Ok(label) } + pub fn get_output_record( + &self, + outpoint: impl Borrow, + ) -> Result>, Error> { + let outpoint = outpoint.borrow(); + let table = self.read_table(OUTPUT_TABLE)?; + let key = OutPointKey::from(outpoint); + let record = table.get(key)?.map(|record| record.value()); + + Ok(record) + } + + pub fn locked_outpoints(&self) -> Result, Error> { + let table = self.read_table(OUTPUT_TABLE)?; + let mut locked = Vec::new(); + for result in table.iter()? { + let (_, v) = result?; + if !v.value().item.spendable { + locked.push(v.value().item.ref_); + } + } + Ok(locked) + } + pub fn get_address_record( &self, address: impl Borrow>, @@ -323,6 +347,51 @@ impl LabelsTable { Ok(()) } + pub fn set_outputs_spendable( + &self, + outpoints: impl IntoIterator, + spendable: bool, + ) -> Result<(), Error> { + let write_txn = self.db.begin_write().map_err_str(DatabaseError::DatabaseAccess)?; + + { + let mut table = write_txn.open_table(OUTPUT_TABLE)?; + for outpoint in outpoints { + let key = OutPointKey::from(&outpoint); + + let existing = table.get(key.clone())?.map(|r| r.value()); + let mut record = match existing { + Some(r) => r, + None => { + // if setting to spendable and no record exists, it's already spendable by default + if spendable { + continue; + } + Record::new(OutputRecord { ref_: outpoint, label: None, spendable: true }) + } + }; + + if record.item.spendable == spendable { + continue; + } + + if spendable && record.item.label.is_none() { + table.remove(key)?; + continue; + } + + record.item.spendable = spendable; + record.timestamps.updated_at = jiff::Timestamp::now().as_second().cast_unsigned(); + + table.insert(key, record)?; + } + } + + write_txn.commit().map_err_str(DatabaseError::DatabaseAccess)?; + + Ok(()) + } + // MARK: DELETE pub fn delete_labels(&self, labels: impl IntoIterator) -> Result<(), Error> { let write_txn = self.db.begin_write().map_err_str(DatabaseError::DatabaseAccess)?; diff --git a/rust/src/manager/wallet_manager.rs b/rust/src/manager/wallet_manager.rs index 1eea3b7a1..109bf0379 100644 --- a/rust/src/manager/wallet_manager.rs +++ b/rust/src/manager/wallet_manager.rs @@ -35,7 +35,8 @@ use crate::{ tap_card::tap_signer_reader::DeriveInfo, transaction::{ Amount, FeeRate, SentAndReceived, Transaction, TransactionDetails, TransactionDirection, - TxId, Unit, ffi::BitcoinTransaction, unsigned_transaction::UnsignedTransaction, + TransactionLockState, TxId, Unit, ffi::BitcoinTransaction, + unsigned_transaction::UnsignedTransaction, }, wallet::{ Address, AddressInfo, Wallet, WalletAddressType, WalletError, @@ -577,6 +578,25 @@ impl RustWalletManager { Ok(address_info) } + #[uniffi::method] + pub async fn transaction_lock_state( + &self, + tx_id: Arc, + ) -> Result { + let state = call!(self.actor.transaction_lock_state(Arc::unwrap_or_clone(tx_id))) + .await + .map_err_str(Error::UnknownError)??; + Ok(state) + } + + #[uniffi::method] + pub async fn toggle_transaction_lock(&self, tx_id: Arc) -> Result<(), Error> { + call!(self.actor.toggle_transaction_lock(Arc::unwrap_or_clone(tx_id))) + .await + .map_err_str(Error::UnknownError)??; + Ok(()) + } + #[uniffi::method] pub fn save_unsigned_transaction(&self, details: Arc) -> Result<(), Error> { let wallet_id = self.id.clone(); diff --git a/rust/src/manager/wallet_manager/actor.rs b/rust/src/manager/wallet_manager/actor.rs index 5d1f85ecb..c2083e4ef 100644 --- a/rust/src/manager/wallet_manager/actor.rs +++ b/rust/src/manager/wallet_manager/actor.rs @@ -8,7 +8,9 @@ use crate::{ client::{NodeClient, NodeClientOptions}, client_builder::NodeClientBuilder, }, - transaction::{ConfirmedTransaction, FeeRate, Transaction, TransactionDetails, TxId}, + transaction::{ + ConfirmedTransaction, FeeRate, Transaction, TransactionDetails, TransactionLockState, TxId, + }, transaction_watcher::TransactionWatcher, wallet::{ Address, AddressInfo, Wallet, WalletAddressType, balance::Balance, metadata::BlockSizeLast, @@ -203,7 +205,10 @@ impl WalletActor { ) -> Result { debug!("build_ephemeral_drain_tx for fee rate {}", fee.sat_per_vb()); let script_pubkey = address.script_pubkey(); + let locked_outpoints = + self.db.labels.locked_outpoints().map_err_str(Error::BuildTxError)?; let mut tx_builder = self.wallet.bdk.build_tx(); + tx_builder.unspendable(locked_outpoints); tx_builder.drain_wallet().drain_to(script_pubkey).fee_rate(fee.into()); let psbt = tx_builder.finish().map_err_str(Error::BuildTxError)?; @@ -224,8 +229,11 @@ impl WalletActor { let fee_rate = fee_rate.into(); let script_pubkey = address.script_pubkey(); + let locked_outpoints = + self.db.labels.locked_outpoints().map_err_str(Error::BuildTxError)?; let coin_selection = CoveDefaultCoinSelection::new(self.seed); let mut tx_builder = self.wallet.bdk.build_tx().coin_selection(coin_selection); + tx_builder.unspendable(locked_outpoints); tx_builder.ordering(TxOrdering::Untouched); tx_builder.add_recipient(script_pubkey, amount); @@ -323,6 +331,77 @@ impl WalletActor { transactions } + #[into_actor_result] + pub async fn transaction_lock_state( + &mut self, + txid: TxId, + ) -> Result { + let outputs = self.wallet_unspent_outputs_for_tx(txid.0); + let state = self.compute_lock_state(&outputs).map_err_str(Error::UnknownError)?; + Ok(state) + } + + #[into_actor_result] + pub async fn toggle_transaction_lock(&mut self, txid: TxId) -> Result<(), Error> { + let outputs = self.wallet_unspent_outputs_for_tx(txid.0); + + if outputs.is_empty() { + return Ok(()); + } + + let current_state = self.compute_lock_state(&outputs).map_err_str(Error::UnknownError)?; + + // unlocked or mixed -> lock all + // locked -> unlock all + let spendable = matches!(current_state, TransactionLockState::Locked); + + self.db + .labels + .set_outputs_spendable(outputs, spendable) + .map_err_str(Error::UnknownError)?; + + // notify cloud backup that labels changed + crate::manager::cloud_backup_manager::CLOUD_BACKUP_MANAGER + .handle_wallet_backup_change(self.wallet.id.clone()); + + Ok(()) + } + + /// Compute the aggregate lock state for the given wallet-owned unspent outputs. + fn compute_lock_state( + &self, + outputs: &[OutPoint], + ) -> Result { + if outputs.is_empty() { + return Ok(TransactionLockState::None); + } + + let mut locked_count = 0; + for outpoint in outputs { + if self.db.labels.get_output_record(outpoint)?.is_some_and(|r| !r.item.spendable) { + locked_count += 1; + } + } + + if locked_count == 0 { + Ok(TransactionLockState::Unlocked) + } else if locked_count == outputs.len() { + Ok(TransactionLockState::Locked) + } else { + Ok(TransactionLockState::Mixed) + } + } + + /// Returns all wallet-owned, still-unspent outpoints created by the given txid. + fn wallet_unspent_outputs_for_tx(&self, txid: Txid) -> Vec { + self.wallet + .bdk + .list_unspent() + .filter(|utxo| utxo.outpoint.txid == txid) + .map(|utxo| utxo.outpoint) + .collect() + } + pub async fn split_transaction_outputs( &mut self, outputs: Vec, diff --git a/rust/src/transaction.rs b/rust/src/transaction.rs index 2d98109a7..6c3a83a2a 100644 --- a/rust/src/transaction.rs +++ b/rust/src/transaction.rs @@ -24,6 +24,7 @@ pub type TransactionDirection = cove_types::transaction::TransactionDirection; pub type TxId = cove_types::TxId; pub type TransactionDetails = transaction_details::TransactionDetails; +pub type TransactionLockState = transaction_details::TransactionLockState; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, uniffi::Enum)] pub enum TransactionState { diff --git a/rust/src/transaction/transaction_details.rs b/rust/src/transaction/transaction_details.rs index 4ba1ed88d..470f3efee 100644 --- a/rust/src/transaction/transaction_details.rs +++ b/rust/src/transaction/transaction_details.rs @@ -81,6 +81,14 @@ pub enum PendingOrConfirmed { Confirmed(ConfirmedDetails), } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, uniffi::Enum)] +pub enum TransactionLockState { + Unlocked, + Locked, + Mixed, + None, +} + impl TransactionDetails { pub fn try_new( wallet: &BdkWallet,