-
Notifications
You must be signed in to change notification settings - Fork 38
feat: add bulk UTXO lock control to Transaction Details (#661) #693
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 3 commits
df22191
55fcb17
6753544
20f3eb0
4dc6b20
b70f5af
56fa710
02133a4
c8f083d
aedde1c
6223a02
7e734ad
b6344a4
d69e29e
6221734
e5e807f
0edfb79
3043eb7
c9fb012
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -78,10 +78,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 | ||||||||
|
|
@@ -125,11 +128,17 @@ 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) } | ||||||||
|
|
||||||||
| // 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 +346,61 @@ fun TransactionDetailsScreen( | |||||||
| ) | ||||||||
| } | ||||||||
| }, | ||||||||
| actions = { | ||||||||
| if (lockState != TransactionLockState.NONE) { | ||||||||
| IconButton(onClick = { | ||||||||
| scope.launch { | ||||||||
| try { | ||||||||
| <<<<<<< HEAD | ||||||||
| ======= | ||||||||
| val previousState = lockState | ||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
| >>>>>>> df221913f2ae388b340cd7564cbc6c71783652e7 | ||||||||
| 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") | ||||||||
| } | ||||||||
| } | ||||||||
| }) { | ||||||||
| androidx.compose.animation.AnimatedContent( | ||||||||
| targetState = lockState, | ||||||||
| transitionSpec = { | ||||||||
| (fadeIn() + androidx.compose.animation.scaleIn()) | ||||||||
| .togetherWith(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 | ||||||||
| ) | ||||||||
| } | ||||||||
| } | ||||||||
| } | ||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||
| } | ||||||||
| ) | ||||||||
| }, | ||||||||
| snackbarHost = { SnackbarHost(snackbarHostState) }, | ||||||||
|
|
@@ -356,12 +420,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) | ||||||||
| } | ||||||||
|
|
||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -197,6 +197,30 @@ | |
| Ok(label) | ||
| } | ||
|
|
||
| pub fn get_output_record( | ||
| &self, | ||
| outpoint: impl Borrow<bitcoin::OutPoint>, | ||
| ) -> Result<Option<Record<OutputRecord>>, 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<Vec<bitcoin::OutPoint>, Error> { | ||
| let table = self.read_table(OUTPUT_TABLE)?; | ||
| let locked = table | ||
| .iter()? | ||
| .filter_map(|r| r.ok()) | ||
| .map(|(_, v)| v.value()) | ||
| .filter(|r| !r.item.spendable) | ||
| .map(|r| r.item.ref_) | ||
| .collect(); | ||
| Ok(locked) | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
|
|
||
| pub fn get_address_record( | ||
| &self, | ||
| address: impl Borrow<Address<NetworkUnchecked>>, | ||
|
|
@@ -323,6 +347,60 @@ | |
| Ok(()) | ||
| } | ||
|
|
||
| pub fn set_outputs_spendable( | ||
| &self, | ||
| outpoints: impl IntoIterator<Item = bitcoin::OutPoint>, | ||
| 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); | ||
|
|
||
| <<<<<<< HEAD | ||
| 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 { | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| continue; | ||
| } | ||
| Record::new(OutputRecord { | ||
| ref_: outpoint, | ||
| label: None, | ||
| spendable: true, | ||
| }) | ||
| } | ||
| }; | ||
|
|
||
| if record.item.spendable == spendable { | ||
| continue; | ||
| } | ||
| ======= | ||
| let mut record = table.get(key.clone())?.map(|r| r.value()).unwrap_or_else(|| { | ||
| Record::new(OutputRecord { | ||
| ref_: outpoint, | ||
| label: None, | ||
| spendable: true, | ||
| }) | ||
| }); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When let existing = table.get(key.clone())?.map(|r| r.value());
let mut record = match existing {
Some(r) => r,
None => {
if spendable { continue; }
Record::new(OutputRecord { ref_: outpoint, label: None, spendable: true })
}
}; |
||
| >>>>>>> df221913f2ae388b340cd7564cbc6c71783652e7 | ||
|
|
||
| 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(()) | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| // MARK: DELETE | ||
| pub fn delete_labels(&self, labels: impl IntoIterator<Item = Label>) -> Result<(), Error> { | ||
| let write_txn = self.db.begin_write().map_err_str(DatabaseError::DatabaseAccess)?; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
conflicts