From 17b44e5bc1d236a37f23bfb0e163b78b7ec3fb8d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Nov 2025 19:01:36 +0000 Subject: [PATCH 1/2] feat: add boosted txids tracking and display parent txids in activity explore --- .../bitkit/data/dto/PendingBoostActivity.kt | 3 +- .../to/bitkit/repositories/ActivityRepo.kt | 9 +++-- .../wallets/activity/ActivityExploreScreen.kt | 8 +--- .../ui/sheets/BoostTransactionViewModel.kt | 38 +++++++++++++------ 4 files changed, 37 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/to/bitkit/data/dto/PendingBoostActivity.kt b/app/src/main/java/to/bitkit/data/dto/PendingBoostActivity.kt index bec9e9c60..622bac53b 100644 --- a/app/src/main/java/to/bitkit/data/dto/PendingBoostActivity.kt +++ b/app/src/main/java/to/bitkit/data/dto/PendingBoostActivity.kt @@ -6,5 +6,6 @@ import kotlinx.serialization.Serializable data class PendingBoostActivity( val txId: String, val updatedAt: ULong, - val activityToDelete: String? + val activityToDelete: String?, + val boostTxIds: List = emptyList(), ) diff --git a/app/src/main/java/to/bitkit/repositories/ActivityRepo.kt b/app/src/main/java/to/bitkit/repositories/ActivityRepo.kt index 0d476281c..9cf0a3008 100644 --- a/app/src/main/java/to/bitkit/repositories/ActivityRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/ActivityRepo.kt @@ -428,22 +428,25 @@ class ActivityRepo @Inject constructor( val updatedActivity = Activity.Onchain( v1 = newOnChainActivity.v1.copy( isBoosted = true, - updatedAt = pendingBoostActivity.updatedAt + boostTxIds = pendingBoostActivity.boostTxIds, + updatedAt = pendingBoostActivity.updatedAt, ) ) if (pendingBoostActivity.activityToDelete != null) { + // RBF: Replace old activity with new one, applying parent chain boostTxIds replaceActivity( id = updatedActivity.v1.id, activity = updatedActivity, - activityIdToDelete = pendingBoostActivity.activityToDelete + activityIdToDelete = pendingBoostActivity.activityToDelete, ).onSuccess { cacheStore.removeActivityFromPendingBoost(pendingBoostActivity) } } else { + // CPFP: Update existing activity (though CPFP is handled immediately now) updateActivity( id = updatedActivity.v1.id, - activity = updatedActivity + activity = updatedActivity, ).onSuccess { cacheStore.removeActivityFromPendingBoost(pendingBoostActivity) } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt index d6a611ea5..4594d7431 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt @@ -271,13 +271,9 @@ private fun ColumnScope.OnchainDetails( .size(16.dp) .align(Alignment.CenterHorizontally) ) - } // TODO use real boosted parents from bitkit-core/ldk-node when available - val boostedParents = listOfNotNull( - "todo_first_parent_txid".takeIf { onchain.isBoosted() && !onchain.v1.confirmed }, - "todo_second_parent_txid".takeIf { onchain.isBoosted() && onchain.v1.confirmed }, - ) + } - boostedParents.forEachIndexed { index, parent -> + onchain.v1.boostTxIds.forEachIndexed { index, parent -> val isRbf = onchain.boostType() == BoostType.RBF Section( title = stringResource( diff --git a/app/src/main/java/to/bitkit/ui/sheets/BoostTransactionViewModel.kt b/app/src/main/java/to/bitkit/ui/sheets/BoostTransactionViewModel.kt index 66debe45a..663d4843e 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/BoostTransactionViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/BoostTransactionViewModel.kt @@ -17,6 +17,7 @@ import org.lightningdevkit.ldknode.Txid import to.bitkit.data.dto.PendingBoostActivity import to.bitkit.ext.BoostType import to.bitkit.ext.boostType +import to.bitkit.ext.nowMillis import to.bitkit.ext.nowTimestamp import to.bitkit.models.TransactionSpeed import to.bitkit.repositories.ActivityRepo @@ -203,6 +204,8 @@ class BoostTransactionViewModel @Inject constructor( destinationAddress = walletRepo.getOnchainAddress(), ).fold( onSuccess = { newTxId -> + // For CPFP, immediately update parent with child txId + Logger.debug("CPFP successful. Appending child txId $newTxId to parent's boostTxIds", context = TAG) handleBoostSuccess(newTxId, isRBF = false) }, onFailure = { error -> @@ -283,7 +286,7 @@ class BoostTransactionViewModel @Inject constructor( /** * Updates activity based on boost type: * - RBF: Updates current activity with boost data, then replaces with new transaction - * - CPFP: Simply updates the current activity + * - CPFP: Updates parent activity by appending child txId to boostTxIds */ private suspend fun updateActivity(newTxId: Txid, isRBF: Boolean): Result { Logger.debug("Updating activity for txId: $newTxId. isRBF: $isRBF", context = TAG) @@ -294,24 +297,25 @@ class BoostTransactionViewModel @Inject constructor( return if (isRBF) { handleRBFUpdate(newTxId, currentActivity) } else { - handleCPFPUpdate(currentActivity) + handleCPFPUpdate(currentActivity, newTxId) } } /** - * Handles CPFP (Child Pays For Parent) update by simply updating the current activity + * Handles CPFP (Child Pays For Parent) update by appending child txId to parent's boostTxIds */ - private suspend fun handleCPFPUpdate(currentActivity: OnchainActivity): Result { + private suspend fun handleCPFPUpdate(currentActivity: OnchainActivity, childTxId: Txid): Result { val updatedActivity = Activity.Onchain( v1 = currentActivity.copy( isBoosted = true, - updatedAt = nowTimestamp().toEpochMilli().toULong() + boostTxIds = currentActivity.boostTxIds + childTxId, + updatedAt = nowMillis().toULong(), ) ) return activityRepo.updateActivity( id = updatedActivity.v1.id, - activity = updatedActivity + activity = updatedActivity, ) } @@ -328,13 +332,13 @@ class BoostTransactionViewModel @Inject constructor( isBoosted = true, feeRate = _uiState.value.feeRate, fee = _uiState.value.totalFeeSats, - updatedAt = nowTimestamp().toEpochMilli().toULong() + updatedAt = nowMillis().toULong(), ) ) activityRepo.updateActivity( id = updatedCurrentActivity.v1.id, - activity = updatedCurrentActivity + activity = updatedCurrentActivity, ) // Then find and replace with the new activity @@ -379,7 +383,7 @@ class BoostTransactionViewModel @Inject constructor( v1 = newOnChainActivity.v1.copy( isBoosted = true, feeRate = _uiState.value.feeRate, - updatedAt = nowTimestamp().toEpochMilli().toULong() + updatedAt = nowMillis().toULong(), ) ) @@ -412,13 +416,25 @@ class BoostTransactionViewModel @Inject constructor( /** * Caches activity data for pending boost operation + * - For RBF: stores parent chain (existing boostTxIds + current txId) + * - For CPFP: stores empty list (child tx is added to parent's boostTxIds during sync) */ private suspend fun cachePendingBoostActivity(newTxId: Txid, activityToDelete: String?) { + val currentActivity = activity?.v1 + val boostTxIds = if (activityToDelete != null && currentActivity != null) { + // RBF: Track full parent chain (existing boostTxIds + current txId being replaced) + currentActivity.boostTxIds + currentActivity.txId + } else { + // CPFP: No parent tracking needed + emptyList() + } + activityRepo.addActivityToPendingBoost( PendingBoostActivity( txId = newTxId, - updatedAt = nowTimestamp().toEpochMilli().toULong(), - activityToDelete = activityToDelete + updatedAt = nowMillis().toULong(), + activityToDelete = activityToDelete, + boostTxIds = boostTxIds, ) ) } From cb4a77ac265c2954d4486cc3b87fa86a3f490cb7 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 7 Nov 2025 15:35:57 +0100 Subject: [PATCH 2/2] feat: copy boosted parent txid on click --- .../wallets/activity/ActivityExploreScreen.kt | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt index 4594d7431..ca3ed2536 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt @@ -116,13 +116,6 @@ fun ActivityExploreScreen( val intent = Intent(Intent.ACTION_VIEW, url.toUri()) context.startActivity(intent) }, - onClickParent = { id -> - app.toast( - type = Toast.ToastType.WARNING, - title = "TODO", - description = "Navigate to Activity Detail for: $id", - ) - }, ) } } @@ -133,7 +126,6 @@ private fun ActivityExploreContent( txDetails: TxDetails? = null, onCopy: (String) -> Unit = {}, onClickExplore: (String) -> Unit = {}, - onClickParent: (String) -> Unit = {}, ) { Column( modifier = Modifier @@ -164,7 +156,6 @@ private fun ActivityExploreContent( onchain = item, onCopy = onCopy, txDetails = txDetails, - onClickParent = onClickParent, ) Spacer(modifier = Modifier.weight(1f)) PrimaryButton( @@ -226,7 +217,6 @@ private fun ColumnScope.OnchainDetails( onchain: Activity.Onchain, onCopy: (String) -> Unit, txDetails: TxDetails?, - onClickParent: (String) -> Unit, ) { val txId = onchain.v1.txId Section( @@ -285,9 +275,11 @@ private fun ColumnScope.OnchainDetails( } }, modifier = Modifier - .clickableAlpha { - onClickParent(parent) - } + .clickableAlpha( + onClick = copyToClipboard(parent) { + onCopy(it) + } + ) .testTag(if (isRbf) "RBFBoosted" else "CPFPBoosted") ) }