Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
df22191
feat: add bulk UTXO lock control to Transaction Details (#661)
gitsofaryan Apr 19, 2026
55fcb17
feat: add bulk UTXO lock control to Transaction Details (#661)
gitsofaryan Apr 19, 2026
6753544
Merge branch 'bulk-utxo-lock-661' of https://github.com/gitsofaryan/c…
gitsofaryan Apr 19, 2026
20f3eb0
feat: implement transaction details screen and add wallet label manag…
gitsofaryan Apr 23, 2026
4dc6b20
feat: implement WalletActor for BDK wallet management and add label d…
gitsofaryan Apr 23, 2026
b70f5af
fix: address CodeRabbit review feedback
gitsofaryan Apr 23, 2026
56fa710
fix: resolve TxBuilder compile error and rustfmt
gitsofaryan Apr 23, 2026
02133a4
fix: revert TxBuilder chaining that caused borrow checker errors
gitsofaryan Apr 23, 2026
c8f083d
fix: use is_some_and instead of map_or for clippy
gitsofaryan Apr 23, 2026
aedde1c
fix: propagate locked_outpoints errors and use map_err_str convention
gitsofaryan Apr 23, 2026
6223a02
fix: use BuildTxError for locked_outpoints in tx build paths
gitsofaryan Apr 23, 2026
7e734ad
Merge remote-tracking branch 'upstream/master' into bulk-utxo-lock-661
gitsofaryan Apr 23, 2026
b6344a4
Merge remote-tracking branch 'upstream/master' into bulk-utxo-lock-661
gitsofaryan Apr 24, 2026
d69e29e
chore: regenerate Android (Kotlin) UniFFI bindings
github-actions[bot] Apr 24, 2026
6221734
chore: regenerate iOS (Swift) UniFFI bindings
github-actions[bot] Apr 24, 2026
e5e807f
ci: trigger CI after binding regeneration
gitsofaryan Apr 24, 2026
0edfb79
fix: use compatible .with() syntax for Compose animations
gitsofaryan Apr 24, 2026
3043eb7
fix: opt-in to ExperimentalAnimationApi for AnimatedContent
gitsofaryan Apr 24, 2026
c9fb012
ci: re-trigger checks for ktlint
gitsofaryan Apr 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
)
}
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
)
},
snackbarHost = { SnackbarHost(snackbarHostState) },
Expand All @@ -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)
}
Expand Down
97 changes: 97 additions & 0 deletions android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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")
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -47640,6 +47701,42 @@ public object FfiConverterTypeTransactionDetailError : FfiConverterRustBuffer<Tr



enum class TransactionLockState {

UNLOCKED,
LOCKED,
MIXED,
NONE;




companion object
}


/**
* @suppress
*/
public object FfiConverterTypeTransactionLockState: FfiConverterRustBuffer<TransactionLockState> {
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,
Expand Down
Loading
Loading