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