feat: activity boosted tx parents - #460

Closed
ovitrif wants to merge 2 commits into
feat/backup-polishfrom
feat/boosted-tx-parents
Closed

feat: activity boosted tx parents#460
ovitrif wants to merge 2 commits into
feat/backup-polishfrom
feat/boosted-tx-parents

Conversation

@ovitrif

@ovitrifovitrif commented Nov 6, 2025

Copy link
Copy Markdown
Collaborator

ROADMAP: Transaction Details

This PR adds boosted txids tracking and displays parent transaction IDs in the activity details, matching iOS PR #199.

Description

  • Added boostTxIds to PendingBoostActivity to store parent transaction IDs
  • CPFP: Append child transaction ID to parent's boostTxIds when boosting
  • RBF:
    • Track parent chain (existing boostTxIds + current txId) in PendingBoostActivity.
    • Store parent txIds when creating replacement
    • retrieve and apply when replacement syncs
  • UI: Display boosted transaction IDs in ActivityExploreScreen

Preview

CPFP (received)RBF (sent)
TODOTODO

QA Notes

1️⃣ RBF Test

  1. Send onchain
  2. Boost transaction
  3. Activity Item > Explore
  4. Expect list containing txid of previously boosted tx under "Boosted Transaction…" header

1️⃣ CPFP Test

Same but start with RECEIVE onchain.

@ovitrif
ovitrif requested a review from CopilotNovember 6, 2025 23:57
Comment threadapp/src/main/java/to/bitkit/repositories/ActivityRepo.kt Fixed
Comment threadapp/src/main/java/to/bitkit/repositories/ActivityRepo.kt Fixed
Comment threadapp/src/main/java/to/bitkit/repositories/ActivityRepo.kt Fixed
Comment threadapp/src/main/java/to/bitkit/repositories/ActivityRepo.kt Fixed
Comment threadapp/src/main/java/to/bitkit/repositories/ActivityRepo.kt Fixed

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR enhances boost transaction tracking by properly recording boost transaction IDs for both RBF (Replace-By-Fee) and CPFP (Child Pays For Parent) scenarios. The implementation ensures that parent-child transaction relationships are maintained and properly displayed in the activity detail screen.

  • Added boostTxIds field to PendingBoostActivity data class
  • Updated boost logic to track and persist transaction IDs through the boost process
  • Added UI component to display boosted transaction IDs in activity details

Reviewed Changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

FileDescription
BoostTransactionViewModel.ktUpdated CPFP and cachePendingBoostActivity logic to properly track boost transaction IDs
ActivityDetailScreen.ktAdded UI section to display boosted transaction IDs with appropriate labels for RBF and CPFP
ActivityRepo.ktUpdated pending boost activities handling to apply boostTxIds during sync
PendingBoostActivity.ktAdded boostTxIds field with default empty list for backward compatibility

@ovitrif
ovitrifforce-pushed the feat/boosted-tx-parents branch from a96c04d to 17b44e5CompareNovember 7, 2025 13:20
@claude

claudeBot commented Nov 7, 2025

Copy link
Copy Markdown
Contributor

PR Review: Activity Boosted TX Parents

Summary

This PR adds tracking and display of parent transaction IDs for boosted transactions (both CPFP and RBF), aligning with iOS implementation. Overall, the implementation is solid with good documentation and careful handling of edge cases. Below are my findings across code quality, potential issues, performance, security, and test coverage.


✅ Strengths

  1. Excellent Documentation: The code includes comprehensive comments explaining RBF vs CPFP behavior, particularly in BoostTransactionViewModel.kt:419-430 and ActivityRepo.kt:437-447.

  2. Proper State Management: The use of PendingBoostActivity to cache boost data for retry on sync failure is a robust pattern.

  3. Good Separation of Concerns: Clear distinction between immediate CPFP updates and deferred RBF replacements.

  4. Consistent Code Style: Follows Kotlin best practices with trailing commas, proper nullability handling, and immutable data structures.


🔴 Critical Issues

1. Race Condition in BackupRepo (High Priority)

Location: BackupRepo.kt:256-274

backupJobs[category] = scope.launch {
// Set running immediately to prevent UI showing failure during debounce
cacheStore.updateBackupStatus(category) {
it.copy(running =true)
}
delay(BACKUP_DEBOUNCE)
// Double-check if backup is still neededval status = cacheStore.backupStatuses.first()[category] ?:BackupItemStatus()
if (status.isRequired &&!isRestoring) {
triggerBackup(category)
} else {
// Backup no longer needed, reset running flag
cacheStore.updateBackupStatus(category) {
it.copy(running =false)
}
}
}

Issue: Not thread-safe. Multiple calls to scheduleBackup() for the same category could overwrite backupJobs[category] without canceling the previous job, leading to:

  • Multiple concurrent backups for the same category
  • Orphaned coroutines that never reset running = false
  • UI showing incorrect backup status

Recommendation:

backupJobs[category]?.cancel() // Cancel existing job
backupJobs[category] = scope.launch {
// ... rest of implementation
}

2. Synchronization Issue in VssBackupClient.reset()

Location: VssBackupClient.kt:65-72

funreset() {
synchronized(this) {
isSetup.cancel()
isSetup =CompletableDeferred()
}
vssStoreIdProvider.clearCache()
Logger.debug("VSS client reset", context =TAG)
}

Issue: vssStoreIdProvider.clearCache() is called outside the synchronized block, which could lead to:

  • Cache being cleared while setup() is reading from it
  • Inconsistent state between isSetup and cache

Recommendation: Move vssStoreIdProvider.clearCache() inside the synchronized block.


🟡 Medium Priority Issues

3. Missing Null Safety Check

Location: BoostTransactionViewModel.kt:422-430

val currentActivity = activity?.v1
val boostTxIds =if (activityToDelete !=null&& currentActivity !=null) {
currentActivity.boostTxIds + currentActivity.txId
} else {
emptyList()
}

Issue: activityToDelete != null is checked but currentActivity could still be null for RBF scenarios, leading to an empty boostTxIds list when it should contain the parent chain.

Recommendation: Add explicit error handling or logging when activityToDelete is non-null but currentActivity is null, as this indicates a bug.

4. Backup Order Changed Without Migration

Location: BackupRepo.kt:402-458

The restore order was changed from:

// Old orderSETTINGSWALLETACTIVITYMETADATABLOCKTANK

to:

// New order METADATASETTINGSWALLETACTIVITYBLOCKTANK

Issue: METADATA is restored first to populate caches early via onCacheRestored() callback, but:

  • No migration logic handles cases where users have existing backups in the old format
  • If METADATA restore fails, subsequent restores may operate with stale cache data
  • The callback onCacheRestored() is not used in this PR

Recommendation:

  • Document why METADATA must be restored first
  • Add error handling if METADATA restore fails (should it abort the entire restore?)
  • Consider if partial restore states are acceptable

5. Potential Memory Leak in WalletRepo

Location: WalletRepo.kt:240-260

suspendfunwipeWallet(walletIndex:Int = 0): Result<Unit> = withContext(bgDispatcher) {
try {
backupRepo.reset() // Cancels all coroutines in BackupRepo_walletState.update { WalletState() }
_balanceState.update { BalanceState() }
keychain.wipe()
// ... more cleanup
}
}

Issue: backupRepo.reset() is called before state is cleared, but if any backup observer coroutines are in the middle of processing state changes, they may attempt to read from already-wiped datastores.

Recommendation: Consider the order of operations - perhaps reset state first, then cancel observers.


🟢 Minor Issues / Suggestions

6. Inconsistent Use of nowMillis() vs nowTimestamp()

The PR introduces nowMillis() but both functions are used inconsistently:

  • BoostTransactionViewModel.kt: Uses nowMillis().toULong()
  • ActivityRepo.kt:316: Still uses nowTimestamp().toEpochMilli().toULong()

Recommendation: Standardize on one approach for consistency.

7. Annotation Retention Changes

Location: DispatchersModule.kt:11-20

@Qualifier
-@Retention(AnnotationRetention.BINARY)
annotationclassUiDispatcher

Issue: Removing @Retention(AnnotationRetention.BINARY) changes the default to RUNTIME, which:

  • Increases APK size slightly (annotations retained in bytecode)
  • Allows runtime reflection (usually not needed for Dagger qualifiers)

Recommendation: Clarify if this change is intentional or if it should use BINARY retention.

8. BackupItemStatus.isRequired Property

Location: BackupCategory.kt:63

data classBackupItemStatus(
valrunning:Boolean = false,
valsynced:Long = 0,
valrequired:Long = 0,
) {
val isRequired:Boolean get() = synced < required
}

Issue: The computed property name isRequired is slightly misleading. It actually means "backup is outdated/stale," not "backup is required by user."

Recommendation: Consider renaming to isStale or needsSync for clarity.

9. Health Check Comparison Changed

Location: HealthRepo.kt:129

-funisSyncOk(synced:Long, required:Long) = synced > required ||...
+funisSyncOk(synced:Long, required:Long) = synced >= required ||...

Issue: Changing from > to >= means synced == required is now considered "OK", but this is technically still a pending backup (hasn't happened yet).

Recommendation: Verify this is the intended behavior. The 5-minute grace period should handle this, but the semantic change is subtle.


🔒 Security Considerations

10. No Sensitive Data Concerns

Status: ✅ Good

  • Transaction IDs are public blockchain data
  • No credentials or private keys exposed in new code
  • VSS backup client properly handles encryption (outside scope of this PR)

🧪 Test Coverage

Issues with Test Coverage:

  1. No Tests for New Functionality: The only test change is adding the backupRepo mock to WalletRepoTest.kt:51-82, but there are:

    • No tests for boostTxIds tracking logic
    • No tests for the CPFP child appending behavior
    • No tests for the RBF parent chain tracking
    • No tests for BackupRepo.reset() or scheduleFullBackup()
  2. Missing Edge Case Tests:

    • What happens if boostTxIds list grows very large (multiple RBF chains)?
    • What if a transaction is boosted multiple times in succession?
    • What if the activity is not found when applying pending boost?
  3. UI Tests: No mention of UI tests for the new "Boosted Transaction" section in ActivityExploreScreen.kt:266-281.

Recommendation: Add unit tests for:

// BoostTransactionViewModel
@Test
fun`CPFP should append child txId to parent boostTxIds`()
@Test fun`RBF should track full parent chain in boostTxIds`()
@Test
fun`cachePendingBoostActivity should store parent chain for RBF`()
// ActivityRepo
@Test
fun`boostPendingActivities should apply parent chain to RBF transactions`()
// BackupRepo
@Test
fun`scheduleBackup should cancel existing job for same category`()

📊 Performance Considerations

11. Potential N+1 Query in UI

Location: ActivityExploreScreen.kt:266-281

onchain.v1.boostTxIds.forEachIndexed { index, parent ->Section(
title = stringResource(...),
// ... rendering
)
}

Issue: Each boostTxIds item renders a full Section. If a transaction has been boosted many times, this could create a very long list.

Recommendation: Consider limiting display to last N transactions with a "Show more" option, or use a lazy loading approach.

12. Backup Debounce is Acceptable

Status: ✅ Good

  • 5-second debounce (BACKUP_DEBOUNCE = 5000L) prevents excessive backup calls
  • Backup check interval of 1 minute is reasonable

🎯 Recommendations Summary

Must Fix Before Merge:

  1. ✅ Fix race condition in BackupRepo.scheduleBackup() by canceling existing jobs
  2. ✅ Move vssStoreIdProvider.clearCache() inside synchronized block in VssBackupClient.reset()

Should Fix Before Merge:

  1. ⚠️ Add null safety check in BoostTransactionViewModel.cachePendingBoostActivity()
  2. ⚠️ Document backup restore order changes and add error handling
  3. ⚠️ Add unit tests for new boostTxIds functionality

Nice to Have:

  1. 💡 Standardize on nowMillis() vs nowTimestamp()
  2. 💡 Clarify annotation retention changes in DispatchersModule
  3. 💡 Rename isRequired to isStale for clarity
  4. 💡 Add UI tests for new explore screen section
  5. 💡 Consider limiting displayed boost chain length

✅ Conclusion

This is a well-structured PR with clear intent and good documentation. The core functionality for tracking boosted transaction parents is sound. However, there are 2 critical thread-safety issues that must be addressed before merging, and test coverage is insufficient for the new functionality.

Once the critical issues are resolved and tests are added, this PR will be ready to merge.

Recommendation: Request changes for issues #1, #2, #3, #4, and #5.

@ovitrifovitrif mentioned this pull request Nov 18, 2025
@ovitrif
ovitrif changed the base branch from master to feat/backup-polishNovember 18, 2025 00:35
@ovitrif

Copy link
Copy Markdown
CollaboratorAuthor

Closed in favour of:

@ovitrifovitrif closed this Nov 18, 2025
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@ovitrif@github-advanced-security@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat: activity boosted tx parents - #460

Closed
ovitrif wants to merge 2 commits into
feat/backup-polishfrom
feat/boosted-tx-parents
Closed

feat: activity boosted tx parents#460
ovitrif wants to merge 2 commits into
feat/backup-polishfrom
feat/boosted-tx-parents

Conversation

@ovitrif

@ovitrifovitrif commented Nov 6, 2025

Copy link
Copy Markdown
Collaborator

ROADMAP: Transaction Details

This PR adds boosted txids tracking and displays parent transaction IDs in the activity details, matching iOS PR #199.

Description

  • Added boostTxIds to PendingBoostActivity to store parent transaction IDs
  • CPFP: Append child transaction ID to parent's boostTxIds when boosting
  • RBF:
    • Track parent chain (existing boostTxIds + current txId) in PendingBoostActivity.
    • Store parent txIds when creating replacement
    • retrieve and apply when replacement syncs
  • UI: Display boosted transaction IDs in ActivityExploreScreen

Preview

CPFP (received)RBF (sent)
TODOTODO

QA Notes

1️⃣ RBF Test

  1. Send onchain
  2. Boost transaction
  3. Activity Item > Explore
  4. Expect list containing txid of previously boosted tx under "Boosted Transaction…" header

1️⃣ CPFP Test

Same but start with RECEIVE onchain.

@ovitrif
ovitrif requested a review from CopilotNovember 6, 2025 23:57
Comment threadapp/src/main/java/to/bitkit/repositories/ActivityRepo.kt Fixed
Comment threadapp/src/main/java/to/bitkit/repositories/ActivityRepo.kt Fixed
Comment threadapp/src/main/java/to/bitkit/repositories/ActivityRepo.kt Fixed
Comment threadapp/src/main/java/to/bitkit/repositories/ActivityRepo.kt Fixed
Comment threadapp/src/main/java/to/bitkit/repositories/ActivityRepo.kt Fixed

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR enhances boost transaction tracking by properly recording boost transaction IDs for both RBF (Replace-By-Fee) and CPFP (Child Pays For Parent) scenarios. The implementation ensures that parent-child transaction relationships are maintained and properly displayed in the activity detail screen.

  • Added boostTxIds field to PendingBoostActivity data class
  • Updated boost logic to track and persist transaction IDs through the boost process
  • Added UI component to display boosted transaction IDs in activity details

Reviewed Changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

FileDescription
BoostTransactionViewModel.ktUpdated CPFP and cachePendingBoostActivity logic to properly track boost transaction IDs
ActivityDetailScreen.ktAdded UI section to display boosted transaction IDs with appropriate labels for RBF and CPFP
ActivityRepo.ktUpdated pending boost activities handling to apply boostTxIds during sync
PendingBoostActivity.ktAdded boostTxIds field with default empty list for backward compatibility

@ovitrif
ovitrifforce-pushed the feat/boosted-tx-parents branch from a96c04d to 17b44e5CompareNovember 7, 2025 13:20
@claude

claudeBot commented Nov 7, 2025

Copy link
Copy Markdown
Contributor

PR Review: Activity Boosted TX Parents

Summary

This PR adds tracking and display of parent transaction IDs for boosted transactions (both CPFP and RBF), aligning with iOS implementation. Overall, the implementation is solid with good documentation and careful handling of edge cases. Below are my findings across code quality, potential issues, performance, security, and test coverage.


✅ Strengths

  1. Excellent Documentation: The code includes comprehensive comments explaining RBF vs CPFP behavior, particularly in BoostTransactionViewModel.kt:419-430 and ActivityRepo.kt:437-447.

  2. Proper State Management: The use of PendingBoostActivity to cache boost data for retry on sync failure is a robust pattern.

  3. Good Separation of Concerns: Clear distinction between immediate CPFP updates and deferred RBF replacements.

  4. Consistent Code Style: Follows Kotlin best practices with trailing commas, proper nullability handling, and immutable data structures.


🔴 Critical Issues

1. Race Condition in BackupRepo (High Priority)

Location: BackupRepo.kt:256-274

backupJobs[category] = scope.launch {
// Set running immediately to prevent UI showing failure during debounce
cacheStore.updateBackupStatus(category) {
it.copy(running =true)
}
delay(BACKUP_DEBOUNCE)
// Double-check if backup is still neededval status = cacheStore.backupStatuses.first()[category] ?:BackupItemStatus()
if (status.isRequired &&!isRestoring) {
triggerBackup(category)
} else {
// Backup no longer needed, reset running flag
cacheStore.updateBackupStatus(category) {
it.copy(running =false)
}
}
}

Issue: Not thread-safe. Multiple calls to scheduleBackup() for the same category could overwrite backupJobs[category] without canceling the previous job, leading to:

  • Multiple concurrent backups for the same category
  • Orphaned coroutines that never reset running = false
  • UI showing incorrect backup status

Recommendation:

backupJobs[category]?.cancel() // Cancel existing job
backupJobs[category] = scope.launch {
// ... rest of implementation
}

2. Synchronization Issue in VssBackupClient.reset()

Location: VssBackupClient.kt:65-72

funreset() {
synchronized(this) {
isSetup.cancel()
isSetup =CompletableDeferred()
}
vssStoreIdProvider.clearCache()
Logger.debug("VSS client reset", context =TAG)
}

Issue: vssStoreIdProvider.clearCache() is called outside the synchronized block, which could lead to:

  • Cache being cleared while setup() is reading from it
  • Inconsistent state between isSetup and cache

Recommendation: Move vssStoreIdProvider.clearCache() inside the synchronized block.


🟡 Medium Priority Issues

3. Missing Null Safety Check

Location: BoostTransactionViewModel.kt:422-430

val currentActivity = activity?.v1
val boostTxIds =if (activityToDelete !=null&& currentActivity !=null) {
currentActivity.boostTxIds + currentActivity.txId
} else {
emptyList()
}

Issue: activityToDelete != null is checked but currentActivity could still be null for RBF scenarios, leading to an empty boostTxIds list when it should contain the parent chain.

Recommendation: Add explicit error handling or logging when activityToDelete is non-null but currentActivity is null, as this indicates a bug.

4. Backup Order Changed Without Migration

Location: BackupRepo.kt:402-458

The restore order was changed from:

// Old orderSETTINGSWALLETACTIVITYMETADATABLOCKTANK

to:

// New order METADATASETTINGSWALLETACTIVITYBLOCKTANK

Issue: METADATA is restored first to populate caches early via onCacheRestored() callback, but:

  • No migration logic handles cases where users have existing backups in the old format
  • If METADATA restore fails, subsequent restores may operate with stale cache data
  • The callback onCacheRestored() is not used in this PR

Recommendation:

  • Document why METADATA must be restored first
  • Add error handling if METADATA restore fails (should it abort the entire restore?)
  • Consider if partial restore states are acceptable

5. Potential Memory Leak in WalletRepo

Location: WalletRepo.kt:240-260

suspendfunwipeWallet(walletIndex:Int = 0): Result<Unit> = withContext(bgDispatcher) {
try {
backupRepo.reset() // Cancels all coroutines in BackupRepo_walletState.update { WalletState() }
_balanceState.update { BalanceState() }
keychain.wipe()
// ... more cleanup
}
}

Issue: backupRepo.reset() is called before state is cleared, but if any backup observer coroutines are in the middle of processing state changes, they may attempt to read from already-wiped datastores.

Recommendation: Consider the order of operations - perhaps reset state first, then cancel observers.


🟢 Minor Issues / Suggestions

6. Inconsistent Use of nowMillis() vs nowTimestamp()

The PR introduces nowMillis() but both functions are used inconsistently:

  • BoostTransactionViewModel.kt: Uses nowMillis().toULong()
  • ActivityRepo.kt:316: Still uses nowTimestamp().toEpochMilli().toULong()

Recommendation: Standardize on one approach for consistency.

7. Annotation Retention Changes

Location: DispatchersModule.kt:11-20

@Qualifier
-@Retention(AnnotationRetention.BINARY)
annotationclassUiDispatcher

Issue: Removing @Retention(AnnotationRetention.BINARY) changes the default to RUNTIME, which:

  • Increases APK size slightly (annotations retained in bytecode)
  • Allows runtime reflection (usually not needed for Dagger qualifiers)

Recommendation: Clarify if this change is intentional or if it should use BINARY retention.

8. BackupItemStatus.isRequired Property

Location: BackupCategory.kt:63

data classBackupItemStatus(
valrunning:Boolean = false,
valsynced:Long = 0,
valrequired:Long = 0,
) {
val isRequired:Boolean get() = synced < required
}

Issue: The computed property name isRequired is slightly misleading. It actually means "backup is outdated/stale," not "backup is required by user."

Recommendation: Consider renaming to isStale or needsSync for clarity.

9. Health Check Comparison Changed

Location: HealthRepo.kt:129

-funisSyncOk(synced:Long, required:Long) = synced > required ||...
+funisSyncOk(synced:Long, required:Long) = synced >= required ||...

Issue: Changing from > to >= means synced == required is now considered "OK", but this is technically still a pending backup (hasn't happened yet).

Recommendation: Verify this is the intended behavior. The 5-minute grace period should handle this, but the semantic change is subtle.


🔒 Security Considerations

10. No Sensitive Data Concerns

Status: ✅ Good

  • Transaction IDs are public blockchain data
  • No credentials or private keys exposed in new code
  • VSS backup client properly handles encryption (outside scope of this PR)

🧪 Test Coverage

Issues with Test Coverage:

  1. No Tests for New Functionality: The only test change is adding the backupRepo mock to WalletRepoTest.kt:51-82, but there are:

    • No tests for boostTxIds tracking logic
    • No tests for the CPFP child appending behavior
    • No tests for the RBF parent chain tracking
    • No tests for BackupRepo.reset() or scheduleFullBackup()
  2. Missing Edge Case Tests:

    • What happens if boostTxIds list grows very large (multiple RBF chains)?
    • What if a transaction is boosted multiple times in succession?
    • What if the activity is not found when applying pending boost?
  3. UI Tests: No mention of UI tests for the new "Boosted Transaction" section in ActivityExploreScreen.kt:266-281.

Recommendation: Add unit tests for:

// BoostTransactionViewModel
@Test
fun`CPFP should append child txId to parent boostTxIds`()
@Test fun`RBF should track full parent chain in boostTxIds`()
@Test
fun`cachePendingBoostActivity should store parent chain for RBF`()
// ActivityRepo
@Test
fun`boostPendingActivities should apply parent chain to RBF transactions`()
// BackupRepo
@Test
fun`scheduleBackup should cancel existing job for same category`()

📊 Performance Considerations

11. Potential N+1 Query in UI

Location: ActivityExploreScreen.kt:266-281

onchain.v1.boostTxIds.forEachIndexed { index, parent ->Section(
title = stringResource(...),
// ... rendering
)
}

Issue: Each boostTxIds item renders a full Section. If a transaction has been boosted many times, this could create a very long list.

Recommendation: Consider limiting display to last N transactions with a "Show more" option, or use a lazy loading approach.

12. Backup Debounce is Acceptable

Status: ✅ Good

  • 5-second debounce (BACKUP_DEBOUNCE = 5000L) prevents excessive backup calls
  • Backup check interval of 1 minute is reasonable

🎯 Recommendations Summary

Must Fix Before Merge:

  1. ✅ Fix race condition in BackupRepo.scheduleBackup() by canceling existing jobs
  2. ✅ Move vssStoreIdProvider.clearCache() inside synchronized block in VssBackupClient.reset()

Should Fix Before Merge:

  1. ⚠️ Add null safety check in BoostTransactionViewModel.cachePendingBoostActivity()
  2. ⚠️ Document backup restore order changes and add error handling
  3. ⚠️ Add unit tests for new boostTxIds functionality

Nice to Have:

  1. 💡 Standardize on nowMillis() vs nowTimestamp()
  2. 💡 Clarify annotation retention changes in DispatchersModule
  3. 💡 Rename isRequired to isStale for clarity
  4. 💡 Add UI tests for new explore screen section
  5. 💡 Consider limiting displayed boost chain length

✅ Conclusion

This is a well-structured PR with clear intent and good documentation. The core functionality for tracking boosted transaction parents is sound. However, there are 2 critical thread-safety issues that must be addressed before merging, and test coverage is insufficient for the new functionality.

Once the critical issues are resolved and tests are added, this PR will be ready to merge.

Recommendation: Request changes for issues #1, #2, #3, #4, and #5.

@ovitrifovitrif mentioned this pull request Nov 18, 2025
@ovitrif
ovitrif changed the base branch from master to feat/backup-polishNovember 18, 2025 00:35
@ovitrif

Copy link
Copy Markdown
CollaboratorAuthor

Closed in favour of:

@ovitrifovitrif closed this Nov 18, 2025
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@ovitrif@github-advanced-security@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: activity boosted tx parents - #460

Closed
ovitrif wants to merge 2 commits into
feat/backup-polishfrom
feat/boosted-tx-parents
Closed

feat: activity boosted tx parents#460
ovitrif wants to merge 2 commits into
feat/backup-polishfrom
feat/boosted-tx-parents

Conversation

@ovitrif

@ovitrifovitrif commented Nov 6, 2025

Copy link
Copy Markdown
Collaborator

ROADMAP: Transaction Details

This PR adds boosted txids tracking and displays parent transaction IDs in the activity details, matching iOS PR #199.

Description

  • Added boostTxIds to PendingBoostActivity to store parent transaction IDs
  • CPFP: Append child transaction ID to parent's boostTxIds when boosting
  • RBF:
    • Track parent chain (existing boostTxIds + current txId) in PendingBoostActivity.
    • Store parent txIds when creating replacement
    • retrieve and apply when replacement syncs
  • UI: Display boosted transaction IDs in ActivityExploreScreen

Preview

CPFP (received)RBF (sent)
TODOTODO

QA Notes

1️⃣ RBF Test

  1. Send onchain
  2. Boost transaction
  3. Activity Item > Explore
  4. Expect list containing txid of previously boosted tx under "Boosted Transaction…" header

1️⃣ CPFP Test

Same but start with RECEIVE onchain.

@ovitrif
ovitrif requested a review from CopilotNovember 6, 2025 23:57
Comment threadapp/src/main/java/to/bitkit/repositories/ActivityRepo.kt Fixed
Comment threadapp/src/main/java/to/bitkit/repositories/ActivityRepo.kt Fixed
Comment threadapp/src/main/java/to/bitkit/repositories/ActivityRepo.kt Fixed
Comment threadapp/src/main/java/to/bitkit/repositories/ActivityRepo.kt Fixed
Comment threadapp/src/main/java/to/bitkit/repositories/ActivityRepo.kt Fixed

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR enhances boost transaction tracking by properly recording boost transaction IDs for both RBF (Replace-By-Fee) and CPFP (Child Pays For Parent) scenarios. The implementation ensures that parent-child transaction relationships are maintained and properly displayed in the activity detail screen.

  • Added boostTxIds field to PendingBoostActivity data class
  • Updated boost logic to track and persist transaction IDs through the boost process
  • Added UI component to display boosted transaction IDs in activity details

Reviewed Changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

FileDescription
BoostTransactionViewModel.ktUpdated CPFP and cachePendingBoostActivity logic to properly track boost transaction IDs
ActivityDetailScreen.ktAdded UI section to display boosted transaction IDs with appropriate labels for RBF and CPFP
ActivityRepo.ktUpdated pending boost activities handling to apply boostTxIds during sync
PendingBoostActivity.ktAdded boostTxIds field with default empty list for backward compatibility

@ovitrif
ovitrifforce-pushed the feat/boosted-tx-parents branch from a96c04d to 17b44e5CompareNovember 7, 2025 13:20
@claude

claudeBot commented Nov 7, 2025

Copy link
Copy Markdown
Contributor

PR Review: Activity Boosted TX Parents

Summary

This PR adds tracking and display of parent transaction IDs for boosted transactions (both CPFP and RBF), aligning with iOS implementation. Overall, the implementation is solid with good documentation and careful handling of edge cases. Below are my findings across code quality, potential issues, performance, security, and test coverage.


✅ Strengths

  1. Excellent Documentation: The code includes comprehensive comments explaining RBF vs CPFP behavior, particularly in BoostTransactionViewModel.kt:419-430 and ActivityRepo.kt:437-447.

  2. Proper State Management: The use of PendingBoostActivity to cache boost data for retry on sync failure is a robust pattern.

  3. Good Separation of Concerns: Clear distinction between immediate CPFP updates and deferred RBF replacements.

  4. Consistent Code Style: Follows Kotlin best practices with trailing commas, proper nullability handling, and immutable data structures.


🔴 Critical Issues

1. Race Condition in BackupRepo (High Priority)

Location: BackupRepo.kt:256-274

backupJobs[category] = scope.launch {
// Set running immediately to prevent UI showing failure during debounce
cacheStore.updateBackupStatus(category) {
it.copy(running =true)
}
delay(BACKUP_DEBOUNCE)
// Double-check if backup is still neededval status = cacheStore.backupStatuses.first()[category] ?:BackupItemStatus()
if (status.isRequired &&!isRestoring) {
triggerBackup(category)
} else {
// Backup no longer needed, reset running flag
cacheStore.updateBackupStatus(category) {
it.copy(running =false)
}
}
}

Issue: Not thread-safe. Multiple calls to scheduleBackup() for the same category could overwrite backupJobs[category] without canceling the previous job, leading to:

  • Multiple concurrent backups for the same category
  • Orphaned coroutines that never reset running = false
  • UI showing incorrect backup status

Recommendation:

backupJobs[category]?.cancel() // Cancel existing job
backupJobs[category] = scope.launch {
// ... rest of implementation
}

2. Synchronization Issue in VssBackupClient.reset()

Location: VssBackupClient.kt:65-72

funreset() {
synchronized(this) {
isSetup.cancel()
isSetup =CompletableDeferred()
}
vssStoreIdProvider.clearCache()
Logger.debug("VSS client reset", context =TAG)
}

Issue: vssStoreIdProvider.clearCache() is called outside the synchronized block, which could lead to:

  • Cache being cleared while setup() is reading from it
  • Inconsistent state between isSetup and cache

Recommendation: Move vssStoreIdProvider.clearCache() inside the synchronized block.


🟡 Medium Priority Issues

3. Missing Null Safety Check

Location: BoostTransactionViewModel.kt:422-430

val currentActivity = activity?.v1
val boostTxIds =if (activityToDelete !=null&& currentActivity !=null) {
currentActivity.boostTxIds + currentActivity.txId
} else {
emptyList()
}

Issue: activityToDelete != null is checked but currentActivity could still be null for RBF scenarios, leading to an empty boostTxIds list when it should contain the parent chain.

Recommendation: Add explicit error handling or logging when activityToDelete is non-null but currentActivity is null, as this indicates a bug.

4. Backup Order Changed Without Migration

Location: BackupRepo.kt:402-458

The restore order was changed from:

// Old orderSETTINGSWALLETACTIVITYMETADATABLOCKTANK

to:

// New order METADATASETTINGSWALLETACTIVITYBLOCKTANK

Issue: METADATA is restored first to populate caches early via onCacheRestored() callback, but:

  • No migration logic handles cases where users have existing backups in the old format
  • If METADATA restore fails, subsequent restores may operate with stale cache data
  • The callback onCacheRestored() is not used in this PR

Recommendation:

  • Document why METADATA must be restored first
  • Add error handling if METADATA restore fails (should it abort the entire restore?)
  • Consider if partial restore states are acceptable

5. Potential Memory Leak in WalletRepo

Location: WalletRepo.kt:240-260

suspendfunwipeWallet(walletIndex:Int = 0): Result<Unit> = withContext(bgDispatcher) {
try {
backupRepo.reset() // Cancels all coroutines in BackupRepo_walletState.update { WalletState() }
_balanceState.update { BalanceState() }
keychain.wipe()
// ... more cleanup
}
}

Issue: backupRepo.reset() is called before state is cleared, but if any backup observer coroutines are in the middle of processing state changes, they may attempt to read from already-wiped datastores.

Recommendation: Consider the order of operations - perhaps reset state first, then cancel observers.


🟢 Minor Issues / Suggestions

6. Inconsistent Use of nowMillis() vs nowTimestamp()

The PR introduces nowMillis() but both functions are used inconsistently:

  • BoostTransactionViewModel.kt: Uses nowMillis().toULong()
  • ActivityRepo.kt:316: Still uses nowTimestamp().toEpochMilli().toULong()

Recommendation: Standardize on one approach for consistency.

7. Annotation Retention Changes

Location: DispatchersModule.kt:11-20

@Qualifier
-@Retention(AnnotationRetention.BINARY)
annotationclassUiDispatcher

Issue: Removing @Retention(AnnotationRetention.BINARY) changes the default to RUNTIME, which:

  • Increases APK size slightly (annotations retained in bytecode)
  • Allows runtime reflection (usually not needed for Dagger qualifiers)

Recommendation: Clarify if this change is intentional or if it should use BINARY retention.

8. BackupItemStatus.isRequired Property

Location: BackupCategory.kt:63

data classBackupItemStatus(
valrunning:Boolean = false,
valsynced:Long = 0,
valrequired:Long = 0,
) {
val isRequired:Boolean get() = synced < required
}

Issue: The computed property name isRequired is slightly misleading. It actually means "backup is outdated/stale," not "backup is required by user."

Recommendation: Consider renaming to isStale or needsSync for clarity.

9. Health Check Comparison Changed

Location: HealthRepo.kt:129

-funisSyncOk(synced:Long, required:Long) = synced > required ||...
+funisSyncOk(synced:Long, required:Long) = synced >= required ||...

Issue: Changing from > to >= means synced == required is now considered "OK", but this is technically still a pending backup (hasn't happened yet).

Recommendation: Verify this is the intended behavior. The 5-minute grace period should handle this, but the semantic change is subtle.


🔒 Security Considerations

10. No Sensitive Data Concerns

Status: ✅ Good

  • Transaction IDs are public blockchain data
  • No credentials or private keys exposed in new code
  • VSS backup client properly handles encryption (outside scope of this PR)

🧪 Test Coverage

Issues with Test Coverage:

  1. No Tests for New Functionality: The only test change is adding the backupRepo mock to WalletRepoTest.kt:51-82, but there are:

    • No tests for boostTxIds tracking logic
    • No tests for the CPFP child appending behavior
    • No tests for the RBF parent chain tracking
    • No tests for BackupRepo.reset() or scheduleFullBackup()
  2. Missing Edge Case Tests:

    • What happens if boostTxIds list grows very large (multiple RBF chains)?
    • What if a transaction is boosted multiple times in succession?
    • What if the activity is not found when applying pending boost?
  3. UI Tests: No mention of UI tests for the new "Boosted Transaction" section in ActivityExploreScreen.kt:266-281.

Recommendation: Add unit tests for:

// BoostTransactionViewModel
@Test
fun`CPFP should append child txId to parent boostTxIds`()
@Test fun`RBF should track full parent chain in boostTxIds`()
@Test
fun`cachePendingBoostActivity should store parent chain for RBF`()
// ActivityRepo
@Test
fun`boostPendingActivities should apply parent chain to RBF transactions`()
// BackupRepo
@Test
fun`scheduleBackup should cancel existing job for same category`()

📊 Performance Considerations

11. Potential N+1 Query in UI

Location: ActivityExploreScreen.kt:266-281

onchain.v1.boostTxIds.forEachIndexed { index, parent ->Section(
title = stringResource(...),
// ... rendering
)
}

Issue: Each boostTxIds item renders a full Section. If a transaction has been boosted many times, this could create a very long list.

Recommendation: Consider limiting display to last N transactions with a "Show more" option, or use a lazy loading approach.

12. Backup Debounce is Acceptable

Status: ✅ Good

  • 5-second debounce (BACKUP_DEBOUNCE = 5000L) prevents excessive backup calls
  • Backup check interval of 1 minute is reasonable

🎯 Recommendations Summary

Must Fix Before Merge:

  1. ✅ Fix race condition in BackupRepo.scheduleBackup() by canceling existing jobs
  2. ✅ Move vssStoreIdProvider.clearCache() inside synchronized block in VssBackupClient.reset()

Should Fix Before Merge:

  1. ⚠️ Add null safety check in BoostTransactionViewModel.cachePendingBoostActivity()
  2. ⚠️ Document backup restore order changes and add error handling
  3. ⚠️ Add unit tests for new boostTxIds functionality

Nice to Have:

  1. 💡 Standardize on nowMillis() vs nowTimestamp()
  2. 💡 Clarify annotation retention changes in DispatchersModule
  3. 💡 Rename isRequired to isStale for clarity
  4. 💡 Add UI tests for new explore screen section
  5. 💡 Consider limiting displayed boost chain length

✅ Conclusion

This is a well-structured PR with clear intent and good documentation. The core functionality for tracking boosted transaction parents is sound. However, there are 2 critical thread-safety issues that must be addressed before merging, and test coverage is insufficient for the new functionality.

Once the critical issues are resolved and tests are added, this PR will be ready to merge.

Recommendation: Request changes for issues #1, #2, #3, #4, and #5.

@ovitrifovitrif mentioned this pull request Nov 18, 2025
@ovitrif
ovitrif changed the base branch from master to feat/backup-polishNovember 18, 2025 00:35
@ovitrif

Copy link
Copy Markdown
CollaboratorAuthor

Closed in favour of:

@ovitrifovitrif closed this Nov 18, 2025
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@ovitrif@github-advanced-security@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: activity boosted tx parents - #460

Closed
ovitrif wants to merge 2 commits into
feat/backup-polishfrom
feat/boosted-tx-parents
Closed

feat: activity boosted tx parents#460
ovitrif wants to merge 2 commits into
feat/backup-polishfrom
feat/boosted-tx-parents

Conversation

@ovitrif

@ovitrifovitrif commented Nov 6, 2025

Copy link
Copy Markdown
Collaborator

ROADMAP: Transaction Details

This PR adds boosted txids tracking and displays parent transaction IDs in the activity details, matching iOS PR #199.

Description

  • Added boostTxIds to PendingBoostActivity to store parent transaction IDs
  • CPFP: Append child transaction ID to parent's boostTxIds when boosting
  • RBF:
    • Track parent chain (existing boostTxIds + current txId) in PendingBoostActivity.
    • Store parent txIds when creating replacement
    • retrieve and apply when replacement syncs
  • UI: Display boosted transaction IDs in ActivityExploreScreen

Preview

CPFP (received)RBF (sent)
TODOTODO

QA Notes

1️⃣ RBF Test

  1. Send onchain
  2. Boost transaction
  3. Activity Item > Explore
  4. Expect list containing txid of previously boosted tx under "Boosted Transaction…" header

1️⃣ CPFP Test

Same but start with RECEIVE onchain.

@ovitrif
ovitrif requested a review from CopilotNovember 6, 2025 23:57
Comment threadapp/src/main/java/to/bitkit/repositories/ActivityRepo.kt Fixed
Comment threadapp/src/main/java/to/bitkit/repositories/ActivityRepo.kt Fixed
Comment threadapp/src/main/java/to/bitkit/repositories/ActivityRepo.kt Fixed
Comment threadapp/src/main/java/to/bitkit/repositories/ActivityRepo.kt Fixed
Comment threadapp/src/main/java/to/bitkit/repositories/ActivityRepo.kt Fixed

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR enhances boost transaction tracking by properly recording boost transaction IDs for both RBF (Replace-By-Fee) and CPFP (Child Pays For Parent) scenarios. The implementation ensures that parent-child transaction relationships are maintained and properly displayed in the activity detail screen.

  • Added boostTxIds field to PendingBoostActivity data class
  • Updated boost logic to track and persist transaction IDs through the boost process
  • Added UI component to display boosted transaction IDs in activity details

Reviewed Changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

FileDescription
BoostTransactionViewModel.ktUpdated CPFP and cachePendingBoostActivity logic to properly track boost transaction IDs
ActivityDetailScreen.ktAdded UI section to display boosted transaction IDs with appropriate labels for RBF and CPFP
ActivityRepo.ktUpdated pending boost activities handling to apply boostTxIds during sync
PendingBoostActivity.ktAdded boostTxIds field with default empty list for backward compatibility

@ovitrif
ovitrifforce-pushed the feat/boosted-tx-parents branch from a96c04d to 17b44e5CompareNovember 7, 2025 13:20
@claude

claudeBot commented Nov 7, 2025

Copy link
Copy Markdown
Contributor

PR Review: Activity Boosted TX Parents

Summary

This PR adds tracking and display of parent transaction IDs for boosted transactions (both CPFP and RBF), aligning with iOS implementation. Overall, the implementation is solid with good documentation and careful handling of edge cases. Below are my findings across code quality, potential issues, performance, security, and test coverage.


✅ Strengths

  1. Excellent Documentation: The code includes comprehensive comments explaining RBF vs CPFP behavior, particularly in BoostTransactionViewModel.kt:419-430 and ActivityRepo.kt:437-447.

  2. Proper State Management: The use of PendingBoostActivity to cache boost data for retry on sync failure is a robust pattern.

  3. Good Separation of Concerns: Clear distinction between immediate CPFP updates and deferred RBF replacements.

  4. Consistent Code Style: Follows Kotlin best practices with trailing commas, proper nullability handling, and immutable data structures.


🔴 Critical Issues

1. Race Condition in BackupRepo (High Priority)

Location: BackupRepo.kt:256-274

backupJobs[category] = scope.launch {
// Set running immediately to prevent UI showing failure during debounce
cacheStore.updateBackupStatus(category) {
it.copy(running =true)
}
delay(BACKUP_DEBOUNCE)
// Double-check if backup is still neededval status = cacheStore.backupStatuses.first()[category] ?:BackupItemStatus()
if (status.isRequired &&!isRestoring) {
triggerBackup(category)
} else {
// Backup no longer needed, reset running flag
cacheStore.updateBackupStatus(category) {
it.copy(running =false)
}
}
}

Issue: Not thread-safe. Multiple calls to scheduleBackup() for the same category could overwrite backupJobs[category] without canceling the previous job, leading to:

  • Multiple concurrent backups for the same category
  • Orphaned coroutines that never reset running = false
  • UI showing incorrect backup status

Recommendation:

backupJobs[category]?.cancel() // Cancel existing job
backupJobs[category] = scope.launch {
// ... rest of implementation
}

2. Synchronization Issue in VssBackupClient.reset()

Location: VssBackupClient.kt:65-72

funreset() {
synchronized(this) {
isSetup.cancel()
isSetup =CompletableDeferred()
}
vssStoreIdProvider.clearCache()
Logger.debug("VSS client reset", context =TAG)
}

Issue: vssStoreIdProvider.clearCache() is called outside the synchronized block, which could lead to:

  • Cache being cleared while setup() is reading from it
  • Inconsistent state between isSetup and cache

Recommendation: Move vssStoreIdProvider.clearCache() inside the synchronized block.


🟡 Medium Priority Issues

3. Missing Null Safety Check

Location: BoostTransactionViewModel.kt:422-430

val currentActivity = activity?.v1
val boostTxIds =if (activityToDelete !=null&& currentActivity !=null) {
currentActivity.boostTxIds + currentActivity.txId
} else {
emptyList()
}

Issue: activityToDelete != null is checked but currentActivity could still be null for RBF scenarios, leading to an empty boostTxIds list when it should contain the parent chain.

Recommendation: Add explicit error handling or logging when activityToDelete is non-null but currentActivity is null, as this indicates a bug.

4. Backup Order Changed Without Migration

Location: BackupRepo.kt:402-458

The restore order was changed from:

// Old orderSETTINGSWALLETACTIVITYMETADATABLOCKTANK

to:

// New order METADATASETTINGSWALLETACTIVITYBLOCKTANK

Issue: METADATA is restored first to populate caches early via onCacheRestored() callback, but:

  • No migration logic handles cases where users have existing backups in the old format
  • If METADATA restore fails, subsequent restores may operate with stale cache data
  • The callback onCacheRestored() is not used in this PR

Recommendation:

  • Document why METADATA must be restored first
  • Add error handling if METADATA restore fails (should it abort the entire restore?)
  • Consider if partial restore states are acceptable

5. Potential Memory Leak in WalletRepo

Location: WalletRepo.kt:240-260

suspendfunwipeWallet(walletIndex:Int = 0): Result<Unit> = withContext(bgDispatcher) {
try {
backupRepo.reset() // Cancels all coroutines in BackupRepo_walletState.update { WalletState() }
_balanceState.update { BalanceState() }
keychain.wipe()
// ... more cleanup
}
}

Issue: backupRepo.reset() is called before state is cleared, but if any backup observer coroutines are in the middle of processing state changes, they may attempt to read from already-wiped datastores.

Recommendation: Consider the order of operations - perhaps reset state first, then cancel observers.


🟢 Minor Issues / Suggestions

6. Inconsistent Use of nowMillis() vs nowTimestamp()

The PR introduces nowMillis() but both functions are used inconsistently:

  • BoostTransactionViewModel.kt: Uses nowMillis().toULong()
  • ActivityRepo.kt:316: Still uses nowTimestamp().toEpochMilli().toULong()

Recommendation: Standardize on one approach for consistency.

7. Annotation Retention Changes

Location: DispatchersModule.kt:11-20

@Qualifier
-@Retention(AnnotationRetention.BINARY)
annotationclassUiDispatcher

Issue: Removing @Retention(AnnotationRetention.BINARY) changes the default to RUNTIME, which:

  • Increases APK size slightly (annotations retained in bytecode)
  • Allows runtime reflection (usually not needed for Dagger qualifiers)

Recommendation: Clarify if this change is intentional or if it should use BINARY retention.

8. BackupItemStatus.isRequired Property

Location: BackupCategory.kt:63

data classBackupItemStatus(
valrunning:Boolean = false,
valsynced:Long = 0,
valrequired:Long = 0,
) {
val isRequired:Boolean get() = synced < required
}

Issue: The computed property name isRequired is slightly misleading. It actually means "backup is outdated/stale," not "backup is required by user."

Recommendation: Consider renaming to isStale or needsSync for clarity.

9. Health Check Comparison Changed

Location: HealthRepo.kt:129

-funisSyncOk(synced:Long, required:Long) = synced > required ||...
+funisSyncOk(synced:Long, required:Long) = synced >= required ||...

Issue: Changing from > to >= means synced == required is now considered "OK", but this is technically still a pending backup (hasn't happened yet).

Recommendation: Verify this is the intended behavior. The 5-minute grace period should handle this, but the semantic change is subtle.


🔒 Security Considerations

10. No Sensitive Data Concerns

Status: ✅ Good

  • Transaction IDs are public blockchain data
  • No credentials or private keys exposed in new code
  • VSS backup client properly handles encryption (outside scope of this PR)

🧪 Test Coverage

Issues with Test Coverage:

  1. No Tests for New Functionality: The only test change is adding the backupRepo mock to WalletRepoTest.kt:51-82, but there are:

    • No tests for boostTxIds tracking logic
    • No tests for the CPFP child appending behavior
    • No tests for the RBF parent chain tracking
    • No tests for BackupRepo.reset() or scheduleFullBackup()
  2. Missing Edge Case Tests:

    • What happens if boostTxIds list grows very large (multiple RBF chains)?
    • What if a transaction is boosted multiple times in succession?
    • What if the activity is not found when applying pending boost?
  3. UI Tests: No mention of UI tests for the new "Boosted Transaction" section in ActivityExploreScreen.kt:266-281.

Recommendation: Add unit tests for:

// BoostTransactionViewModel
@Test
fun`CPFP should append child txId to parent boostTxIds`()
@Test fun`RBF should track full parent chain in boostTxIds`()
@Test
fun`cachePendingBoostActivity should store parent chain for RBF`()
// ActivityRepo
@Test
fun`boostPendingActivities should apply parent chain to RBF transactions`()
// BackupRepo
@Test
fun`scheduleBackup should cancel existing job for same category`()

📊 Performance Considerations

11. Potential N+1 Query in UI

Location: ActivityExploreScreen.kt:266-281

onchain.v1.boostTxIds.forEachIndexed { index, parent ->Section(
title = stringResource(...),
// ... rendering
)
}

Issue: Each boostTxIds item renders a full Section. If a transaction has been boosted many times, this could create a very long list.

Recommendation: Consider limiting display to last N transactions with a "Show more" option, or use a lazy loading approach.

12. Backup Debounce is Acceptable

Status: ✅ Good

  • 5-second debounce (BACKUP_DEBOUNCE = 5000L) prevents excessive backup calls
  • Backup check interval of 1 minute is reasonable

🎯 Recommendations Summary

Must Fix Before Merge:

  1. ✅ Fix race condition in BackupRepo.scheduleBackup() by canceling existing jobs
  2. ✅ Move vssStoreIdProvider.clearCache() inside synchronized block in VssBackupClient.reset()

Should Fix Before Merge:

  1. ⚠️ Add null safety check in BoostTransactionViewModel.cachePendingBoostActivity()
  2. ⚠️ Document backup restore order changes and add error handling
  3. ⚠️ Add unit tests for new boostTxIds functionality

Nice to Have:

  1. 💡 Standardize on nowMillis() vs nowTimestamp()
  2. 💡 Clarify annotation retention changes in DispatchersModule
  3. 💡 Rename isRequired to isStale for clarity
  4. 💡 Add UI tests for new explore screen section
  5. 💡 Consider limiting displayed boost chain length

✅ Conclusion

This is a well-structured PR with clear intent and good documentation. The core functionality for tracking boosted transaction parents is sound. However, there are 2 critical thread-safety issues that must be addressed before merging, and test coverage is insufficient for the new functionality.

Once the critical issues are resolved and tests are added, this PR will be ready to merge.

Recommendation: Request changes for issues #1, #2, #3, #4, and #5.

@ovitrifovitrif mentioned this pull request Nov 18, 2025
@ovitrif
ovitrif changed the base branch from master to feat/backup-polishNovember 18, 2025 00:35
@ovitrif

Copy link
Copy Markdown
CollaboratorAuthor

Closed in favour of:

@ovitrifovitrif closed this Nov 18, 2025
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@ovitrif@github-advanced-security@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat: activity boosted tx parents - #460

Closed
ovitrif wants to merge 2 commits into
feat/backup-polishfrom
feat/boosted-tx-parents
Closed

feat: activity boosted tx parents#460
ovitrif wants to merge 2 commits into
feat/backup-polishfrom
feat/boosted-tx-parents

Conversation

@ovitrif

@ovitrifovitrif commented Nov 6, 2025

Copy link
Copy Markdown
Collaborator

ROADMAP: Transaction Details

This PR adds boosted txids tracking and displays parent transaction IDs in the activity details, matching iOS PR #199.

Description

  • Added boostTxIds to PendingBoostActivity to store parent transaction IDs
  • CPFP: Append child transaction ID to parent's boostTxIds when boosting
  • RBF:
    • Track parent chain (existing boostTxIds + current txId) in PendingBoostActivity.
    • Store parent txIds when creating replacement
    • retrieve and apply when replacement syncs
  • UI: Display boosted transaction IDs in ActivityExploreScreen

Preview

CPFP (received)RBF (sent)
TODOTODO

QA Notes

1️⃣ RBF Test

  1. Send onchain
  2. Boost transaction
  3. Activity Item > Explore
  4. Expect list containing txid of previously boosted tx under "Boosted Transaction…" header

1️⃣ CPFP Test

Same but start with RECEIVE onchain.

@ovitrif
ovitrif requested a review from CopilotNovember 6, 2025 23:57
Comment threadapp/src/main/java/to/bitkit/repositories/ActivityRepo.kt Fixed
Comment threadapp/src/main/java/to/bitkit/repositories/ActivityRepo.kt Fixed
Comment threadapp/src/main/java/to/bitkit/repositories/ActivityRepo.kt Fixed
Comment threadapp/src/main/java/to/bitkit/repositories/ActivityRepo.kt Fixed
Comment threadapp/src/main/java/to/bitkit/repositories/ActivityRepo.kt Fixed

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR enhances boost transaction tracking by properly recording boost transaction IDs for both RBF (Replace-By-Fee) and CPFP (Child Pays For Parent) scenarios. The implementation ensures that parent-child transaction relationships are maintained and properly displayed in the activity detail screen.

  • Added boostTxIds field to PendingBoostActivity data class
  • Updated boost logic to track and persist transaction IDs through the boost process
  • Added UI component to display boosted transaction IDs in activity details

Reviewed Changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

FileDescription
BoostTransactionViewModel.ktUpdated CPFP and cachePendingBoostActivity logic to properly track boost transaction IDs
ActivityDetailScreen.ktAdded UI section to display boosted transaction IDs with appropriate labels for RBF and CPFP
ActivityRepo.ktUpdated pending boost activities handling to apply boostTxIds during sync
PendingBoostActivity.ktAdded boostTxIds field with default empty list for backward compatibility

@ovitrif
ovitrifforce-pushed the feat/boosted-tx-parents branch from a96c04d to 17b44e5CompareNovember 7, 2025 13:20
@claude

claudeBot commented Nov 7, 2025

Copy link
Copy Markdown
Contributor

PR Review: Activity Boosted TX Parents

Summary

This PR adds tracking and display of parent transaction IDs for boosted transactions (both CPFP and RBF), aligning with iOS implementation. Overall, the implementation is solid with good documentation and careful handling of edge cases. Below are my findings across code quality, potential issues, performance, security, and test coverage.


✅ Strengths

  1. Excellent Documentation: The code includes comprehensive comments explaining RBF vs CPFP behavior, particularly in BoostTransactionViewModel.kt:419-430 and ActivityRepo.kt:437-447.

  2. Proper State Management: The use of PendingBoostActivity to cache boost data for retry on sync failure is a robust pattern.

  3. Good Separation of Concerns: Clear distinction between immediate CPFP updates and deferred RBF replacements.

  4. Consistent Code Style: Follows Kotlin best practices with trailing commas, proper nullability handling, and immutable data structures.


🔴 Critical Issues

1. Race Condition in BackupRepo (High Priority)

Location: BackupRepo.kt:256-274

backupJobs[category] = scope.launch {
// Set running immediately to prevent UI showing failure during debounce
cacheStore.updateBackupStatus(category) {
it.copy(running =true)
}
delay(BACKUP_DEBOUNCE)
// Double-check if backup is still neededval status = cacheStore.backupStatuses.first()[category] ?:BackupItemStatus()
if (status.isRequired &&!isRestoring) {
triggerBackup(category)
} else {
// Backup no longer needed, reset running flag
cacheStore.updateBackupStatus(category) {
it.copy(running =false)
}
}
}

Issue: Not thread-safe. Multiple calls to scheduleBackup() for the same category could overwrite backupJobs[category] without canceling the previous job, leading to:

  • Multiple concurrent backups for the same category
  • Orphaned coroutines that never reset running = false
  • UI showing incorrect backup status

Recommendation:

backupJobs[category]?.cancel() // Cancel existing job
backupJobs[category] = scope.launch {
// ... rest of implementation
}

2. Synchronization Issue in VssBackupClient.reset()

Location: VssBackupClient.kt:65-72

funreset() {
synchronized(this) {
isSetup.cancel()
isSetup =CompletableDeferred()
}
vssStoreIdProvider.clearCache()
Logger.debug("VSS client reset", context =TAG)
}

Issue: vssStoreIdProvider.clearCache() is called outside the synchronized block, which could lead to:

  • Cache being cleared while setup() is reading from it
  • Inconsistent state between isSetup and cache

Recommendation: Move vssStoreIdProvider.clearCache() inside the synchronized block.


🟡 Medium Priority Issues

3. Missing Null Safety Check

Location: BoostTransactionViewModel.kt:422-430

val currentActivity = activity?.v1
val boostTxIds =if (activityToDelete !=null&& currentActivity !=null) {
currentActivity.boostTxIds + currentActivity.txId
} else {
emptyList()
}

Issue: activityToDelete != null is checked but currentActivity could still be null for RBF scenarios, leading to an empty boostTxIds list when it should contain the parent chain.

Recommendation: Add explicit error handling or logging when activityToDelete is non-null but currentActivity is null, as this indicates a bug.

4. Backup Order Changed Without Migration

Location: BackupRepo.kt:402-458

The restore order was changed from:

// Old orderSETTINGSWALLETACTIVITYMETADATABLOCKTANK

to:

// New order METADATASETTINGSWALLETACTIVITYBLOCKTANK

Issue: METADATA is restored first to populate caches early via onCacheRestored() callback, but:

  • No migration logic handles cases where users have existing backups in the old format
  • If METADATA restore fails, subsequent restores may operate with stale cache data
  • The callback onCacheRestored() is not used in this PR

Recommendation:

  • Document why METADATA must be restored first
  • Add error handling if METADATA restore fails (should it abort the entire restore?)
  • Consider if partial restore states are acceptable

5. Potential Memory Leak in WalletRepo

Location: WalletRepo.kt:240-260

suspendfunwipeWallet(walletIndex:Int = 0): Result<Unit> = withContext(bgDispatcher) {
try {
backupRepo.reset() // Cancels all coroutines in BackupRepo_walletState.update { WalletState() }
_balanceState.update { BalanceState() }
keychain.wipe()
// ... more cleanup
}
}

Issue: backupRepo.reset() is called before state is cleared, but if any backup observer coroutines are in the middle of processing state changes, they may attempt to read from already-wiped datastores.

Recommendation: Consider the order of operations - perhaps reset state first, then cancel observers.


🟢 Minor Issues / Suggestions

6. Inconsistent Use of nowMillis() vs nowTimestamp()

The PR introduces nowMillis() but both functions are used inconsistently:

  • BoostTransactionViewModel.kt: Uses nowMillis().toULong()
  • ActivityRepo.kt:316: Still uses nowTimestamp().toEpochMilli().toULong()

Recommendation: Standardize on one approach for consistency.

7. Annotation Retention Changes

Location: DispatchersModule.kt:11-20

@Qualifier
-@Retention(AnnotationRetention.BINARY)
annotationclassUiDispatcher

Issue: Removing @Retention(AnnotationRetention.BINARY) changes the default to RUNTIME, which:

  • Increases APK size slightly (annotations retained in bytecode)
  • Allows runtime reflection (usually not needed for Dagger qualifiers)

Recommendation: Clarify if this change is intentional or if it should use BINARY retention.

8. BackupItemStatus.isRequired Property

Location: BackupCategory.kt:63

data classBackupItemStatus(
valrunning:Boolean = false,
valsynced:Long = 0,
valrequired:Long = 0,
) {
val isRequired:Boolean get() = synced < required
}

Issue: The computed property name isRequired is slightly misleading. It actually means "backup is outdated/stale," not "backup is required by user."

Recommendation: Consider renaming to isStale or needsSync for clarity.

9. Health Check Comparison Changed

Location: HealthRepo.kt:129

-funisSyncOk(synced:Long, required:Long) = synced > required ||...
+funisSyncOk(synced:Long, required:Long) = synced >= required ||...

Issue: Changing from > to >= means synced == required is now considered "OK", but this is technically still a pending backup (hasn't happened yet).

Recommendation: Verify this is the intended behavior. The 5-minute grace period should handle this, but the semantic change is subtle.


🔒 Security Considerations

10. No Sensitive Data Concerns

Status: ✅ Good

  • Transaction IDs are public blockchain data
  • No credentials or private keys exposed in new code
  • VSS backup client properly handles encryption (outside scope of this PR)

🧪 Test Coverage

Issues with Test Coverage:

  1. No Tests for New Functionality: The only test change is adding the backupRepo mock to WalletRepoTest.kt:51-82, but there are:

    • No tests for boostTxIds tracking logic
    • No tests for the CPFP child appending behavior
    • No tests for the RBF parent chain tracking
    • No tests for BackupRepo.reset() or scheduleFullBackup()
  2. Missing Edge Case Tests:

    • What happens if boostTxIds list grows very large (multiple RBF chains)?
    • What if a transaction is boosted multiple times in succession?
    • What if the activity is not found when applying pending boost?
  3. UI Tests: No mention of UI tests for the new "Boosted Transaction" section in ActivityExploreScreen.kt:266-281.

Recommendation: Add unit tests for:

// BoostTransactionViewModel
@Test
fun`CPFP should append child txId to parent boostTxIds`()
@Test fun`RBF should track full parent chain in boostTxIds`()
@Test
fun`cachePendingBoostActivity should store parent chain for RBF`()
// ActivityRepo
@Test
fun`boostPendingActivities should apply parent chain to RBF transactions`()
// BackupRepo
@Test
fun`scheduleBackup should cancel existing job for same category`()

📊 Performance Considerations

11. Potential N+1 Query in UI

Location: ActivityExploreScreen.kt:266-281

onchain.v1.boostTxIds.forEachIndexed { index, parent ->Section(
title = stringResource(...),
// ... rendering
)
}

Issue: Each boostTxIds item renders a full Section. If a transaction has been boosted many times, this could create a very long list.

Recommendation: Consider limiting display to last N transactions with a "Show more" option, or use a lazy loading approach.

12. Backup Debounce is Acceptable

Status: ✅ Good

  • 5-second debounce (BACKUP_DEBOUNCE = 5000L) prevents excessive backup calls
  • Backup check interval of 1 minute is reasonable

🎯 Recommendations Summary

Must Fix Before Merge:

  1. ✅ Fix race condition in BackupRepo.scheduleBackup() by canceling existing jobs
  2. ✅ Move vssStoreIdProvider.clearCache() inside synchronized block in VssBackupClient.reset()

Should Fix Before Merge:

  1. ⚠️ Add null safety check in BoostTransactionViewModel.cachePendingBoostActivity()
  2. ⚠️ Document backup restore order changes and add error handling
  3. ⚠️ Add unit tests for new boostTxIds functionality

Nice to Have:

  1. 💡 Standardize on nowMillis() vs nowTimestamp()
  2. 💡 Clarify annotation retention changes in DispatchersModule
  3. 💡 Rename isRequired to isStale for clarity
  4. 💡 Add UI tests for new explore screen section
  5. 💡 Consider limiting displayed boost chain length

✅ Conclusion

This is a well-structured PR with clear intent and good documentation. The core functionality for tracking boosted transaction parents is sound. However, there are 2 critical thread-safety issues that must be addressed before merging, and test coverage is insufficient for the new functionality.

Once the critical issues are resolved and tests are added, this PR will be ready to merge.

Recommendation: Request changes for issues #1, #2, #3, #4, and #5.

@ovitrifovitrif mentioned this pull request Nov 18, 2025
@ovitrif
ovitrif changed the base branch from master to feat/backup-polishNovember 18, 2025 00:35
@ovitrif

Copy link
Copy Markdown
CollaboratorAuthor

Closed in favour of:

@ovitrifovitrif closed this Nov 18, 2025
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@ovitrif@github-advanced-security@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: activity boosted tx parents - #460

Closed
ovitrif wants to merge 2 commits into
feat/backup-polishfrom
feat/boosted-tx-parents
Closed

feat: activity boosted tx parents#460
ovitrif wants to merge 2 commits into
feat/backup-polishfrom
feat/boosted-tx-parents

Conversation

@ovitrif

@ovitrifovitrif commented Nov 6, 2025

Copy link
Copy Markdown
Collaborator

ROADMAP: Transaction Details

This PR adds boosted txids tracking and displays parent transaction IDs in the activity details, matching iOS PR #199.

Description

  • Added boostTxIds to PendingBoostActivity to store parent transaction IDs
  • CPFP: Append child transaction ID to parent's boostTxIds when boosting
  • RBF:
    • Track parent chain (existing boostTxIds + current txId) in PendingBoostActivity.
    • Store parent txIds when creating replacement
    • retrieve and apply when replacement syncs
  • UI: Display boosted transaction IDs in ActivityExploreScreen

Preview

CPFP (received)RBF (sent)
TODOTODO

QA Notes

1️⃣ RBF Test

  1. Send onchain
  2. Boost transaction
  3. Activity Item > Explore
  4. Expect list containing txid of previously boosted tx under "Boosted Transaction…" header

1️⃣ CPFP Test

Same but start with RECEIVE onchain.

@ovitrif
ovitrif requested a review from CopilotNovember 6, 2025 23:57
Comment threadapp/src/main/java/to/bitkit/repositories/ActivityRepo.kt Fixed
Comment threadapp/src/main/java/to/bitkit/repositories/ActivityRepo.kt Fixed
Comment threadapp/src/main/java/to/bitkit/repositories/ActivityRepo.kt Fixed
Comment threadapp/src/main/java/to/bitkit/repositories/ActivityRepo.kt Fixed
Comment threadapp/src/main/java/to/bitkit/repositories/ActivityRepo.kt Fixed

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR enhances boost transaction tracking by properly recording boost transaction IDs for both RBF (Replace-By-Fee) and CPFP (Child Pays For Parent) scenarios. The implementation ensures that parent-child transaction relationships are maintained and properly displayed in the activity detail screen.

  • Added boostTxIds field to PendingBoostActivity data class
  • Updated boost logic to track and persist transaction IDs through the boost process
  • Added UI component to display boosted transaction IDs in activity details

Reviewed Changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

FileDescription
BoostTransactionViewModel.ktUpdated CPFP and cachePendingBoostActivity logic to properly track boost transaction IDs
ActivityDetailScreen.ktAdded UI section to display boosted transaction IDs with appropriate labels for RBF and CPFP
ActivityRepo.ktUpdated pending boost activities handling to apply boostTxIds during sync
PendingBoostActivity.ktAdded boostTxIds field with default empty list for backward compatibility

@ovitrif
ovitrifforce-pushed the feat/boosted-tx-parents branch from a96c04d to 17b44e5CompareNovember 7, 2025 13:20
@claude

claudeBot commented Nov 7, 2025

Copy link
Copy Markdown
Contributor

PR Review: Activity Boosted TX Parents

Summary

This PR adds tracking and display of parent transaction IDs for boosted transactions (both CPFP and RBF), aligning with iOS implementation. Overall, the implementation is solid with good documentation and careful handling of edge cases. Below are my findings across code quality, potential issues, performance, security, and test coverage.


✅ Strengths

  1. Excellent Documentation: The code includes comprehensive comments explaining RBF vs CPFP behavior, particularly in BoostTransactionViewModel.kt:419-430 and ActivityRepo.kt:437-447.

  2. Proper State Management: The use of PendingBoostActivity to cache boost data for retry on sync failure is a robust pattern.

  3. Good Separation of Concerns: Clear distinction between immediate CPFP updates and deferred RBF replacements.

  4. Consistent Code Style: Follows Kotlin best practices with trailing commas, proper nullability handling, and immutable data structures.


🔴 Critical Issues

1. Race Condition in BackupRepo (High Priority)

Location: BackupRepo.kt:256-274

backupJobs[category] = scope.launch {
// Set running immediately to prevent UI showing failure during debounce
cacheStore.updateBackupStatus(category) {
it.copy(running =true)
}
delay(BACKUP_DEBOUNCE)
// Double-check if backup is still neededval status = cacheStore.backupStatuses.first()[category] ?:BackupItemStatus()
if (status.isRequired &&!isRestoring) {
triggerBackup(category)
} else {
// Backup no longer needed, reset running flag
cacheStore.updateBackupStatus(category) {
it.copy(running =false)
}
}
}

Issue: Not thread-safe. Multiple calls to scheduleBackup() for the same category could overwrite backupJobs[category] without canceling the previous job, leading to:

  • Multiple concurrent backups for the same category
  • Orphaned coroutines that never reset running = false
  • UI showing incorrect backup status

Recommendation:

backupJobs[category]?.cancel() // Cancel existing job
backupJobs[category] = scope.launch {
// ... rest of implementation
}

2. Synchronization Issue in VssBackupClient.reset()

Location: VssBackupClient.kt:65-72

funreset() {
synchronized(this) {
isSetup.cancel()
isSetup =CompletableDeferred()
}
vssStoreIdProvider.clearCache()
Logger.debug("VSS client reset", context =TAG)
}

Issue: vssStoreIdProvider.clearCache() is called outside the synchronized block, which could lead to:

  • Cache being cleared while setup() is reading from it
  • Inconsistent state between isSetup and cache

Recommendation: Move vssStoreIdProvider.clearCache() inside the synchronized block.


🟡 Medium Priority Issues

3. Missing Null Safety Check

Location: BoostTransactionViewModel.kt:422-430

val currentActivity = activity?.v1
val boostTxIds =if (activityToDelete !=null&& currentActivity !=null) {
currentActivity.boostTxIds + currentActivity.txId
} else {
emptyList()
}

Issue: activityToDelete != null is checked but currentActivity could still be null for RBF scenarios, leading to an empty boostTxIds list when it should contain the parent chain.

Recommendation: Add explicit error handling or logging when activityToDelete is non-null but currentActivity is null, as this indicates a bug.

4. Backup Order Changed Without Migration

Location: BackupRepo.kt:402-458

The restore order was changed from:

// Old orderSETTINGSWALLETACTIVITYMETADATABLOCKTANK

to:

// New order METADATASETTINGSWALLETACTIVITYBLOCKTANK

Issue: METADATA is restored first to populate caches early via onCacheRestored() callback, but:

  • No migration logic handles cases where users have existing backups in the old format
  • If METADATA restore fails, subsequent restores may operate with stale cache data
  • The callback onCacheRestored() is not used in this PR

Recommendation:

  • Document why METADATA must be restored first
  • Add error handling if METADATA restore fails (should it abort the entire restore?)
  • Consider if partial restore states are acceptable

5. Potential Memory Leak in WalletRepo

Location: WalletRepo.kt:240-260

suspendfunwipeWallet(walletIndex:Int = 0): Result<Unit> = withContext(bgDispatcher) {
try {
backupRepo.reset() // Cancels all coroutines in BackupRepo_walletState.update { WalletState() }
_balanceState.update { BalanceState() }
keychain.wipe()
// ... more cleanup
}
}

Issue: backupRepo.reset() is called before state is cleared, but if any backup observer coroutines are in the middle of processing state changes, they may attempt to read from already-wiped datastores.

Recommendation: Consider the order of operations - perhaps reset state first, then cancel observers.


🟢 Minor Issues / Suggestions

6. Inconsistent Use of nowMillis() vs nowTimestamp()

The PR introduces nowMillis() but both functions are used inconsistently:

  • BoostTransactionViewModel.kt: Uses nowMillis().toULong()
  • ActivityRepo.kt:316: Still uses nowTimestamp().toEpochMilli().toULong()

Recommendation: Standardize on one approach for consistency.

7. Annotation Retention Changes

Location: DispatchersModule.kt:11-20

@Qualifier
-@Retention(AnnotationRetention.BINARY)
annotationclassUiDispatcher

Issue: Removing @Retention(AnnotationRetention.BINARY) changes the default to RUNTIME, which:

  • Increases APK size slightly (annotations retained in bytecode)
  • Allows runtime reflection (usually not needed for Dagger qualifiers)

Recommendation: Clarify if this change is intentional or if it should use BINARY retention.

8. BackupItemStatus.isRequired Property

Location: BackupCategory.kt:63

data classBackupItemStatus(
valrunning:Boolean = false,
valsynced:Long = 0,
valrequired:Long = 0,
) {
val isRequired:Boolean get() = synced < required
}

Issue: The computed property name isRequired is slightly misleading. It actually means "backup is outdated/stale," not "backup is required by user."

Recommendation: Consider renaming to isStale or needsSync for clarity.

9. Health Check Comparison Changed

Location: HealthRepo.kt:129

-funisSyncOk(synced:Long, required:Long) = synced > required ||...
+funisSyncOk(synced:Long, required:Long) = synced >= required ||...

Issue: Changing from > to >= means synced == required is now considered "OK", but this is technically still a pending backup (hasn't happened yet).

Recommendation: Verify this is the intended behavior. The 5-minute grace period should handle this, but the semantic change is subtle.


🔒 Security Considerations

10. No Sensitive Data Concerns

Status: ✅ Good

  • Transaction IDs are public blockchain data
  • No credentials or private keys exposed in new code
  • VSS backup client properly handles encryption (outside scope of this PR)

🧪 Test Coverage

Issues with Test Coverage:

  1. No Tests for New Functionality: The only test change is adding the backupRepo mock to WalletRepoTest.kt:51-82, but there are:

    • No tests for boostTxIds tracking logic
    • No tests for the CPFP child appending behavior
    • No tests for the RBF parent chain tracking
    • No tests for BackupRepo.reset() or scheduleFullBackup()
  2. Missing Edge Case Tests:

    • What happens if boostTxIds list grows very large (multiple RBF chains)?
    • What if a transaction is boosted multiple times in succession?
    • What if the activity is not found when applying pending boost?
  3. UI Tests: No mention of UI tests for the new "Boosted Transaction" section in ActivityExploreScreen.kt:266-281.

Recommendation: Add unit tests for:

// BoostTransactionViewModel
@Test
fun`CPFP should append child txId to parent boostTxIds`()
@Test fun`RBF should track full parent chain in boostTxIds`()
@Test
fun`cachePendingBoostActivity should store parent chain for RBF`()
// ActivityRepo
@Test
fun`boostPendingActivities should apply parent chain to RBF transactions`()
// BackupRepo
@Test
fun`scheduleBackup should cancel existing job for same category`()

📊 Performance Considerations

11. Potential N+1 Query in UI

Location: ActivityExploreScreen.kt:266-281

onchain.v1.boostTxIds.forEachIndexed { index, parent ->Section(
title = stringResource(...),
// ... rendering
)
}

Issue: Each boostTxIds item renders a full Section. If a transaction has been boosted many times, this could create a very long list.

Recommendation: Consider limiting display to last N transactions with a "Show more" option, or use a lazy loading approach.

12. Backup Debounce is Acceptable

Status: ✅ Good

  • 5-second debounce (BACKUP_DEBOUNCE = 5000L) prevents excessive backup calls
  • Backup check interval of 1 minute is reasonable

🎯 Recommendations Summary

Must Fix Before Merge:

  1. ✅ Fix race condition in BackupRepo.scheduleBackup() by canceling existing jobs
  2. ✅ Move vssStoreIdProvider.clearCache() inside synchronized block in VssBackupClient.reset()

Should Fix Before Merge:

  1. ⚠️ Add null safety check in BoostTransactionViewModel.cachePendingBoostActivity()
  2. ⚠️ Document backup restore order changes and add error handling
  3. ⚠️ Add unit tests for new boostTxIds functionality

Nice to Have:

  1. 💡 Standardize on nowMillis() vs nowTimestamp()
  2. 💡 Clarify annotation retention changes in DispatchersModule
  3. 💡 Rename isRequired to isStale for clarity
  4. 💡 Add UI tests for new explore screen section
  5. 💡 Consider limiting displayed boost chain length

✅ Conclusion

This is a well-structured PR with clear intent and good documentation. The core functionality for tracking boosted transaction parents is sound. However, there are 2 critical thread-safety issues that must be addressed before merging, and test coverage is insufficient for the new functionality.

Once the critical issues are resolved and tests are added, this PR will be ready to merge.

Recommendation: Request changes for issues #1, #2, #3, #4, and #5.

@ovitrifovitrif mentioned this pull request Nov 18, 2025
@ovitrif
ovitrif changed the base branch from master to feat/backup-polishNovember 18, 2025 00:35
@ovitrif

Copy link
Copy Markdown
CollaboratorAuthor

Closed in favour of:

@ovitrifovitrif closed this Nov 18, 2025
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@ovitrif@github-advanced-security@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: activity boosted tx parents - #460

Closed
ovitrif wants to merge 2 commits into
feat/backup-polishfrom
feat/boosted-tx-parents
Closed

feat: activity boosted tx parents#460
ovitrif wants to merge 2 commits into
feat/backup-polishfrom
feat/boosted-tx-parents

Conversation

@ovitrif

@ovitrifovitrif commented Nov 6, 2025

Copy link
Copy Markdown
Collaborator

ROADMAP: Transaction Details

This PR adds boosted txids tracking and displays parent transaction IDs in the activity details, matching iOS PR #199.

Description

  • Added boostTxIds to PendingBoostActivity to store parent transaction IDs
  • CPFP: Append child transaction ID to parent's boostTxIds when boosting
  • RBF:
    • Track parent chain (existing boostTxIds + current txId) in PendingBoostActivity.
    • Store parent txIds when creating replacement
    • retrieve and apply when replacement syncs
  • UI: Display boosted transaction IDs in ActivityExploreScreen

Preview

CPFP (received)RBF (sent)
TODOTODO

QA Notes

1️⃣ RBF Test

  1. Send onchain
  2. Boost transaction
  3. Activity Item > Explore
  4. Expect list containing txid of previously boosted tx under "Boosted Transaction…" header

1️⃣ CPFP Test

Same but start with RECEIVE onchain.

@ovitrif
ovitrif requested a review from CopilotNovember 6, 2025 23:57
Comment threadapp/src/main/java/to/bitkit/repositories/ActivityRepo.kt Fixed
Comment threadapp/src/main/java/to/bitkit/repositories/ActivityRepo.kt Fixed
Comment threadapp/src/main/java/to/bitkit/repositories/ActivityRepo.kt Fixed
Comment threadapp/src/main/java/to/bitkit/repositories/ActivityRepo.kt Fixed
Comment threadapp/src/main/java/to/bitkit/repositories/ActivityRepo.kt Fixed

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR enhances boost transaction tracking by properly recording boost transaction IDs for both RBF (Replace-By-Fee) and CPFP (Child Pays For Parent) scenarios. The implementation ensures that parent-child transaction relationships are maintained and properly displayed in the activity detail screen.

  • Added boostTxIds field to PendingBoostActivity data class
  • Updated boost logic to track and persist transaction IDs through the boost process
  • Added UI component to display boosted transaction IDs in activity details

Reviewed Changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

FileDescription
BoostTransactionViewModel.ktUpdated CPFP and cachePendingBoostActivity logic to properly track boost transaction IDs
ActivityDetailScreen.ktAdded UI section to display boosted transaction IDs with appropriate labels for RBF and CPFP
ActivityRepo.ktUpdated pending boost activities handling to apply boostTxIds during sync
PendingBoostActivity.ktAdded boostTxIds field with default empty list for backward compatibility

@ovitrif
ovitrifforce-pushed the feat/boosted-tx-parents branch from a96c04d to 17b44e5CompareNovember 7, 2025 13:20
@claude

claudeBot commented Nov 7, 2025

Copy link
Copy Markdown
Contributor

PR Review: Activity Boosted TX Parents

Summary

This PR adds tracking and display of parent transaction IDs for boosted transactions (both CPFP and RBF), aligning with iOS implementation. Overall, the implementation is solid with good documentation and careful handling of edge cases. Below are my findings across code quality, potential issues, performance, security, and test coverage.


✅ Strengths

  1. Excellent Documentation: The code includes comprehensive comments explaining RBF vs CPFP behavior, particularly in BoostTransactionViewModel.kt:419-430 and ActivityRepo.kt:437-447.

  2. Proper State Management: The use of PendingBoostActivity to cache boost data for retry on sync failure is a robust pattern.

  3. Good Separation of Concerns: Clear distinction between immediate CPFP updates and deferred RBF replacements.

  4. Consistent Code Style: Follows Kotlin best practices with trailing commas, proper nullability handling, and immutable data structures.


🔴 Critical Issues

1. Race Condition in BackupRepo (High Priority)

Location: BackupRepo.kt:256-274

backupJobs[category] = scope.launch {
// Set running immediately to prevent UI showing failure during debounce
cacheStore.updateBackupStatus(category) {
it.copy(running =true)
}
delay(BACKUP_DEBOUNCE)
// Double-check if backup is still neededval status = cacheStore.backupStatuses.first()[category] ?:BackupItemStatus()
if (status.isRequired &&!isRestoring) {
triggerBackup(category)
} else {
// Backup no longer needed, reset running flag
cacheStore.updateBackupStatus(category) {
it.copy(running =false)
}
}
}

Issue: Not thread-safe. Multiple calls to scheduleBackup() for the same category could overwrite backupJobs[category] without canceling the previous job, leading to:

  • Multiple concurrent backups for the same category
  • Orphaned coroutines that never reset running = false
  • UI showing incorrect backup status

Recommendation:

backupJobs[category]?.cancel() // Cancel existing job
backupJobs[category] = scope.launch {
// ... rest of implementation
}

2. Synchronization Issue in VssBackupClient.reset()

Location: VssBackupClient.kt:65-72

funreset() {
synchronized(this) {
isSetup.cancel()
isSetup =CompletableDeferred()
}
vssStoreIdProvider.clearCache()
Logger.debug("VSS client reset", context =TAG)
}

Issue: vssStoreIdProvider.clearCache() is called outside the synchronized block, which could lead to:

  • Cache being cleared while setup() is reading from it
  • Inconsistent state between isSetup and cache

Recommendation: Move vssStoreIdProvider.clearCache() inside the synchronized block.


🟡 Medium Priority Issues

3. Missing Null Safety Check

Location: BoostTransactionViewModel.kt:422-430

val currentActivity = activity?.v1
val boostTxIds =if (activityToDelete !=null&& currentActivity !=null) {
currentActivity.boostTxIds + currentActivity.txId
} else {
emptyList()
}

Issue: activityToDelete != null is checked but currentActivity could still be null for RBF scenarios, leading to an empty boostTxIds list when it should contain the parent chain.

Recommendation: Add explicit error handling or logging when activityToDelete is non-null but currentActivity is null, as this indicates a bug.

4. Backup Order Changed Without Migration

Location: BackupRepo.kt:402-458

The restore order was changed from:

// Old orderSETTINGSWALLETACTIVITYMETADATABLOCKTANK

to:

// New order METADATASETTINGSWALLETACTIVITYBLOCKTANK

Issue: METADATA is restored first to populate caches early via onCacheRestored() callback, but:

  • No migration logic handles cases where users have existing backups in the old format
  • If METADATA restore fails, subsequent restores may operate with stale cache data
  • The callback onCacheRestored() is not used in this PR

Recommendation:

  • Document why METADATA must be restored first
  • Add error handling if METADATA restore fails (should it abort the entire restore?)
  • Consider if partial restore states are acceptable

5. Potential Memory Leak in WalletRepo

Location: WalletRepo.kt:240-260

suspendfunwipeWallet(walletIndex:Int = 0): Result<Unit> = withContext(bgDispatcher) {
try {
backupRepo.reset() // Cancels all coroutines in BackupRepo_walletState.update { WalletState() }
_balanceState.update { BalanceState() }
keychain.wipe()
// ... more cleanup
}
}

Issue: backupRepo.reset() is called before state is cleared, but if any backup observer coroutines are in the middle of processing state changes, they may attempt to read from already-wiped datastores.

Recommendation: Consider the order of operations - perhaps reset state first, then cancel observers.


🟢 Minor Issues / Suggestions

6. Inconsistent Use of nowMillis() vs nowTimestamp()

The PR introduces nowMillis() but both functions are used inconsistently:

  • BoostTransactionViewModel.kt: Uses nowMillis().toULong()
  • ActivityRepo.kt:316: Still uses nowTimestamp().toEpochMilli().toULong()

Recommendation: Standardize on one approach for consistency.

7. Annotation Retention Changes

Location: DispatchersModule.kt:11-20

@Qualifier
-@Retention(AnnotationRetention.BINARY)
annotationclassUiDispatcher

Issue: Removing @Retention(AnnotationRetention.BINARY) changes the default to RUNTIME, which:

  • Increases APK size slightly (annotations retained in bytecode)
  • Allows runtime reflection (usually not needed for Dagger qualifiers)

Recommendation: Clarify if this change is intentional or if it should use BINARY retention.

8. BackupItemStatus.isRequired Property

Location: BackupCategory.kt:63

data classBackupItemStatus(
valrunning:Boolean = false,
valsynced:Long = 0,
valrequired:Long = 0,
) {
val isRequired:Boolean get() = synced < required
}

Issue: The computed property name isRequired is slightly misleading. It actually means "backup is outdated/stale," not "backup is required by user."

Recommendation: Consider renaming to isStale or needsSync for clarity.

9. Health Check Comparison Changed

Location: HealthRepo.kt:129

-funisSyncOk(synced:Long, required:Long) = synced > required ||...
+funisSyncOk(synced:Long, required:Long) = synced >= required ||...

Issue: Changing from > to >= means synced == required is now considered "OK", but this is technically still a pending backup (hasn't happened yet).

Recommendation: Verify this is the intended behavior. The 5-minute grace period should handle this, but the semantic change is subtle.


🔒 Security Considerations

10. No Sensitive Data Concerns

Status: ✅ Good

  • Transaction IDs are public blockchain data
  • No credentials or private keys exposed in new code
  • VSS backup client properly handles encryption (outside scope of this PR)

🧪 Test Coverage

Issues with Test Coverage:

  1. No Tests for New Functionality: The only test change is adding the backupRepo mock to WalletRepoTest.kt:51-82, but there are:

    • No tests for boostTxIds tracking logic
    • No tests for the CPFP child appending behavior
    • No tests for the RBF parent chain tracking
    • No tests for BackupRepo.reset() or scheduleFullBackup()
  2. Missing Edge Case Tests:

    • What happens if boostTxIds list grows very large (multiple RBF chains)?
    • What if a transaction is boosted multiple times in succession?
    • What if the activity is not found when applying pending boost?
  3. UI Tests: No mention of UI tests for the new "Boosted Transaction" section in ActivityExploreScreen.kt:266-281.

Recommendation: Add unit tests for:

// BoostTransactionViewModel
@Test
fun`CPFP should append child txId to parent boostTxIds`()
@Test fun`RBF should track full parent chain in boostTxIds`()
@Test
fun`cachePendingBoostActivity should store parent chain for RBF`()
// ActivityRepo
@Test
fun`boostPendingActivities should apply parent chain to RBF transactions`()
// BackupRepo
@Test
fun`scheduleBackup should cancel existing job for same category`()

📊 Performance Considerations

11. Potential N+1 Query in UI

Location: ActivityExploreScreen.kt:266-281

onchain.v1.boostTxIds.forEachIndexed { index, parent ->Section(
title = stringResource(...),
// ... rendering
)
}

Issue: Each boostTxIds item renders a full Section. If a transaction has been boosted many times, this could create a very long list.

Recommendation: Consider limiting display to last N transactions with a "Show more" option, or use a lazy loading approach.

12. Backup Debounce is Acceptable

Status: ✅ Good

  • 5-second debounce (BACKUP_DEBOUNCE = 5000L) prevents excessive backup calls
  • Backup check interval of 1 minute is reasonable

🎯 Recommendations Summary

Must Fix Before Merge:

  1. ✅ Fix race condition in BackupRepo.scheduleBackup() by canceling existing jobs
  2. ✅ Move vssStoreIdProvider.clearCache() inside synchronized block in VssBackupClient.reset()

Should Fix Before Merge:

  1. ⚠️ Add null safety check in BoostTransactionViewModel.cachePendingBoostActivity()
  2. ⚠️ Document backup restore order changes and add error handling
  3. ⚠️ Add unit tests for new boostTxIds functionality

Nice to Have:

  1. 💡 Standardize on nowMillis() vs nowTimestamp()
  2. 💡 Clarify annotation retention changes in DispatchersModule
  3. 💡 Rename isRequired to isStale for clarity
  4. 💡 Add UI tests for new explore screen section
  5. 💡 Consider limiting displayed boost chain length

✅ Conclusion

This is a well-structured PR with clear intent and good documentation. The core functionality for tracking boosted transaction parents is sound. However, there are 2 critical thread-safety issues that must be addressed before merging, and test coverage is insufficient for the new functionality.

Once the critical issues are resolved and tests are added, this PR will be ready to merge.

Recommendation: Request changes for issues #1, #2, #3, #4, and #5.

@ovitrifovitrif mentioned this pull request Nov 18, 2025
@ovitrif
ovitrif changed the base branch from master to feat/backup-polishNovember 18, 2025 00:35
@ovitrif

Copy link
Copy Markdown
CollaboratorAuthor

Closed in favour of:

@ovitrifovitrif closed this Nov 18, 2025
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@ovitrif@github-advanced-security@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat: activity boosted tx parents - #460

Closed
ovitrif wants to merge 2 commits into
feat/backup-polishfrom
feat/boosted-tx-parents
Closed

feat: activity boosted tx parents#460
ovitrif wants to merge 2 commits into
feat/backup-polishfrom
feat/boosted-tx-parents

Conversation

@ovitrif

@ovitrifovitrif commented Nov 6, 2025

Copy link
Copy Markdown
Collaborator

ROADMAP: Transaction Details

This PR adds boosted txids tracking and displays parent transaction IDs in the activity details, matching iOS PR #199.

Description

  • Added boostTxIds to PendingBoostActivity to store parent transaction IDs
  • CPFP: Append child transaction ID to parent's boostTxIds when boosting
  • RBF:
    • Track parent chain (existing boostTxIds + current txId) in PendingBoostActivity.
    • Store parent txIds when creating replacement
    • retrieve and apply when replacement syncs
  • UI: Display boosted transaction IDs in ActivityExploreScreen

Preview

CPFP (received)RBF (sent)
TODOTODO

QA Notes

1️⃣ RBF Test

  1. Send onchain
  2. Boost transaction
  3. Activity Item > Explore
  4. Expect list containing txid of previously boosted tx under "Boosted Transaction…" header

1️⃣ CPFP Test

Same but start with RECEIVE onchain.

@ovitrif
ovitrif requested a review from CopilotNovember 6, 2025 23:57
Comment threadapp/src/main/java/to/bitkit/repositories/ActivityRepo.kt Fixed
Comment threadapp/src/main/java/to/bitkit/repositories/ActivityRepo.kt Fixed
Comment threadapp/src/main/java/to/bitkit/repositories/ActivityRepo.kt Fixed
Comment threadapp/src/main/java/to/bitkit/repositories/ActivityRepo.kt Fixed
Comment threadapp/src/main/java/to/bitkit/repositories/ActivityRepo.kt Fixed

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR enhances boost transaction tracking by properly recording boost transaction IDs for both RBF (Replace-By-Fee) and CPFP (Child Pays For Parent) scenarios. The implementation ensures that parent-child transaction relationships are maintained and properly displayed in the activity detail screen.

  • Added boostTxIds field to PendingBoostActivity data class
  • Updated boost logic to track and persist transaction IDs through the boost process
  • Added UI component to display boosted transaction IDs in activity details

Reviewed Changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

FileDescription
BoostTransactionViewModel.ktUpdated CPFP and cachePendingBoostActivity logic to properly track boost transaction IDs
ActivityDetailScreen.ktAdded UI section to display boosted transaction IDs with appropriate labels for RBF and CPFP
ActivityRepo.ktUpdated pending boost activities handling to apply boostTxIds during sync
PendingBoostActivity.ktAdded boostTxIds field with default empty list for backward compatibility

@ovitrif
ovitrifforce-pushed the feat/boosted-tx-parents branch from a96c04d to 17b44e5CompareNovember 7, 2025 13:20
@claude

claudeBot commented Nov 7, 2025

Copy link
Copy Markdown
Contributor

PR Review: Activity Boosted TX Parents

Summary

This PR adds tracking and display of parent transaction IDs for boosted transactions (both CPFP and RBF), aligning with iOS implementation. Overall, the implementation is solid with good documentation and careful handling of edge cases. Below are my findings across code quality, potential issues, performance, security, and test coverage.


✅ Strengths

  1. Excellent Documentation: The code includes comprehensive comments explaining RBF vs CPFP behavior, particularly in BoostTransactionViewModel.kt:419-430 and ActivityRepo.kt:437-447.

  2. Proper State Management: The use of PendingBoostActivity to cache boost data for retry on sync failure is a robust pattern.

  3. Good Separation of Concerns: Clear distinction between immediate CPFP updates and deferred RBF replacements.

  4. Consistent Code Style: Follows Kotlin best practices with trailing commas, proper nullability handling, and immutable data structures.


🔴 Critical Issues

1. Race Condition in BackupRepo (High Priority)

Location: BackupRepo.kt:256-274

backupJobs[category] = scope.launch {
// Set running immediately to prevent UI showing failure during debounce
cacheStore.updateBackupStatus(category) {
it.copy(running =true)
}
delay(BACKUP_DEBOUNCE)
// Double-check if backup is still neededval status = cacheStore.backupStatuses.first()[category] ?:BackupItemStatus()
if (status.isRequired &&!isRestoring) {
triggerBackup(category)
} else {
// Backup no longer needed, reset running flag
cacheStore.updateBackupStatus(category) {
it.copy(running =false)
}
}
}

Issue: Not thread-safe. Multiple calls to scheduleBackup() for the same category could overwrite backupJobs[category] without canceling the previous job, leading to:

  • Multiple concurrent backups for the same category
  • Orphaned coroutines that never reset running = false
  • UI showing incorrect backup status

Recommendation:

backupJobs[category]?.cancel() // Cancel existing job
backupJobs[category] = scope.launch {
// ... rest of implementation
}

2. Synchronization Issue in VssBackupClient.reset()

Location: VssBackupClient.kt:65-72

funreset() {
synchronized(this) {
isSetup.cancel()
isSetup =CompletableDeferred()
}
vssStoreIdProvider.clearCache()
Logger.debug("VSS client reset", context =TAG)
}

Issue: vssStoreIdProvider.clearCache() is called outside the synchronized block, which could lead to:

  • Cache being cleared while setup() is reading from it
  • Inconsistent state between isSetup and cache

Recommendation: Move vssStoreIdProvider.clearCache() inside the synchronized block.


🟡 Medium Priority Issues

3. Missing Null Safety Check

Location: BoostTransactionViewModel.kt:422-430

val currentActivity = activity?.v1
val boostTxIds =if (activityToDelete !=null&& currentActivity !=null) {
currentActivity.boostTxIds + currentActivity.txId
} else {
emptyList()
}

Issue: activityToDelete != null is checked but currentActivity could still be null for RBF scenarios, leading to an empty boostTxIds list when it should contain the parent chain.

Recommendation: Add explicit error handling or logging when activityToDelete is non-null but currentActivity is null, as this indicates a bug.

4. Backup Order Changed Without Migration

Location: BackupRepo.kt:402-458

The restore order was changed from:

// Old orderSETTINGSWALLETACTIVITYMETADATABLOCKTANK

to:

// New order METADATASETTINGSWALLETACTIVITYBLOCKTANK

Issue: METADATA is restored first to populate caches early via onCacheRestored() callback, but:

  • No migration logic handles cases where users have existing backups in the old format
  • If METADATA restore fails, subsequent restores may operate with stale cache data
  • The callback onCacheRestored() is not used in this PR

Recommendation:

  • Document why METADATA must be restored first
  • Add error handling if METADATA restore fails (should it abort the entire restore?)
  • Consider if partial restore states are acceptable

5. Potential Memory Leak in WalletRepo

Location: WalletRepo.kt:240-260

suspendfunwipeWallet(walletIndex:Int = 0): Result<Unit> = withContext(bgDispatcher) {
try {
backupRepo.reset() // Cancels all coroutines in BackupRepo_walletState.update { WalletState() }
_balanceState.update { BalanceState() }
keychain.wipe()
// ... more cleanup
}
}

Issue: backupRepo.reset() is called before state is cleared, but if any backup observer coroutines are in the middle of processing state changes, they may attempt to read from already-wiped datastores.

Recommendation: Consider the order of operations - perhaps reset state first, then cancel observers.


🟢 Minor Issues / Suggestions

6. Inconsistent Use of nowMillis() vs nowTimestamp()

The PR introduces nowMillis() but both functions are used inconsistently:

  • BoostTransactionViewModel.kt: Uses nowMillis().toULong()
  • ActivityRepo.kt:316: Still uses nowTimestamp().toEpochMilli().toULong()

Recommendation: Standardize on one approach for consistency.

7. Annotation Retention Changes

Location: DispatchersModule.kt:11-20

@Qualifier
-@Retention(AnnotationRetention.BINARY)
annotationclassUiDispatcher

Issue: Removing @Retention(AnnotationRetention.BINARY) changes the default to RUNTIME, which:

  • Increases APK size slightly (annotations retained in bytecode)
  • Allows runtime reflection (usually not needed for Dagger qualifiers)

Recommendation: Clarify if this change is intentional or if it should use BINARY retention.

8. BackupItemStatus.isRequired Property

Location: BackupCategory.kt:63

data classBackupItemStatus(
valrunning:Boolean = false,
valsynced:Long = 0,
valrequired:Long = 0,
) {
val isRequired:Boolean get() = synced < required
}

Issue: The computed property name isRequired is slightly misleading. It actually means "backup is outdated/stale," not "backup is required by user."

Recommendation: Consider renaming to isStale or needsSync for clarity.

9. Health Check Comparison Changed

Location: HealthRepo.kt:129

-funisSyncOk(synced:Long, required:Long) = synced > required ||...
+funisSyncOk(synced:Long, required:Long) = synced >= required ||...

Issue: Changing from > to >= means synced == required is now considered "OK", but this is technically still a pending backup (hasn't happened yet).

Recommendation: Verify this is the intended behavior. The 5-minute grace period should handle this, but the semantic change is subtle.


🔒 Security Considerations

10. No Sensitive Data Concerns

Status: ✅ Good

  • Transaction IDs are public blockchain data
  • No credentials or private keys exposed in new code
  • VSS backup client properly handles encryption (outside scope of this PR)

🧪 Test Coverage

Issues with Test Coverage:

  1. No Tests for New Functionality: The only test change is adding the backupRepo mock to WalletRepoTest.kt:51-82, but there are:

    • No tests for boostTxIds tracking logic
    • No tests for the CPFP child appending behavior
    • No tests for the RBF parent chain tracking
    • No tests for BackupRepo.reset() or scheduleFullBackup()
  2. Missing Edge Case Tests:

    • What happens if boostTxIds list grows very large (multiple RBF chains)?
    • What if a transaction is boosted multiple times in succession?
    • What if the activity is not found when applying pending boost?
  3. UI Tests: No mention of UI tests for the new "Boosted Transaction" section in ActivityExploreScreen.kt:266-281.

Recommendation: Add unit tests for:

// BoostTransactionViewModel
@Test
fun`CPFP should append child txId to parent boostTxIds`()
@Test fun`RBF should track full parent chain in boostTxIds`()
@Test
fun`cachePendingBoostActivity should store parent chain for RBF`()
// ActivityRepo
@Test
fun`boostPendingActivities should apply parent chain to RBF transactions`()
// BackupRepo
@Test
fun`scheduleBackup should cancel existing job for same category`()

📊 Performance Considerations

11. Potential N+1 Query in UI

Location: ActivityExploreScreen.kt:266-281

onchain.v1.boostTxIds.forEachIndexed { index, parent ->Section(
title = stringResource(...),
// ... rendering
)
}

Issue: Each boostTxIds item renders a full Section. If a transaction has been boosted many times, this could create a very long list.

Recommendation: Consider limiting display to last N transactions with a "Show more" option, or use a lazy loading approach.

12. Backup Debounce is Acceptable

Status: ✅ Good

  • 5-second debounce (BACKUP_DEBOUNCE = 5000L) prevents excessive backup calls
  • Backup check interval of 1 minute is reasonable

🎯 Recommendations Summary

Must Fix Before Merge:

  1. ✅ Fix race condition in BackupRepo.scheduleBackup() by canceling existing jobs
  2. ✅ Move vssStoreIdProvider.clearCache() inside synchronized block in VssBackupClient.reset()

Should Fix Before Merge:

  1. ⚠️ Add null safety check in BoostTransactionViewModel.cachePendingBoostActivity()
  2. ⚠️ Document backup restore order changes and add error handling
  3. ⚠️ Add unit tests for new boostTxIds functionality

Nice to Have:

  1. 💡 Standardize on nowMillis() vs nowTimestamp()
  2. 💡 Clarify annotation retention changes in DispatchersModule
  3. 💡 Rename isRequired to isStale for clarity
  4. 💡 Add UI tests for new explore screen section
  5. 💡 Consider limiting displayed boost chain length

✅ Conclusion

This is a well-structured PR with clear intent and good documentation. The core functionality for tracking boosted transaction parents is sound. However, there are 2 critical thread-safety issues that must be addressed before merging, and test coverage is insufficient for the new functionality.

Once the critical issues are resolved and tests are added, this PR will be ready to merge.

Recommendation: Request changes for issues #1, #2, #3, #4, and #5.

@ovitrifovitrif mentioned this pull request Nov 18, 2025
@ovitrif
ovitrif changed the base branch from master to feat/backup-polishNovember 18, 2025 00:35
@ovitrif

Copy link
Copy Markdown
CollaboratorAuthor

Closed in favour of:

@ovitrifovitrif closed this Nov 18, 2025
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@ovitrif@github-advanced-security@claude