Integrate LDK onchain events - #245

Merged
ovitrif merged 20 commits into
masterfrom
feat/onchain-events
Nov 28, 2025
Merged

Integrate LDK onchain events#245
ovitrif merged 20 commits into
masterfrom
feat/onchain-events

Conversation

@ben-kaufman

@ben-kaufmanben-kaufman commented Nov 26, 2025

Copy link
Copy Markdown
Contributor

This PR integrates LDK node on chain events to replace the previous pollin and full syncs.

It also removes Address Checker, and improves handling on RBFed transactions to be recognized on both sides.

Testing:
Should test all actions, send, receive (onchain and LN), transfer to savings, transfer to spendings, etc.

Screen.Recording.2025-11-26.at.9.05.11.AM.mov

Reorg removes tx from blocks demo:

Screen.Recording.2025-11-26.at.5.13.45.PM.mov

Transaction removed from mempool demo:

Screen.Recording.2025-11-26.at.5.38.36.PM.mov

@claude

This comment was marked as outdated.

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 integrates LDK node onchain events to replace previous polling and full sync mechanisms for Bitcoin transaction management. It removes the AddressChecker utility class that relied on external API calls, and improves handling of RBF (Replace-By-Fee) transactions to properly distinguish them from CPFP (Child-Pays-For-Parent) transactions on both sender and receiver sides.

Key changes:

  • Event-driven architecture for onchain transaction updates (received, confirmed, replaced, reorged, evicted)
  • Removal of 1-second polling timer in favor of real-time LDK events
  • Enhanced RBF transaction detection using doesExist flag to differentiate from CPFP

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
BitkitNotification/NotificationService.swiftAdded notification handling for new onchain transaction events
Bitkit/Views/Wallets/Sheets/BoostSheet.swiftRemoved redundant LDK node payment sync after boost operation
Bitkit/Views/Wallets/Activity/ActivityItemView.swiftEnhanced boost button logic to differentiate between RBF and CPFP transactions
Bitkit/Views/Wallets/Activity/ActivityExplorerView.swiftReplaced AddressChecker API calls with LDK node transaction details
Bitkit/ViewModels/WalletViewModel.swiftRemoved polling mechanism and added event-driven state updates for onchain transactions
Bitkit/ViewModels/AppViewModel.swiftAdded comprehensive event handling for onchain transaction lifecycle
Bitkit/Utilities/AddressChecker.swiftRemoved external API-dependent utility in favor of native LDK functionality
Bitkit/Services/LightningService.swiftAdded transaction detail methods and event handlers for onchain events
Bitkit/Services/CoreService.swiftRefactored payment processing to handle LDK events and removed replacement tracking maps
Bitkit/AppScene.swiftRemoved balance change observer and event-based activity sync
Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolvedUpdated bitkit-core and ldk-node dependencies
Bitkit.xcodeproj/project.pbxprojUpdated project references for LDK package

Comment threadBitkit/Services/CoreService.swift
Comment threadBitkit/Services/CoreService.swift
Comment threadBitkit/Services/CoreService.swift
@claude

This comment was marked as outdated.

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

@piotr-iohk please note we will need to update the e2e tests to close the onchain received sheet.

@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

ovitrif
ovitrif previously approved these changes Nov 26, 2025

@ovitrifovitrif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

utAck

Amazing work, I've been waiting for over a year to finally have this setup in place: event-based sync for transactions 🎉

Added a few small nit comments, the one about the localizable strings applies to all cases where we added new toast messages, not only the code where I added it.

I wanted to give you the time to already make some small updates as I am starting to test this, hence me approving without testing; but I trust you'll wait for my testing 🙏🏻

Comment threadBitkit/ViewModels/AppViewModel.swift Outdated
Comment threadBitkitNotification/NotificationService.swift
@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

Comment threadBitkit/Resources/Localization/en.lproj/Localizable.strings Outdated
@claude

This comment was marked as outdated.

@piotr-iohk

Copy link
Copy Markdown
Collaborator

Ok, updated e2e tests for current state (synonymdev/bitkit-e2e-tests#56). Re-running 🤞
It would be good to coordinate merging with Android (synonymdev/bitkit-android#475)

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

@pwltr it's not due to race condition, it's just because the event of the new tx takes a few seconds to come in from the LDK node.

We can solve it by immediately caching the "intermediate" state in memory until the event comes in.

@pwltr

Copy link
Copy Markdown
Contributor

@pwltr it's not due to race condition, it's just because the event of the new tx takes a few seconds to come in from the LDK node.

We can solve it by immediately caching the "intermediate" state in memory until the event comes in.

Sounds good yeah I think an optimistic UI like this would be the way to go. Assume everything went right and correct if needed when the event(s) come in from LDK. Still feel free to postpone for now.

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

Actually had a simpler solution, fixed in: #247

pwltr
pwltr previously approved these changes Nov 27, 2025

@pwltrpwltr 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.

tACK with follow-up task here

Also note @piotr-iohk's request to merge this together with the Android counterpart.

@ben-kaufman
ben-kaufman dismissed stale reviews from pwltr, ovitrif, and jvsena42 via ab36651November 27, 2025 17:01
@claude

claudeBot commented Nov 27, 2025

Copy link
Copy Markdown
Contributor

PR Review: Integrate LDK onchain events

This is an excellent architectural improvement that modernizes the onchain transaction handling by leveraging LDK's event-driven architecture. The change from polling to event-driven updates should significantly improve performance and user experience.


Summary of Changes

Major Improvements:

  • Event-driven onchain transaction handling via LDK Node events
  • Removed AddressChecker utility (110 lines removed)
  • Enhanced RBF transaction tracking and handling
  • Added comprehensive onchain event handling (received, confirmed, replaced, reorged, evicted)
  • Improved user notifications for transaction state changes
  • Better handling of edge cases (reorgs, mempool evictions)

Impact:

  • 1,031 additions, 495 deletions across 20 files
  • Core architectural change in CoreService.swift (+529/-273)
  • Enhanced event handling in AppViewModel.swift (+108/-4)
  • Improved LightningService.swift integration (+120/-3)

Code Quality & Best Practices

Strengths

  1. Event-Driven Architecture - Proper use of LDK onchain events eliminates polling overhead
  2. Comprehensive Error Handling - Good use of try-catch blocks with descriptive logging
  3. Service Queue Pattern - Correctly uses ServiceQueue.background(.core) for Core operations
  4. MainActor Compliance - Proper async/await patterns with @mainactor for UI updates
  5. Caching Strategy - cachedTxIdsInBoostTxIds improves performance for boost transaction lookups
  6. Detailed Logging - Excellent context-aware logging throughout
  7. Accessibility - Added accessibilityIdentifier to Toast model and components

Code Organization

The new event handlers follow a clean pattern:

  • handleOnchainTransactionReceived
  • handleOnchainTransactionConfirmed
  • handleOnchainTransactionReplaced
  • handleOnchainTransactionReorged
  • handleOnchainTransactionEvicted

Each handler properly delegates to ServiceQueue.background(.core) and updates the activity list appropriately.


Potential Bugs & Issues

1. Race Condition in handleOnchainTransactionReplaced

Location: CoreService.swift:346-433

Issue: When processing replacement transactions, there is a potential race condition between checking if replacementActivity exists and processing the payment. Another event handler could process the same transaction concurrently.

Recommendation: Add a lock or transaction ID tracking to prevent concurrent processing of the same transaction.

2. Cache Invalidation Issue

Location: CoreService.swift:34-62

Issue: The cachedTxIdsInBoostTxIds cache is updated in insert and bulk refreshed in upsertList, but individual update operations don't update the cache. This could lead to stale cache data.

Recommendation: Add cache update to the update method to keep cache consistent.

3. Potential Memory Leak in Event Handlers

Location: AppScene.swift:213-215

Issue: The event handler closure captures app weakly, but the closure itself is never removed when the scene is dismissed.

Recommendation: Ensure event handlers are removed when the scene is dismissed. The addOnEvent API should support removal, or use a cleanup mechanism in deinit or onDisappear.

4. Missing nil-check in processOnchainTransaction

Location: CoreService.swift:315-332

Issue: The function logs a warning if payment is not found but doesn't throw an error. The calling code may not handle this gracefully.

Recommendation: Consider throwing a specific error so callers can decide how to handle missing payments, or add metrics to track how often this occurs.


Performance Considerations

Strengths

  1. Eliminated Polling - Removing AddressChecker and full syncs significantly reduces CPU/network overhead
  2. Efficient Caching - cachedTxIdsInBoostTxIds prevents repeated database queries
  3. Lazy Cache Initialization - Cache is only built when first accessed

Concerns

  1. Cache Refresh Performance - CoreService.swift:47-62 loads ALL onchain activities from the database, which could be slow for users with large transaction histories (1000+ transactions). Consider a more efficient query that only fetches boostTxIds without loading full activity objects.

  2. Delay in Received Sheet - AppViewModel.swift:399 has a 500ms delay. Is this necessary? It could make the UI feel sluggish. Consider removing or reducing if not strictly required.


Security Concerns

Good Practices

  1. Proper use of ServiceQueue prevents concurrent access to Core/LDK
  2. Transactions marked as doesExist = false when replaced (prevents double-counting)
  3. RBF detection prevents showing duplicate received sheets

Recommendations

  1. Transaction Validation - Consider adding validation that conflicts array in handleOnchainTransactionReplaced actually contains valid transaction IDs to prevent malformed data corruption.

  2. Timestamp Validation - In processOnchainPayment, ensure timestamps are within reasonable ranges to prevent time-based attacks.


Test Coverage

Missing Test Scenarios

  1. RBF Handling - Need tests for single replacement, multiple conflicts, chain of replacements, replacement arriving before original

  2. Reorg Handling - Need tests for confirmed tx becoming unconfirmed, multiple txs affected by reorg, reorg depth > 1 block

  3. Edge Cases - Need tests for tx evicted from mempool that was not replaced, race conditions, cache invalidation scenarios

Testing Recommendations

Add integration tests that simulate these event sequences:

  • onchainTransactionReceived → onchainTransactionReplaced → onchainTransactionConfirmed
  • onchainTransactionConfirmed → onchainTransactionReorged
  • onchainTransactionReceived → onchainTransactionEvicted

Final Recommendations

Must Fix (Before Merge)

  1. Run swiftformat . to ensure code formatting compliance
  2. Fix cache invalidation in update method
  3. Add race condition protection in handleOnchainTransactionReplaced
  4. Verify event handler cleanup to prevent memory leaks

Should Fix (High Priority)

  1. Optimize refreshBoostTxIdsCache for large transaction histories
  2. Add comprehensive tests for RBF and reorg scenarios
  3. Review the 500ms delay in received sheet display

Nice to Have

  1. Add transaction ID truncation to user-facing messages
  2. Improve user-friendly language for technical events (reorg, eviction)
  3. Add metrics/telemetry for tracking event frequencies

Conclusion

This is a high-quality PR that represents a significant architectural improvement. The event-driven approach is the correct design pattern and will eliminate polling overhead. The code quality is generally excellent with proper error handling, logging, and async patterns.

Recommendation:Approve with minor changes. Address the cache invalidation and race condition issues before merge, and plan follow-up work for comprehensive testing and performance optimization.

Great work! The videos demonstrating reorg and eviction handling are particularly impressive.

pwltr
pwltr previously approved these changes Nov 27, 2025
@ovitrif
ovitrif enabled auto-merge November 27, 2025 18:26
@ovitrif
ovitrif disabled auto-merge November 27, 2025 18:26
ovitrif
ovitrif previously approved these changes Nov 27, 2025

@ovitrifovitrif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

tAck

now it really shows the boosted TX update instantly every time I try it

@ovitrif
ovitrif dismissed stale reviews from pwltr and themself via 4b9083dNovember 27, 2025 22:39
@claude

claudeBot commented Nov 27, 2025

Copy link
Copy Markdown
Contributor

PR Review: Integrate LDK onchain events

Summary

This PR successfully replaces polling-based transaction monitoring with event-driven LDK onchain events, removes the AddressChecker, and improves RBF transaction handling. This is a significant architectural improvement that makes the app more reactive and efficient.

✅ Strengths

Architecture & Design

  1. Event-driven architecture: The transition from polling to LDK events (onchainTransactionReceived, onchainTransactionConfirmed, onchainTransactionReplaced, onchainTransactionReorged, onchainTransactionEvicted) is excellent. This provides real-time updates and reduces unnecessary syncs.

  2. Comprehensive RBF handling: The implementation properly tracks replacement transactions through boostTxIds and distinguishes between RBF (replaced transactions with doesExist=false) and CPFP (child transactions with doesExist=true).

  3. Clean separation of concerns: Event handlers in LightningService.swift (708-757) delegate to ActivityService methods, maintaining good separation between Lightning layer and activity management.

  4. Improved user feedback: New toast notifications with accessibility identifiers for transaction states (replaced, reorged, evicted) provide better UX.

  5. Removal of polling: Removing the onChange(of: wallet.totalBalanceSats) trigger in AppScene.swift:217 and the event-based sync in AppScene.swift:217-220 eliminates redundant activity syncing.

Code Quality

  1. Good error handling: All event handlers wrap operations in try-catch blocks with proper logging.
  2. Proper concurrency: Uses ServiceQueue.background(.core) for all database operations.
  3. Cache optimization: The cachedTxIdsInBoostTxIds cache improves performance for checking replaced transactions.

⚠️ Issues & Concerns

Critical

1. Potential cache inconsistency in CoreService.swift:36-43

func getTxIdsInBoostTxIds()async->Set<String>{if cachedTxIdsInBoostTxIds.isEmpty {awaitrefreshBoostTxIdsCache()}return cachedTxIdsInBoostTxIds
}

Issue: The cache is only refreshed when empty, but individual updates via updateBoostTxIdsCache(for:) might not be called for all updates. The update() method at line 195 doesn't update the cache.

Recommendation:

  • Update the cache in the update() method, or
  • Implement a TTL-based cache invalidation strategy, or
  • Document why the cache only needs to be refreshed when empty

2. Race condition in RBF handling (CoreService.swift:355-441)

When handleOnchainTransactionReplaced is called, it:

  1. Marks the replaced transaction as doesExist=false
  2. Processes replacement transactions that might not exist yet in the payment store
  3. Waits for them via processOnchainPayment

Issue: There's a timing window where a replacement transaction event arrives before LDK updates the payment store. The code handles this (line 385-403) by processing the payment, but there's a potential race if the transaction hasn't been added to the payments list yet.

Recommendation: Consider adding retry logic or ensuring LDK guarantees payment store updates before emitting replacement events.

Moderate

3. Missing cache update in update() method (CoreService.swift:193-199)

func update(id:String, activity:Activity)asyncthrows{tryawaitServiceQueue.background(.core){tryupdateActivity(activityId: id, activity: activity)self.metadataChangedSubject.send()}}

This doesn't call updateBoostTxIdsCache(for: activity) like insert() does at line 172.

Recommendation: Add cache update to maintain consistency.

4. Incomplete validation in shouldShowReceivedSheet (CoreService.swift:76-107)

The method checks if a replacement transaction has the same value as the replaced transaction to avoid showing duplicate sheets. However:

  • It only checks exact value matches
  • Doesn't account for fee differences in RBF
  • Could show sheets for CPFP where the child transaction might have a different value

Recommendation: Consider checking isBoosted flag and the nature of the boost (RBF vs CPFP) for more precise logic.

5. Error handling in handleOnchainTransactionReplaced (CoreService.swift:355-441)

When processing replacement transactions, errors are caught and logged (line 401-405), but the loop continues. If multiple replacement transactions fail to process, the relationship tracking could be incomplete.

Recommendation: Consider collecting errors and either throwing or returning them to the caller for better error visibility.

6. CI workflow changes remove caching (integration-tests.yml, unit-tests.yml)

The PR removes DerivedData caching and restore-keys from SPM cache. While this ensures clean builds, it will increase build times significantly.

Recommendation:

  • Document why caching was removed (likely due to cache invalidation issues)
  • Consider using Xcode Cloud or other CI optimizations to offset the performance loss
  • The -onlyUsePackageVersionsFromResolvedFile flag is good for reproducibility

Minor

7. Potential N+1 query in getBoostTxDoesExist (CoreService.swift:130-138)

func getBoostTxDoesExist(boostTxIds:[String])async->[String:Bool]{vardoesExistMap:[String:Bool]=[:]forboostTxIdin boostTxIds {
if let boostActivity =try?awaitgetOnchainActivityByTxId(txid: boostTxId){doesExistMap[boostTxId]= boostActivity.doesExist
}}return doesExistMap
}

Issue: Makes individual database queries for each boost transaction.

Recommendation: Consider adding a batch query method to BitkitCore to fetch multiple activities by txId in one query.

8. Magic number delay in AppViewModel.swift:399

try?awaitTask.sleep(nanoseconds:500_000_000) // 500ms delay

Recommendation: Extract to a named constant with documentation explaining why the delay is necessary.

9. Missing test coverage

While TxBumpingTests.swift and ActivityListTest.swift exist, there don't appear to be specific tests for:

  • Onchain event handlers
  • RBF replacement tracking with boostTxIds
  • Reorg and eviction scenarios
  • Cache consistency

Recommendation: Add integration tests for the new event handlers.

🔒 Security

No major security concerns identified. The code properly:

  • Uses ServiceQueue for thread safety
  • Doesn't expose sensitive transaction data in logs beyond txids
  • Maintains Bitcoin/Lightning operation isolation in the service layer

Minor: Logging in LightningService.swift:711-757 includes transaction amounts. Consider whether this is acceptable for production logging.

🚀 Performance

Positive impacts:

  1. Eliminates redundant polling and full activity list syncs
  2. Event-driven updates only process changed transactions
  3. Cache for boostTxIds lookups

Potential concerns:

  1. Cache not updated consistently (see issue Project setup #1, Migrate channels from LDK to LDK-node #3)
  2. N+1 queries in getBoostTxDoesExist (see issue feat: keychain #7)

🧪 Testing

Existing test coverage:

  • ActivityListTest.swift: Good coverage of basic activity CRUD operations
  • TxBumpingTests.swift: Tests RBF scenarios

Missing:

  • Tests for new onchain event handlers
  • Tests for handleOnchainTransactionReplaced, handleOnchainTransactionReorged, handleOnchainTransactionEvicted
  • Tests for cache consistency
  • Integration tests for the complete event flow

Recommendation: Add integration tests that simulate the event sequences shown in the PR videos.

📝 Recommendations

Must Fix Before Merge

  1. Fix cache consistency issues (Issues Project setup #1, Migrate channels from LDK to LDK-node #3)
  2. Add error handling for race conditions in RBF handling (Issue Push service setup and service queues  #2)

Should Fix Before Merge

  1. Improve shouldShowReceivedSheet validation logic (Issue Create swift.yml #4)
  2. Better error aggregation in handleOnchainTransactionReplaced (Issue Create ios.yml #5)

Consider for Follow-up

  1. Add batch query support for getBoostTxDoesExist (Issue feat: keychain #7)
  2. Add integration tests for event handlers (Issue Use SwiftFormat #9)
  3. Extract magic constants (Issue BDK 1.0.0-alpha.11 #8)
  4. Document CI caching removal reasoning (Issue Create ios.yml #6)

🎯 Overall Assessment

This is a high-quality PR that represents a significant architectural improvement. The event-driven approach is the correct design, and the RBF handling logic is well thought out. The main concerns are around cache consistency and potential race conditions in RBF processing, which should be addressed before merge.

The removal of AddressChecker and polling-based syncing will improve app performance and responsiveness.

Recommendation: Request changes to address cache consistency and race condition handling, then approve after fixes.


Great work on this PR! The demos showing reorg and mempool eviction handling are particularly impressive. Once the cache and race condition issues are resolved, this will be a solid improvement to the codebase.

@ovitrifovitrif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

tAck

now it really shows the boosted TX update instantly every time

@pwltrpwltr linked an issue Nov 28, 2025 that may be closed by this pull request
@ovitrif

Copy link
Copy Markdown
Collaborator

Android PR should succeed e2e once Electrum on staging is fixed. Merging this.

1 similar comment
@ovitrif

Copy link
Copy Markdown
Collaborator

Android PR should succeed e2e once Electrum on staging is fixed. Merging this.

@ovitrif
ovitrif merged commit 4b25919 into masterNov 28, 2025
10 checks passed
@ovitrif
ovitrif deleted the feat/onchain-events branch November 28, 2025 23:14
BitcoinErrorLog pushed a commit to BitcoinErrorLog/bitkit-ios that referenced this pull request Dec 15, 2025
BitcoinErrorLog pushed a commit to BitcoinErrorLog/bitkit-ios that referenced this pull request Feb 7, 2026
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.

[Bug]: Polish RBF & CPFP activity list behaviour

6 participants

@ben-kaufman@ovitrif@piotr-iohk@pwltr@jvsena42
, '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

Integrate LDK onchain events - #245

Merged
ovitrif merged 20 commits into
masterfrom
feat/onchain-events
Nov 28, 2025
Merged

Integrate LDK onchain events#245
ovitrif merged 20 commits into
masterfrom
feat/onchain-events

Conversation

@ben-kaufman

@ben-kaufmanben-kaufman commented Nov 26, 2025

Copy link
Copy Markdown
Contributor

This PR integrates LDK node on chain events to replace the previous pollin and full syncs.

It also removes Address Checker, and improves handling on RBFed transactions to be recognized on both sides.

Testing:
Should test all actions, send, receive (onchain and LN), transfer to savings, transfer to spendings, etc.

Screen.Recording.2025-11-26.at.9.05.11.AM.mov

Reorg removes tx from blocks demo:

Screen.Recording.2025-11-26.at.5.13.45.PM.mov

Transaction removed from mempool demo:

Screen.Recording.2025-11-26.at.5.38.36.PM.mov

@claude

This comment was marked as outdated.

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 integrates LDK node onchain events to replace previous polling and full sync mechanisms for Bitcoin transaction management. It removes the AddressChecker utility class that relied on external API calls, and improves handling of RBF (Replace-By-Fee) transactions to properly distinguish them from CPFP (Child-Pays-For-Parent) transactions on both sender and receiver sides.

Key changes:

  • Event-driven architecture for onchain transaction updates (received, confirmed, replaced, reorged, evicted)
  • Removal of 1-second polling timer in favor of real-time LDK events
  • Enhanced RBF transaction detection using doesExist flag to differentiate from CPFP

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
BitkitNotification/NotificationService.swiftAdded notification handling for new onchain transaction events
Bitkit/Views/Wallets/Sheets/BoostSheet.swiftRemoved redundant LDK node payment sync after boost operation
Bitkit/Views/Wallets/Activity/ActivityItemView.swiftEnhanced boost button logic to differentiate between RBF and CPFP transactions
Bitkit/Views/Wallets/Activity/ActivityExplorerView.swiftReplaced AddressChecker API calls with LDK node transaction details
Bitkit/ViewModels/WalletViewModel.swiftRemoved polling mechanism and added event-driven state updates for onchain transactions
Bitkit/ViewModels/AppViewModel.swiftAdded comprehensive event handling for onchain transaction lifecycle
Bitkit/Utilities/AddressChecker.swiftRemoved external API-dependent utility in favor of native LDK functionality
Bitkit/Services/LightningService.swiftAdded transaction detail methods and event handlers for onchain events
Bitkit/Services/CoreService.swiftRefactored payment processing to handle LDK events and removed replacement tracking maps
Bitkit/AppScene.swiftRemoved balance change observer and event-based activity sync
Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolvedUpdated bitkit-core and ldk-node dependencies
Bitkit.xcodeproj/project.pbxprojUpdated project references for LDK package

Comment threadBitkit/Services/CoreService.swift
Comment threadBitkit/Services/CoreService.swift
Comment threadBitkit/Services/CoreService.swift
@claude

This comment was marked as outdated.

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

@piotr-iohk please note we will need to update the e2e tests to close the onchain received sheet.

@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

ovitrif
ovitrif previously approved these changes Nov 26, 2025

@ovitrifovitrif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

utAck

Amazing work, I've been waiting for over a year to finally have this setup in place: event-based sync for transactions 🎉

Added a few small nit comments, the one about the localizable strings applies to all cases where we added new toast messages, not only the code where I added it.

I wanted to give you the time to already make some small updates as I am starting to test this, hence me approving without testing; but I trust you'll wait for my testing 🙏🏻

Comment threadBitkit/ViewModels/AppViewModel.swift Outdated
Comment threadBitkitNotification/NotificationService.swift
@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

Comment threadBitkit/Resources/Localization/en.lproj/Localizable.strings Outdated
@claude

This comment was marked as outdated.

@piotr-iohk

Copy link
Copy Markdown
Collaborator

Ok, updated e2e tests for current state (synonymdev/bitkit-e2e-tests#56). Re-running 🤞
It would be good to coordinate merging with Android (synonymdev/bitkit-android#475)

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

@pwltr it's not due to race condition, it's just because the event of the new tx takes a few seconds to come in from the LDK node.

We can solve it by immediately caching the "intermediate" state in memory until the event comes in.

@pwltr

Copy link
Copy Markdown
Contributor

@pwltr it's not due to race condition, it's just because the event of the new tx takes a few seconds to come in from the LDK node.

We can solve it by immediately caching the "intermediate" state in memory until the event comes in.

Sounds good yeah I think an optimistic UI like this would be the way to go. Assume everything went right and correct if needed when the event(s) come in from LDK. Still feel free to postpone for now.

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

Actually had a simpler solution, fixed in: #247

pwltr
pwltr previously approved these changes Nov 27, 2025

@pwltrpwltr 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.

tACK with follow-up task here

Also note @piotr-iohk's request to merge this together with the Android counterpart.

@ben-kaufman
ben-kaufman dismissed stale reviews from pwltr, ovitrif, and jvsena42 via ab36651November 27, 2025 17:01
@claude

claudeBot commented Nov 27, 2025

Copy link
Copy Markdown
Contributor

PR Review: Integrate LDK onchain events

This is an excellent architectural improvement that modernizes the onchain transaction handling by leveraging LDK's event-driven architecture. The change from polling to event-driven updates should significantly improve performance and user experience.


Summary of Changes

Major Improvements:

  • Event-driven onchain transaction handling via LDK Node events
  • Removed AddressChecker utility (110 lines removed)
  • Enhanced RBF transaction tracking and handling
  • Added comprehensive onchain event handling (received, confirmed, replaced, reorged, evicted)
  • Improved user notifications for transaction state changes
  • Better handling of edge cases (reorgs, mempool evictions)

Impact:

  • 1,031 additions, 495 deletions across 20 files
  • Core architectural change in CoreService.swift (+529/-273)
  • Enhanced event handling in AppViewModel.swift (+108/-4)
  • Improved LightningService.swift integration (+120/-3)

Code Quality & Best Practices

Strengths

  1. Event-Driven Architecture - Proper use of LDK onchain events eliminates polling overhead
  2. Comprehensive Error Handling - Good use of try-catch blocks with descriptive logging
  3. Service Queue Pattern - Correctly uses ServiceQueue.background(.core) for Core operations
  4. MainActor Compliance - Proper async/await patterns with @mainactor for UI updates
  5. Caching Strategy - cachedTxIdsInBoostTxIds improves performance for boost transaction lookups
  6. Detailed Logging - Excellent context-aware logging throughout
  7. Accessibility - Added accessibilityIdentifier to Toast model and components

Code Organization

The new event handlers follow a clean pattern:

  • handleOnchainTransactionReceived
  • handleOnchainTransactionConfirmed
  • handleOnchainTransactionReplaced
  • handleOnchainTransactionReorged
  • handleOnchainTransactionEvicted

Each handler properly delegates to ServiceQueue.background(.core) and updates the activity list appropriately.


Potential Bugs & Issues

1. Race Condition in handleOnchainTransactionReplaced

Location: CoreService.swift:346-433

Issue: When processing replacement transactions, there is a potential race condition between checking if replacementActivity exists and processing the payment. Another event handler could process the same transaction concurrently.

Recommendation: Add a lock or transaction ID tracking to prevent concurrent processing of the same transaction.

2. Cache Invalidation Issue

Location: CoreService.swift:34-62

Issue: The cachedTxIdsInBoostTxIds cache is updated in insert and bulk refreshed in upsertList, but individual update operations don't update the cache. This could lead to stale cache data.

Recommendation: Add cache update to the update method to keep cache consistent.

3. Potential Memory Leak in Event Handlers

Location: AppScene.swift:213-215

Issue: The event handler closure captures app weakly, but the closure itself is never removed when the scene is dismissed.

Recommendation: Ensure event handlers are removed when the scene is dismissed. The addOnEvent API should support removal, or use a cleanup mechanism in deinit or onDisappear.

4. Missing nil-check in processOnchainTransaction

Location: CoreService.swift:315-332

Issue: The function logs a warning if payment is not found but doesn't throw an error. The calling code may not handle this gracefully.

Recommendation: Consider throwing a specific error so callers can decide how to handle missing payments, or add metrics to track how often this occurs.


Performance Considerations

Strengths

  1. Eliminated Polling - Removing AddressChecker and full syncs significantly reduces CPU/network overhead
  2. Efficient Caching - cachedTxIdsInBoostTxIds prevents repeated database queries
  3. Lazy Cache Initialization - Cache is only built when first accessed

Concerns

  1. Cache Refresh Performance - CoreService.swift:47-62 loads ALL onchain activities from the database, which could be slow for users with large transaction histories (1000+ transactions). Consider a more efficient query that only fetches boostTxIds without loading full activity objects.

  2. Delay in Received Sheet - AppViewModel.swift:399 has a 500ms delay. Is this necessary? It could make the UI feel sluggish. Consider removing or reducing if not strictly required.


Security Concerns

Good Practices

  1. Proper use of ServiceQueue prevents concurrent access to Core/LDK
  2. Transactions marked as doesExist = false when replaced (prevents double-counting)
  3. RBF detection prevents showing duplicate received sheets

Recommendations

  1. Transaction Validation - Consider adding validation that conflicts array in handleOnchainTransactionReplaced actually contains valid transaction IDs to prevent malformed data corruption.

  2. Timestamp Validation - In processOnchainPayment, ensure timestamps are within reasonable ranges to prevent time-based attacks.


Test Coverage

Missing Test Scenarios

  1. RBF Handling - Need tests for single replacement, multiple conflicts, chain of replacements, replacement arriving before original

  2. Reorg Handling - Need tests for confirmed tx becoming unconfirmed, multiple txs affected by reorg, reorg depth > 1 block

  3. Edge Cases - Need tests for tx evicted from mempool that was not replaced, race conditions, cache invalidation scenarios

Testing Recommendations

Add integration tests that simulate these event sequences:

  • onchainTransactionReceived → onchainTransactionReplaced → onchainTransactionConfirmed
  • onchainTransactionConfirmed → onchainTransactionReorged
  • onchainTransactionReceived → onchainTransactionEvicted

Final Recommendations

Must Fix (Before Merge)

  1. Run swiftformat . to ensure code formatting compliance
  2. Fix cache invalidation in update method
  3. Add race condition protection in handleOnchainTransactionReplaced
  4. Verify event handler cleanup to prevent memory leaks

Should Fix (High Priority)

  1. Optimize refreshBoostTxIdsCache for large transaction histories
  2. Add comprehensive tests for RBF and reorg scenarios
  3. Review the 500ms delay in received sheet display

Nice to Have

  1. Add transaction ID truncation to user-facing messages
  2. Improve user-friendly language for technical events (reorg, eviction)
  3. Add metrics/telemetry for tracking event frequencies

Conclusion

This is a high-quality PR that represents a significant architectural improvement. The event-driven approach is the correct design pattern and will eliminate polling overhead. The code quality is generally excellent with proper error handling, logging, and async patterns.

Recommendation:Approve with minor changes. Address the cache invalidation and race condition issues before merge, and plan follow-up work for comprehensive testing and performance optimization.

Great work! The videos demonstrating reorg and eviction handling are particularly impressive.

pwltr
pwltr previously approved these changes Nov 27, 2025
@ovitrif
ovitrif enabled auto-merge November 27, 2025 18:26
@ovitrif
ovitrif disabled auto-merge November 27, 2025 18:26
ovitrif
ovitrif previously approved these changes Nov 27, 2025

@ovitrifovitrif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

tAck

now it really shows the boosted TX update instantly every time I try it

@ovitrif
ovitrif dismissed stale reviews from pwltr and themself via 4b9083dNovember 27, 2025 22:39
@claude

claudeBot commented Nov 27, 2025

Copy link
Copy Markdown
Contributor

PR Review: Integrate LDK onchain events

Summary

This PR successfully replaces polling-based transaction monitoring with event-driven LDK onchain events, removes the AddressChecker, and improves RBF transaction handling. This is a significant architectural improvement that makes the app more reactive and efficient.

✅ Strengths

Architecture & Design

  1. Event-driven architecture: The transition from polling to LDK events (onchainTransactionReceived, onchainTransactionConfirmed, onchainTransactionReplaced, onchainTransactionReorged, onchainTransactionEvicted) is excellent. This provides real-time updates and reduces unnecessary syncs.

  2. Comprehensive RBF handling: The implementation properly tracks replacement transactions through boostTxIds and distinguishes between RBF (replaced transactions with doesExist=false) and CPFP (child transactions with doesExist=true).

  3. Clean separation of concerns: Event handlers in LightningService.swift (708-757) delegate to ActivityService methods, maintaining good separation between Lightning layer and activity management.

  4. Improved user feedback: New toast notifications with accessibility identifiers for transaction states (replaced, reorged, evicted) provide better UX.

  5. Removal of polling: Removing the onChange(of: wallet.totalBalanceSats) trigger in AppScene.swift:217 and the event-based sync in AppScene.swift:217-220 eliminates redundant activity syncing.

Code Quality

  1. Good error handling: All event handlers wrap operations in try-catch blocks with proper logging.
  2. Proper concurrency: Uses ServiceQueue.background(.core) for all database operations.
  3. Cache optimization: The cachedTxIdsInBoostTxIds cache improves performance for checking replaced transactions.

⚠️ Issues & Concerns

Critical

1. Potential cache inconsistency in CoreService.swift:36-43

func getTxIdsInBoostTxIds()async->Set<String>{if cachedTxIdsInBoostTxIds.isEmpty {awaitrefreshBoostTxIdsCache()}return cachedTxIdsInBoostTxIds
}

Issue: The cache is only refreshed when empty, but individual updates via updateBoostTxIdsCache(for:) might not be called for all updates. The update() method at line 195 doesn't update the cache.

Recommendation:

  • Update the cache in the update() method, or
  • Implement a TTL-based cache invalidation strategy, or
  • Document why the cache only needs to be refreshed when empty

2. Race condition in RBF handling (CoreService.swift:355-441)

When handleOnchainTransactionReplaced is called, it:

  1. Marks the replaced transaction as doesExist=false
  2. Processes replacement transactions that might not exist yet in the payment store
  3. Waits for them via processOnchainPayment

Issue: There's a timing window where a replacement transaction event arrives before LDK updates the payment store. The code handles this (line 385-403) by processing the payment, but there's a potential race if the transaction hasn't been added to the payments list yet.

Recommendation: Consider adding retry logic or ensuring LDK guarantees payment store updates before emitting replacement events.

Moderate

3. Missing cache update in update() method (CoreService.swift:193-199)

func update(id:String, activity:Activity)asyncthrows{tryawaitServiceQueue.background(.core){tryupdateActivity(activityId: id, activity: activity)self.metadataChangedSubject.send()}}

This doesn't call updateBoostTxIdsCache(for: activity) like insert() does at line 172.

Recommendation: Add cache update to maintain consistency.

4. Incomplete validation in shouldShowReceivedSheet (CoreService.swift:76-107)

The method checks if a replacement transaction has the same value as the replaced transaction to avoid showing duplicate sheets. However:

  • It only checks exact value matches
  • Doesn't account for fee differences in RBF
  • Could show sheets for CPFP where the child transaction might have a different value

Recommendation: Consider checking isBoosted flag and the nature of the boost (RBF vs CPFP) for more precise logic.

5. Error handling in handleOnchainTransactionReplaced (CoreService.swift:355-441)

When processing replacement transactions, errors are caught and logged (line 401-405), but the loop continues. If multiple replacement transactions fail to process, the relationship tracking could be incomplete.

Recommendation: Consider collecting errors and either throwing or returning them to the caller for better error visibility.

6. CI workflow changes remove caching (integration-tests.yml, unit-tests.yml)

The PR removes DerivedData caching and restore-keys from SPM cache. While this ensures clean builds, it will increase build times significantly.

Recommendation:

  • Document why caching was removed (likely due to cache invalidation issues)
  • Consider using Xcode Cloud or other CI optimizations to offset the performance loss
  • The -onlyUsePackageVersionsFromResolvedFile flag is good for reproducibility

Minor

7. Potential N+1 query in getBoostTxDoesExist (CoreService.swift:130-138)

func getBoostTxDoesExist(boostTxIds:[String])async->[String:Bool]{vardoesExistMap:[String:Bool]=[:]forboostTxIdin boostTxIds {
if let boostActivity =try?awaitgetOnchainActivityByTxId(txid: boostTxId){doesExistMap[boostTxId]= boostActivity.doesExist
}}return doesExistMap
}

Issue: Makes individual database queries for each boost transaction.

Recommendation: Consider adding a batch query method to BitkitCore to fetch multiple activities by txId in one query.

8. Magic number delay in AppViewModel.swift:399

try?awaitTask.sleep(nanoseconds:500_000_000) // 500ms delay

Recommendation: Extract to a named constant with documentation explaining why the delay is necessary.

9. Missing test coverage

While TxBumpingTests.swift and ActivityListTest.swift exist, there don't appear to be specific tests for:

  • Onchain event handlers
  • RBF replacement tracking with boostTxIds
  • Reorg and eviction scenarios
  • Cache consistency

Recommendation: Add integration tests for the new event handlers.

🔒 Security

No major security concerns identified. The code properly:

  • Uses ServiceQueue for thread safety
  • Doesn't expose sensitive transaction data in logs beyond txids
  • Maintains Bitcoin/Lightning operation isolation in the service layer

Minor: Logging in LightningService.swift:711-757 includes transaction amounts. Consider whether this is acceptable for production logging.

🚀 Performance

Positive impacts:

  1. Eliminates redundant polling and full activity list syncs
  2. Event-driven updates only process changed transactions
  3. Cache for boostTxIds lookups

Potential concerns:

  1. Cache not updated consistently (see issue Project setup #1, Migrate channels from LDK to LDK-node #3)
  2. N+1 queries in getBoostTxDoesExist (see issue feat: keychain #7)

🧪 Testing

Existing test coverage:

  • ActivityListTest.swift: Good coverage of basic activity CRUD operations
  • TxBumpingTests.swift: Tests RBF scenarios

Missing:

  • Tests for new onchain event handlers
  • Tests for handleOnchainTransactionReplaced, handleOnchainTransactionReorged, handleOnchainTransactionEvicted
  • Tests for cache consistency
  • Integration tests for the complete event flow

Recommendation: Add integration tests that simulate the event sequences shown in the PR videos.

📝 Recommendations

Must Fix Before Merge

  1. Fix cache consistency issues (Issues Project setup #1, Migrate channels from LDK to LDK-node #3)
  2. Add error handling for race conditions in RBF handling (Issue Push service setup and service queues  #2)

Should Fix Before Merge

  1. Improve shouldShowReceivedSheet validation logic (Issue Create swift.yml #4)
  2. Better error aggregation in handleOnchainTransactionReplaced (Issue Create ios.yml #5)

Consider for Follow-up

  1. Add batch query support for getBoostTxDoesExist (Issue feat: keychain #7)
  2. Add integration tests for event handlers (Issue Use SwiftFormat #9)
  3. Extract magic constants (Issue BDK 1.0.0-alpha.11 #8)
  4. Document CI caching removal reasoning (Issue Create ios.yml #6)

🎯 Overall Assessment

This is a high-quality PR that represents a significant architectural improvement. The event-driven approach is the correct design, and the RBF handling logic is well thought out. The main concerns are around cache consistency and potential race conditions in RBF processing, which should be addressed before merge.

The removal of AddressChecker and polling-based syncing will improve app performance and responsiveness.

Recommendation: Request changes to address cache consistency and race condition handling, then approve after fixes.


Great work on this PR! The demos showing reorg and mempool eviction handling are particularly impressive. Once the cache and race condition issues are resolved, this will be a solid improvement to the codebase.

@ovitrifovitrif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

tAck

now it really shows the boosted TX update instantly every time

@pwltrpwltr linked an issue Nov 28, 2025 that may be closed by this pull request
@ovitrif

Copy link
Copy Markdown
Collaborator

Android PR should succeed e2e once Electrum on staging is fixed. Merging this.

1 similar comment
@ovitrif

Copy link
Copy Markdown
Collaborator

Android PR should succeed e2e once Electrum on staging is fixed. Merging this.

@ovitrif
ovitrif merged commit 4b25919 into masterNov 28, 2025
10 checks passed
@ovitrif
ovitrif deleted the feat/onchain-events branch November 28, 2025 23:14
BitcoinErrorLog pushed a commit to BitcoinErrorLog/bitkit-ios that referenced this pull request Dec 15, 2025
BitcoinErrorLog pushed a commit to BitcoinErrorLog/bitkit-ios that referenced this pull request Feb 7, 2026
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.

[Bug]: Polish RBF & CPFP activity list behaviour

6 participants

@ben-kaufman@ovitrif@piotr-iohk@pwltr@jvsena42
, '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

Integrate LDK onchain events - #245

Merged
ovitrif merged 20 commits into
masterfrom
feat/onchain-events
Nov 28, 2025
Merged

Integrate LDK onchain events#245
ovitrif merged 20 commits into
masterfrom
feat/onchain-events

Conversation

@ben-kaufman

@ben-kaufmanben-kaufman commented Nov 26, 2025

Copy link
Copy Markdown
Contributor

This PR integrates LDK node on chain events to replace the previous pollin and full syncs.

It also removes Address Checker, and improves handling on RBFed transactions to be recognized on both sides.

Testing:
Should test all actions, send, receive (onchain and LN), transfer to savings, transfer to spendings, etc.

Screen.Recording.2025-11-26.at.9.05.11.AM.mov

Reorg removes tx from blocks demo:

Screen.Recording.2025-11-26.at.5.13.45.PM.mov

Transaction removed from mempool demo:

Screen.Recording.2025-11-26.at.5.38.36.PM.mov

@claude

This comment was marked as outdated.

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 integrates LDK node onchain events to replace previous polling and full sync mechanisms for Bitcoin transaction management. It removes the AddressChecker utility class that relied on external API calls, and improves handling of RBF (Replace-By-Fee) transactions to properly distinguish them from CPFP (Child-Pays-For-Parent) transactions on both sender and receiver sides.

Key changes:

  • Event-driven architecture for onchain transaction updates (received, confirmed, replaced, reorged, evicted)
  • Removal of 1-second polling timer in favor of real-time LDK events
  • Enhanced RBF transaction detection using doesExist flag to differentiate from CPFP

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
BitkitNotification/NotificationService.swiftAdded notification handling for new onchain transaction events
Bitkit/Views/Wallets/Sheets/BoostSheet.swiftRemoved redundant LDK node payment sync after boost operation
Bitkit/Views/Wallets/Activity/ActivityItemView.swiftEnhanced boost button logic to differentiate between RBF and CPFP transactions
Bitkit/Views/Wallets/Activity/ActivityExplorerView.swiftReplaced AddressChecker API calls with LDK node transaction details
Bitkit/ViewModels/WalletViewModel.swiftRemoved polling mechanism and added event-driven state updates for onchain transactions
Bitkit/ViewModels/AppViewModel.swiftAdded comprehensive event handling for onchain transaction lifecycle
Bitkit/Utilities/AddressChecker.swiftRemoved external API-dependent utility in favor of native LDK functionality
Bitkit/Services/LightningService.swiftAdded transaction detail methods and event handlers for onchain events
Bitkit/Services/CoreService.swiftRefactored payment processing to handle LDK events and removed replacement tracking maps
Bitkit/AppScene.swiftRemoved balance change observer and event-based activity sync
Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolvedUpdated bitkit-core and ldk-node dependencies
Bitkit.xcodeproj/project.pbxprojUpdated project references for LDK package

Comment threadBitkit/Services/CoreService.swift
Comment threadBitkit/Services/CoreService.swift
Comment threadBitkit/Services/CoreService.swift
@claude

This comment was marked as outdated.

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

@piotr-iohk please note we will need to update the e2e tests to close the onchain received sheet.

@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

ovitrif
ovitrif previously approved these changes Nov 26, 2025

@ovitrifovitrif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

utAck

Amazing work, I've been waiting for over a year to finally have this setup in place: event-based sync for transactions 🎉

Added a few small nit comments, the one about the localizable strings applies to all cases where we added new toast messages, not only the code where I added it.

I wanted to give you the time to already make some small updates as I am starting to test this, hence me approving without testing; but I trust you'll wait for my testing 🙏🏻

Comment threadBitkit/ViewModels/AppViewModel.swift Outdated
Comment threadBitkitNotification/NotificationService.swift
@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

Comment threadBitkit/Resources/Localization/en.lproj/Localizable.strings Outdated
@claude

This comment was marked as outdated.

@piotr-iohk

Copy link
Copy Markdown
Collaborator

Ok, updated e2e tests for current state (synonymdev/bitkit-e2e-tests#56). Re-running 🤞
It would be good to coordinate merging with Android (synonymdev/bitkit-android#475)

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

@pwltr it's not due to race condition, it's just because the event of the new tx takes a few seconds to come in from the LDK node.

We can solve it by immediately caching the "intermediate" state in memory until the event comes in.

@pwltr

Copy link
Copy Markdown
Contributor

@pwltr it's not due to race condition, it's just because the event of the new tx takes a few seconds to come in from the LDK node.

We can solve it by immediately caching the "intermediate" state in memory until the event comes in.

Sounds good yeah I think an optimistic UI like this would be the way to go. Assume everything went right and correct if needed when the event(s) come in from LDK. Still feel free to postpone for now.

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

Actually had a simpler solution, fixed in: #247

pwltr
pwltr previously approved these changes Nov 27, 2025

@pwltrpwltr 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.

tACK with follow-up task here

Also note @piotr-iohk's request to merge this together with the Android counterpart.

@ben-kaufman
ben-kaufman dismissed stale reviews from pwltr, ovitrif, and jvsena42 via ab36651November 27, 2025 17:01
@claude

claudeBot commented Nov 27, 2025

Copy link
Copy Markdown
Contributor

PR Review: Integrate LDK onchain events

This is an excellent architectural improvement that modernizes the onchain transaction handling by leveraging LDK's event-driven architecture. The change from polling to event-driven updates should significantly improve performance and user experience.


Summary of Changes

Major Improvements:

  • Event-driven onchain transaction handling via LDK Node events
  • Removed AddressChecker utility (110 lines removed)
  • Enhanced RBF transaction tracking and handling
  • Added comprehensive onchain event handling (received, confirmed, replaced, reorged, evicted)
  • Improved user notifications for transaction state changes
  • Better handling of edge cases (reorgs, mempool evictions)

Impact:

  • 1,031 additions, 495 deletions across 20 files
  • Core architectural change in CoreService.swift (+529/-273)
  • Enhanced event handling in AppViewModel.swift (+108/-4)
  • Improved LightningService.swift integration (+120/-3)

Code Quality & Best Practices

Strengths

  1. Event-Driven Architecture - Proper use of LDK onchain events eliminates polling overhead
  2. Comprehensive Error Handling - Good use of try-catch blocks with descriptive logging
  3. Service Queue Pattern - Correctly uses ServiceQueue.background(.core) for Core operations
  4. MainActor Compliance - Proper async/await patterns with @mainactor for UI updates
  5. Caching Strategy - cachedTxIdsInBoostTxIds improves performance for boost transaction lookups
  6. Detailed Logging - Excellent context-aware logging throughout
  7. Accessibility - Added accessibilityIdentifier to Toast model and components

Code Organization

The new event handlers follow a clean pattern:

  • handleOnchainTransactionReceived
  • handleOnchainTransactionConfirmed
  • handleOnchainTransactionReplaced
  • handleOnchainTransactionReorged
  • handleOnchainTransactionEvicted

Each handler properly delegates to ServiceQueue.background(.core) and updates the activity list appropriately.


Potential Bugs & Issues

1. Race Condition in handleOnchainTransactionReplaced

Location: CoreService.swift:346-433

Issue: When processing replacement transactions, there is a potential race condition between checking if replacementActivity exists and processing the payment. Another event handler could process the same transaction concurrently.

Recommendation: Add a lock or transaction ID tracking to prevent concurrent processing of the same transaction.

2. Cache Invalidation Issue

Location: CoreService.swift:34-62

Issue: The cachedTxIdsInBoostTxIds cache is updated in insert and bulk refreshed in upsertList, but individual update operations don't update the cache. This could lead to stale cache data.

Recommendation: Add cache update to the update method to keep cache consistent.

3. Potential Memory Leak in Event Handlers

Location: AppScene.swift:213-215

Issue: The event handler closure captures app weakly, but the closure itself is never removed when the scene is dismissed.

Recommendation: Ensure event handlers are removed when the scene is dismissed. The addOnEvent API should support removal, or use a cleanup mechanism in deinit or onDisappear.

4. Missing nil-check in processOnchainTransaction

Location: CoreService.swift:315-332

Issue: The function logs a warning if payment is not found but doesn't throw an error. The calling code may not handle this gracefully.

Recommendation: Consider throwing a specific error so callers can decide how to handle missing payments, or add metrics to track how often this occurs.


Performance Considerations

Strengths

  1. Eliminated Polling - Removing AddressChecker and full syncs significantly reduces CPU/network overhead
  2. Efficient Caching - cachedTxIdsInBoostTxIds prevents repeated database queries
  3. Lazy Cache Initialization - Cache is only built when first accessed

Concerns

  1. Cache Refresh Performance - CoreService.swift:47-62 loads ALL onchain activities from the database, which could be slow for users with large transaction histories (1000+ transactions). Consider a more efficient query that only fetches boostTxIds without loading full activity objects.

  2. Delay in Received Sheet - AppViewModel.swift:399 has a 500ms delay. Is this necessary? It could make the UI feel sluggish. Consider removing or reducing if not strictly required.


Security Concerns

Good Practices

  1. Proper use of ServiceQueue prevents concurrent access to Core/LDK
  2. Transactions marked as doesExist = false when replaced (prevents double-counting)
  3. RBF detection prevents showing duplicate received sheets

Recommendations

  1. Transaction Validation - Consider adding validation that conflicts array in handleOnchainTransactionReplaced actually contains valid transaction IDs to prevent malformed data corruption.

  2. Timestamp Validation - In processOnchainPayment, ensure timestamps are within reasonable ranges to prevent time-based attacks.


Test Coverage

Missing Test Scenarios

  1. RBF Handling - Need tests for single replacement, multiple conflicts, chain of replacements, replacement arriving before original

  2. Reorg Handling - Need tests for confirmed tx becoming unconfirmed, multiple txs affected by reorg, reorg depth > 1 block

  3. Edge Cases - Need tests for tx evicted from mempool that was not replaced, race conditions, cache invalidation scenarios

Testing Recommendations

Add integration tests that simulate these event sequences:

  • onchainTransactionReceived → onchainTransactionReplaced → onchainTransactionConfirmed
  • onchainTransactionConfirmed → onchainTransactionReorged
  • onchainTransactionReceived → onchainTransactionEvicted

Final Recommendations

Must Fix (Before Merge)

  1. Run swiftformat . to ensure code formatting compliance
  2. Fix cache invalidation in update method
  3. Add race condition protection in handleOnchainTransactionReplaced
  4. Verify event handler cleanup to prevent memory leaks

Should Fix (High Priority)

  1. Optimize refreshBoostTxIdsCache for large transaction histories
  2. Add comprehensive tests for RBF and reorg scenarios
  3. Review the 500ms delay in received sheet display

Nice to Have

  1. Add transaction ID truncation to user-facing messages
  2. Improve user-friendly language for technical events (reorg, eviction)
  3. Add metrics/telemetry for tracking event frequencies

Conclusion

This is a high-quality PR that represents a significant architectural improvement. The event-driven approach is the correct design pattern and will eliminate polling overhead. The code quality is generally excellent with proper error handling, logging, and async patterns.

Recommendation:Approve with minor changes. Address the cache invalidation and race condition issues before merge, and plan follow-up work for comprehensive testing and performance optimization.

Great work! The videos demonstrating reorg and eviction handling are particularly impressive.

pwltr
pwltr previously approved these changes Nov 27, 2025
@ovitrif
ovitrif enabled auto-merge November 27, 2025 18:26
@ovitrif
ovitrif disabled auto-merge November 27, 2025 18:26
ovitrif
ovitrif previously approved these changes Nov 27, 2025

@ovitrifovitrif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

tAck

now it really shows the boosted TX update instantly every time I try it

@ovitrif
ovitrif dismissed stale reviews from pwltr and themself via 4b9083dNovember 27, 2025 22:39
@claude

claudeBot commented Nov 27, 2025

Copy link
Copy Markdown
Contributor

PR Review: Integrate LDK onchain events

Summary

This PR successfully replaces polling-based transaction monitoring with event-driven LDK onchain events, removes the AddressChecker, and improves RBF transaction handling. This is a significant architectural improvement that makes the app more reactive and efficient.

✅ Strengths

Architecture & Design

  1. Event-driven architecture: The transition from polling to LDK events (onchainTransactionReceived, onchainTransactionConfirmed, onchainTransactionReplaced, onchainTransactionReorged, onchainTransactionEvicted) is excellent. This provides real-time updates and reduces unnecessary syncs.

  2. Comprehensive RBF handling: The implementation properly tracks replacement transactions through boostTxIds and distinguishes between RBF (replaced transactions with doesExist=false) and CPFP (child transactions with doesExist=true).

  3. Clean separation of concerns: Event handlers in LightningService.swift (708-757) delegate to ActivityService methods, maintaining good separation between Lightning layer and activity management.

  4. Improved user feedback: New toast notifications with accessibility identifiers for transaction states (replaced, reorged, evicted) provide better UX.

  5. Removal of polling: Removing the onChange(of: wallet.totalBalanceSats) trigger in AppScene.swift:217 and the event-based sync in AppScene.swift:217-220 eliminates redundant activity syncing.

Code Quality

  1. Good error handling: All event handlers wrap operations in try-catch blocks with proper logging.
  2. Proper concurrency: Uses ServiceQueue.background(.core) for all database operations.
  3. Cache optimization: The cachedTxIdsInBoostTxIds cache improves performance for checking replaced transactions.

⚠️ Issues & Concerns

Critical

1. Potential cache inconsistency in CoreService.swift:36-43

func getTxIdsInBoostTxIds()async->Set<String>{if cachedTxIdsInBoostTxIds.isEmpty {awaitrefreshBoostTxIdsCache()}return cachedTxIdsInBoostTxIds
}

Issue: The cache is only refreshed when empty, but individual updates via updateBoostTxIdsCache(for:) might not be called for all updates. The update() method at line 195 doesn't update the cache.

Recommendation:

  • Update the cache in the update() method, or
  • Implement a TTL-based cache invalidation strategy, or
  • Document why the cache only needs to be refreshed when empty

2. Race condition in RBF handling (CoreService.swift:355-441)

When handleOnchainTransactionReplaced is called, it:

  1. Marks the replaced transaction as doesExist=false
  2. Processes replacement transactions that might not exist yet in the payment store
  3. Waits for them via processOnchainPayment

Issue: There's a timing window where a replacement transaction event arrives before LDK updates the payment store. The code handles this (line 385-403) by processing the payment, but there's a potential race if the transaction hasn't been added to the payments list yet.

Recommendation: Consider adding retry logic or ensuring LDK guarantees payment store updates before emitting replacement events.

Moderate

3. Missing cache update in update() method (CoreService.swift:193-199)

func update(id:String, activity:Activity)asyncthrows{tryawaitServiceQueue.background(.core){tryupdateActivity(activityId: id, activity: activity)self.metadataChangedSubject.send()}}

This doesn't call updateBoostTxIdsCache(for: activity) like insert() does at line 172.

Recommendation: Add cache update to maintain consistency.

4. Incomplete validation in shouldShowReceivedSheet (CoreService.swift:76-107)

The method checks if a replacement transaction has the same value as the replaced transaction to avoid showing duplicate sheets. However:

  • It only checks exact value matches
  • Doesn't account for fee differences in RBF
  • Could show sheets for CPFP where the child transaction might have a different value

Recommendation: Consider checking isBoosted flag and the nature of the boost (RBF vs CPFP) for more precise logic.

5. Error handling in handleOnchainTransactionReplaced (CoreService.swift:355-441)

When processing replacement transactions, errors are caught and logged (line 401-405), but the loop continues. If multiple replacement transactions fail to process, the relationship tracking could be incomplete.

Recommendation: Consider collecting errors and either throwing or returning them to the caller for better error visibility.

6. CI workflow changes remove caching (integration-tests.yml, unit-tests.yml)

The PR removes DerivedData caching and restore-keys from SPM cache. While this ensures clean builds, it will increase build times significantly.

Recommendation:

  • Document why caching was removed (likely due to cache invalidation issues)
  • Consider using Xcode Cloud or other CI optimizations to offset the performance loss
  • The -onlyUsePackageVersionsFromResolvedFile flag is good for reproducibility

Minor

7. Potential N+1 query in getBoostTxDoesExist (CoreService.swift:130-138)

func getBoostTxDoesExist(boostTxIds:[String])async->[String:Bool]{vardoesExistMap:[String:Bool]=[:]forboostTxIdin boostTxIds {
if let boostActivity =try?awaitgetOnchainActivityByTxId(txid: boostTxId){doesExistMap[boostTxId]= boostActivity.doesExist
}}return doesExistMap
}

Issue: Makes individual database queries for each boost transaction.

Recommendation: Consider adding a batch query method to BitkitCore to fetch multiple activities by txId in one query.

8. Magic number delay in AppViewModel.swift:399

try?awaitTask.sleep(nanoseconds:500_000_000) // 500ms delay

Recommendation: Extract to a named constant with documentation explaining why the delay is necessary.

9. Missing test coverage

While TxBumpingTests.swift and ActivityListTest.swift exist, there don't appear to be specific tests for:

  • Onchain event handlers
  • RBF replacement tracking with boostTxIds
  • Reorg and eviction scenarios
  • Cache consistency

Recommendation: Add integration tests for the new event handlers.

🔒 Security

No major security concerns identified. The code properly:

  • Uses ServiceQueue for thread safety
  • Doesn't expose sensitive transaction data in logs beyond txids
  • Maintains Bitcoin/Lightning operation isolation in the service layer

Minor: Logging in LightningService.swift:711-757 includes transaction amounts. Consider whether this is acceptable for production logging.

🚀 Performance

Positive impacts:

  1. Eliminates redundant polling and full activity list syncs
  2. Event-driven updates only process changed transactions
  3. Cache for boostTxIds lookups

Potential concerns:

  1. Cache not updated consistently (see issue Project setup #1, Migrate channels from LDK to LDK-node #3)
  2. N+1 queries in getBoostTxDoesExist (see issue feat: keychain #7)

🧪 Testing

Existing test coverage:

  • ActivityListTest.swift: Good coverage of basic activity CRUD operations
  • TxBumpingTests.swift: Tests RBF scenarios

Missing:

  • Tests for new onchain event handlers
  • Tests for handleOnchainTransactionReplaced, handleOnchainTransactionReorged, handleOnchainTransactionEvicted
  • Tests for cache consistency
  • Integration tests for the complete event flow

Recommendation: Add integration tests that simulate the event sequences shown in the PR videos.

📝 Recommendations

Must Fix Before Merge

  1. Fix cache consistency issues (Issues Project setup #1, Migrate channels from LDK to LDK-node #3)
  2. Add error handling for race conditions in RBF handling (Issue Push service setup and service queues  #2)

Should Fix Before Merge

  1. Improve shouldShowReceivedSheet validation logic (Issue Create swift.yml #4)
  2. Better error aggregation in handleOnchainTransactionReplaced (Issue Create ios.yml #5)

Consider for Follow-up

  1. Add batch query support for getBoostTxDoesExist (Issue feat: keychain #7)
  2. Add integration tests for event handlers (Issue Use SwiftFormat #9)
  3. Extract magic constants (Issue BDK 1.0.0-alpha.11 #8)
  4. Document CI caching removal reasoning (Issue Create ios.yml #6)

🎯 Overall Assessment

This is a high-quality PR that represents a significant architectural improvement. The event-driven approach is the correct design, and the RBF handling logic is well thought out. The main concerns are around cache consistency and potential race conditions in RBF processing, which should be addressed before merge.

The removal of AddressChecker and polling-based syncing will improve app performance and responsiveness.

Recommendation: Request changes to address cache consistency and race condition handling, then approve after fixes.


Great work on this PR! The demos showing reorg and mempool eviction handling are particularly impressive. Once the cache and race condition issues are resolved, this will be a solid improvement to the codebase.

@ovitrifovitrif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

tAck

now it really shows the boosted TX update instantly every time

@pwltrpwltr linked an issue Nov 28, 2025 that may be closed by this pull request
@ovitrif

Copy link
Copy Markdown
Collaborator

Android PR should succeed e2e once Electrum on staging is fixed. Merging this.

1 similar comment
@ovitrif

Copy link
Copy Markdown
Collaborator

Android PR should succeed e2e once Electrum on staging is fixed. Merging this.

@ovitrif
ovitrif merged commit 4b25919 into masterNov 28, 2025
10 checks passed
@ovitrif
ovitrif deleted the feat/onchain-events branch November 28, 2025 23:14
BitcoinErrorLog pushed a commit to BitcoinErrorLog/bitkit-ios that referenced this pull request Dec 15, 2025
BitcoinErrorLog pushed a commit to BitcoinErrorLog/bitkit-ios that referenced this pull request Feb 7, 2026
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.

[Bug]: Polish RBF & CPFP activity list behaviour

6 participants

@ben-kaufman@ovitrif@piotr-iohk@pwltr@jvsena42
, '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

Integrate LDK onchain events - #245

Merged
ovitrif merged 20 commits into
masterfrom
feat/onchain-events
Nov 28, 2025
Merged

Integrate LDK onchain events#245
ovitrif merged 20 commits into
masterfrom
feat/onchain-events

Conversation

@ben-kaufman

@ben-kaufmanben-kaufman commented Nov 26, 2025

Copy link
Copy Markdown
Contributor

This PR integrates LDK node on chain events to replace the previous pollin and full syncs.

It also removes Address Checker, and improves handling on RBFed transactions to be recognized on both sides.

Testing:
Should test all actions, send, receive (onchain and LN), transfer to savings, transfer to spendings, etc.

Screen.Recording.2025-11-26.at.9.05.11.AM.mov

Reorg removes tx from blocks demo:

Screen.Recording.2025-11-26.at.5.13.45.PM.mov

Transaction removed from mempool demo:

Screen.Recording.2025-11-26.at.5.38.36.PM.mov

@claude

This comment was marked as outdated.

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 integrates LDK node onchain events to replace previous polling and full sync mechanisms for Bitcoin transaction management. It removes the AddressChecker utility class that relied on external API calls, and improves handling of RBF (Replace-By-Fee) transactions to properly distinguish them from CPFP (Child-Pays-For-Parent) transactions on both sender and receiver sides.

Key changes:

  • Event-driven architecture for onchain transaction updates (received, confirmed, replaced, reorged, evicted)
  • Removal of 1-second polling timer in favor of real-time LDK events
  • Enhanced RBF transaction detection using doesExist flag to differentiate from CPFP

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
BitkitNotification/NotificationService.swiftAdded notification handling for new onchain transaction events
Bitkit/Views/Wallets/Sheets/BoostSheet.swiftRemoved redundant LDK node payment sync after boost operation
Bitkit/Views/Wallets/Activity/ActivityItemView.swiftEnhanced boost button logic to differentiate between RBF and CPFP transactions
Bitkit/Views/Wallets/Activity/ActivityExplorerView.swiftReplaced AddressChecker API calls with LDK node transaction details
Bitkit/ViewModels/WalletViewModel.swiftRemoved polling mechanism and added event-driven state updates for onchain transactions
Bitkit/ViewModels/AppViewModel.swiftAdded comprehensive event handling for onchain transaction lifecycle
Bitkit/Utilities/AddressChecker.swiftRemoved external API-dependent utility in favor of native LDK functionality
Bitkit/Services/LightningService.swiftAdded transaction detail methods and event handlers for onchain events
Bitkit/Services/CoreService.swiftRefactored payment processing to handle LDK events and removed replacement tracking maps
Bitkit/AppScene.swiftRemoved balance change observer and event-based activity sync
Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolvedUpdated bitkit-core and ldk-node dependencies
Bitkit.xcodeproj/project.pbxprojUpdated project references for LDK package

Comment threadBitkit/Services/CoreService.swift
Comment threadBitkit/Services/CoreService.swift
Comment threadBitkit/Services/CoreService.swift
@claude

This comment was marked as outdated.

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

@piotr-iohk please note we will need to update the e2e tests to close the onchain received sheet.

@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

ovitrif
ovitrif previously approved these changes Nov 26, 2025

@ovitrifovitrif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

utAck

Amazing work, I've been waiting for over a year to finally have this setup in place: event-based sync for transactions 🎉

Added a few small nit comments, the one about the localizable strings applies to all cases where we added new toast messages, not only the code where I added it.

I wanted to give you the time to already make some small updates as I am starting to test this, hence me approving without testing; but I trust you'll wait for my testing 🙏🏻

Comment threadBitkit/ViewModels/AppViewModel.swift Outdated
Comment threadBitkitNotification/NotificationService.swift
@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

Comment threadBitkit/Resources/Localization/en.lproj/Localizable.strings Outdated
@claude

This comment was marked as outdated.

@piotr-iohk

Copy link
Copy Markdown
Collaborator

Ok, updated e2e tests for current state (synonymdev/bitkit-e2e-tests#56). Re-running 🤞
It would be good to coordinate merging with Android (synonymdev/bitkit-android#475)

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

@pwltr it's not due to race condition, it's just because the event of the new tx takes a few seconds to come in from the LDK node.

We can solve it by immediately caching the "intermediate" state in memory until the event comes in.

@pwltr

Copy link
Copy Markdown
Contributor

@pwltr it's not due to race condition, it's just because the event of the new tx takes a few seconds to come in from the LDK node.

We can solve it by immediately caching the "intermediate" state in memory until the event comes in.

Sounds good yeah I think an optimistic UI like this would be the way to go. Assume everything went right and correct if needed when the event(s) come in from LDK. Still feel free to postpone for now.

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

Actually had a simpler solution, fixed in: #247

pwltr
pwltr previously approved these changes Nov 27, 2025

@pwltrpwltr 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.

tACK with follow-up task here

Also note @piotr-iohk's request to merge this together with the Android counterpart.

@ben-kaufman
ben-kaufman dismissed stale reviews from pwltr, ovitrif, and jvsena42 via ab36651November 27, 2025 17:01
@claude

claudeBot commented Nov 27, 2025

Copy link
Copy Markdown
Contributor

PR Review: Integrate LDK onchain events

This is an excellent architectural improvement that modernizes the onchain transaction handling by leveraging LDK's event-driven architecture. The change from polling to event-driven updates should significantly improve performance and user experience.


Summary of Changes

Major Improvements:

  • Event-driven onchain transaction handling via LDK Node events
  • Removed AddressChecker utility (110 lines removed)
  • Enhanced RBF transaction tracking and handling
  • Added comprehensive onchain event handling (received, confirmed, replaced, reorged, evicted)
  • Improved user notifications for transaction state changes
  • Better handling of edge cases (reorgs, mempool evictions)

Impact:

  • 1,031 additions, 495 deletions across 20 files
  • Core architectural change in CoreService.swift (+529/-273)
  • Enhanced event handling in AppViewModel.swift (+108/-4)
  • Improved LightningService.swift integration (+120/-3)

Code Quality & Best Practices

Strengths

  1. Event-Driven Architecture - Proper use of LDK onchain events eliminates polling overhead
  2. Comprehensive Error Handling - Good use of try-catch blocks with descriptive logging
  3. Service Queue Pattern - Correctly uses ServiceQueue.background(.core) for Core operations
  4. MainActor Compliance - Proper async/await patterns with @mainactor for UI updates
  5. Caching Strategy - cachedTxIdsInBoostTxIds improves performance for boost transaction lookups
  6. Detailed Logging - Excellent context-aware logging throughout
  7. Accessibility - Added accessibilityIdentifier to Toast model and components

Code Organization

The new event handlers follow a clean pattern:

  • handleOnchainTransactionReceived
  • handleOnchainTransactionConfirmed
  • handleOnchainTransactionReplaced
  • handleOnchainTransactionReorged
  • handleOnchainTransactionEvicted

Each handler properly delegates to ServiceQueue.background(.core) and updates the activity list appropriately.


Potential Bugs & Issues

1. Race Condition in handleOnchainTransactionReplaced

Location: CoreService.swift:346-433

Issue: When processing replacement transactions, there is a potential race condition between checking if replacementActivity exists and processing the payment. Another event handler could process the same transaction concurrently.

Recommendation: Add a lock or transaction ID tracking to prevent concurrent processing of the same transaction.

2. Cache Invalidation Issue

Location: CoreService.swift:34-62

Issue: The cachedTxIdsInBoostTxIds cache is updated in insert and bulk refreshed in upsertList, but individual update operations don't update the cache. This could lead to stale cache data.

Recommendation: Add cache update to the update method to keep cache consistent.

3. Potential Memory Leak in Event Handlers

Location: AppScene.swift:213-215

Issue: The event handler closure captures app weakly, but the closure itself is never removed when the scene is dismissed.

Recommendation: Ensure event handlers are removed when the scene is dismissed. The addOnEvent API should support removal, or use a cleanup mechanism in deinit or onDisappear.

4. Missing nil-check in processOnchainTransaction

Location: CoreService.swift:315-332

Issue: The function logs a warning if payment is not found but doesn't throw an error. The calling code may not handle this gracefully.

Recommendation: Consider throwing a specific error so callers can decide how to handle missing payments, or add metrics to track how often this occurs.


Performance Considerations

Strengths

  1. Eliminated Polling - Removing AddressChecker and full syncs significantly reduces CPU/network overhead
  2. Efficient Caching - cachedTxIdsInBoostTxIds prevents repeated database queries
  3. Lazy Cache Initialization - Cache is only built when first accessed

Concerns

  1. Cache Refresh Performance - CoreService.swift:47-62 loads ALL onchain activities from the database, which could be slow for users with large transaction histories (1000+ transactions). Consider a more efficient query that only fetches boostTxIds without loading full activity objects.

  2. Delay in Received Sheet - AppViewModel.swift:399 has a 500ms delay. Is this necessary? It could make the UI feel sluggish. Consider removing or reducing if not strictly required.


Security Concerns

Good Practices

  1. Proper use of ServiceQueue prevents concurrent access to Core/LDK
  2. Transactions marked as doesExist = false when replaced (prevents double-counting)
  3. RBF detection prevents showing duplicate received sheets

Recommendations

  1. Transaction Validation - Consider adding validation that conflicts array in handleOnchainTransactionReplaced actually contains valid transaction IDs to prevent malformed data corruption.

  2. Timestamp Validation - In processOnchainPayment, ensure timestamps are within reasonable ranges to prevent time-based attacks.


Test Coverage

Missing Test Scenarios

  1. RBF Handling - Need tests for single replacement, multiple conflicts, chain of replacements, replacement arriving before original

  2. Reorg Handling - Need tests for confirmed tx becoming unconfirmed, multiple txs affected by reorg, reorg depth > 1 block

  3. Edge Cases - Need tests for tx evicted from mempool that was not replaced, race conditions, cache invalidation scenarios

Testing Recommendations

Add integration tests that simulate these event sequences:

  • onchainTransactionReceived → onchainTransactionReplaced → onchainTransactionConfirmed
  • onchainTransactionConfirmed → onchainTransactionReorged
  • onchainTransactionReceived → onchainTransactionEvicted

Final Recommendations

Must Fix (Before Merge)

  1. Run swiftformat . to ensure code formatting compliance
  2. Fix cache invalidation in update method
  3. Add race condition protection in handleOnchainTransactionReplaced
  4. Verify event handler cleanup to prevent memory leaks

Should Fix (High Priority)

  1. Optimize refreshBoostTxIdsCache for large transaction histories
  2. Add comprehensive tests for RBF and reorg scenarios
  3. Review the 500ms delay in received sheet display

Nice to Have

  1. Add transaction ID truncation to user-facing messages
  2. Improve user-friendly language for technical events (reorg, eviction)
  3. Add metrics/telemetry for tracking event frequencies

Conclusion

This is a high-quality PR that represents a significant architectural improvement. The event-driven approach is the correct design pattern and will eliminate polling overhead. The code quality is generally excellent with proper error handling, logging, and async patterns.

Recommendation:Approve with minor changes. Address the cache invalidation and race condition issues before merge, and plan follow-up work for comprehensive testing and performance optimization.

Great work! The videos demonstrating reorg and eviction handling are particularly impressive.

pwltr
pwltr previously approved these changes Nov 27, 2025
@ovitrif
ovitrif enabled auto-merge November 27, 2025 18:26
@ovitrif
ovitrif disabled auto-merge November 27, 2025 18:26
ovitrif
ovitrif previously approved these changes Nov 27, 2025

@ovitrifovitrif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

tAck

now it really shows the boosted TX update instantly every time I try it

@ovitrif
ovitrif dismissed stale reviews from pwltr and themself via 4b9083dNovember 27, 2025 22:39
@claude

claudeBot commented Nov 27, 2025

Copy link
Copy Markdown
Contributor

PR Review: Integrate LDK onchain events

Summary

This PR successfully replaces polling-based transaction monitoring with event-driven LDK onchain events, removes the AddressChecker, and improves RBF transaction handling. This is a significant architectural improvement that makes the app more reactive and efficient.

✅ Strengths

Architecture & Design

  1. Event-driven architecture: The transition from polling to LDK events (onchainTransactionReceived, onchainTransactionConfirmed, onchainTransactionReplaced, onchainTransactionReorged, onchainTransactionEvicted) is excellent. This provides real-time updates and reduces unnecessary syncs.

  2. Comprehensive RBF handling: The implementation properly tracks replacement transactions through boostTxIds and distinguishes between RBF (replaced transactions with doesExist=false) and CPFP (child transactions with doesExist=true).

  3. Clean separation of concerns: Event handlers in LightningService.swift (708-757) delegate to ActivityService methods, maintaining good separation between Lightning layer and activity management.

  4. Improved user feedback: New toast notifications with accessibility identifiers for transaction states (replaced, reorged, evicted) provide better UX.

  5. Removal of polling: Removing the onChange(of: wallet.totalBalanceSats) trigger in AppScene.swift:217 and the event-based sync in AppScene.swift:217-220 eliminates redundant activity syncing.

Code Quality

  1. Good error handling: All event handlers wrap operations in try-catch blocks with proper logging.
  2. Proper concurrency: Uses ServiceQueue.background(.core) for all database operations.
  3. Cache optimization: The cachedTxIdsInBoostTxIds cache improves performance for checking replaced transactions.

⚠️ Issues & Concerns

Critical

1. Potential cache inconsistency in CoreService.swift:36-43

func getTxIdsInBoostTxIds()async->Set<String>{if cachedTxIdsInBoostTxIds.isEmpty {awaitrefreshBoostTxIdsCache()}return cachedTxIdsInBoostTxIds
}

Issue: The cache is only refreshed when empty, but individual updates via updateBoostTxIdsCache(for:) might not be called for all updates. The update() method at line 195 doesn't update the cache.

Recommendation:

  • Update the cache in the update() method, or
  • Implement a TTL-based cache invalidation strategy, or
  • Document why the cache only needs to be refreshed when empty

2. Race condition in RBF handling (CoreService.swift:355-441)

When handleOnchainTransactionReplaced is called, it:

  1. Marks the replaced transaction as doesExist=false
  2. Processes replacement transactions that might not exist yet in the payment store
  3. Waits for them via processOnchainPayment

Issue: There's a timing window where a replacement transaction event arrives before LDK updates the payment store. The code handles this (line 385-403) by processing the payment, but there's a potential race if the transaction hasn't been added to the payments list yet.

Recommendation: Consider adding retry logic or ensuring LDK guarantees payment store updates before emitting replacement events.

Moderate

3. Missing cache update in update() method (CoreService.swift:193-199)

func update(id:String, activity:Activity)asyncthrows{tryawaitServiceQueue.background(.core){tryupdateActivity(activityId: id, activity: activity)self.metadataChangedSubject.send()}}

This doesn't call updateBoostTxIdsCache(for: activity) like insert() does at line 172.

Recommendation: Add cache update to maintain consistency.

4. Incomplete validation in shouldShowReceivedSheet (CoreService.swift:76-107)

The method checks if a replacement transaction has the same value as the replaced transaction to avoid showing duplicate sheets. However:

  • It only checks exact value matches
  • Doesn't account for fee differences in RBF
  • Could show sheets for CPFP where the child transaction might have a different value

Recommendation: Consider checking isBoosted flag and the nature of the boost (RBF vs CPFP) for more precise logic.

5. Error handling in handleOnchainTransactionReplaced (CoreService.swift:355-441)

When processing replacement transactions, errors are caught and logged (line 401-405), but the loop continues. If multiple replacement transactions fail to process, the relationship tracking could be incomplete.

Recommendation: Consider collecting errors and either throwing or returning them to the caller for better error visibility.

6. CI workflow changes remove caching (integration-tests.yml, unit-tests.yml)

The PR removes DerivedData caching and restore-keys from SPM cache. While this ensures clean builds, it will increase build times significantly.

Recommendation:

  • Document why caching was removed (likely due to cache invalidation issues)
  • Consider using Xcode Cloud or other CI optimizations to offset the performance loss
  • The -onlyUsePackageVersionsFromResolvedFile flag is good for reproducibility

Minor

7. Potential N+1 query in getBoostTxDoesExist (CoreService.swift:130-138)

func getBoostTxDoesExist(boostTxIds:[String])async->[String:Bool]{vardoesExistMap:[String:Bool]=[:]forboostTxIdin boostTxIds {
if let boostActivity =try?awaitgetOnchainActivityByTxId(txid: boostTxId){doesExistMap[boostTxId]= boostActivity.doesExist
}}return doesExistMap
}

Issue: Makes individual database queries for each boost transaction.

Recommendation: Consider adding a batch query method to BitkitCore to fetch multiple activities by txId in one query.

8. Magic number delay in AppViewModel.swift:399

try?awaitTask.sleep(nanoseconds:500_000_000) // 500ms delay

Recommendation: Extract to a named constant with documentation explaining why the delay is necessary.

9. Missing test coverage

While TxBumpingTests.swift and ActivityListTest.swift exist, there don't appear to be specific tests for:

  • Onchain event handlers
  • RBF replacement tracking with boostTxIds
  • Reorg and eviction scenarios
  • Cache consistency

Recommendation: Add integration tests for the new event handlers.

🔒 Security

No major security concerns identified. The code properly:

  • Uses ServiceQueue for thread safety
  • Doesn't expose sensitive transaction data in logs beyond txids
  • Maintains Bitcoin/Lightning operation isolation in the service layer

Minor: Logging in LightningService.swift:711-757 includes transaction amounts. Consider whether this is acceptable for production logging.

🚀 Performance

Positive impacts:

  1. Eliminates redundant polling and full activity list syncs
  2. Event-driven updates only process changed transactions
  3. Cache for boostTxIds lookups

Potential concerns:

  1. Cache not updated consistently (see issue Project setup #1, Migrate channels from LDK to LDK-node #3)
  2. N+1 queries in getBoostTxDoesExist (see issue feat: keychain #7)

🧪 Testing

Existing test coverage:

  • ActivityListTest.swift: Good coverage of basic activity CRUD operations
  • TxBumpingTests.swift: Tests RBF scenarios

Missing:

  • Tests for new onchain event handlers
  • Tests for handleOnchainTransactionReplaced, handleOnchainTransactionReorged, handleOnchainTransactionEvicted
  • Tests for cache consistency
  • Integration tests for the complete event flow

Recommendation: Add integration tests that simulate the event sequences shown in the PR videos.

📝 Recommendations

Must Fix Before Merge

  1. Fix cache consistency issues (Issues Project setup #1, Migrate channels from LDK to LDK-node #3)
  2. Add error handling for race conditions in RBF handling (Issue Push service setup and service queues  #2)

Should Fix Before Merge

  1. Improve shouldShowReceivedSheet validation logic (Issue Create swift.yml #4)
  2. Better error aggregation in handleOnchainTransactionReplaced (Issue Create ios.yml #5)

Consider for Follow-up

  1. Add batch query support for getBoostTxDoesExist (Issue feat: keychain #7)
  2. Add integration tests for event handlers (Issue Use SwiftFormat #9)
  3. Extract magic constants (Issue BDK 1.0.0-alpha.11 #8)
  4. Document CI caching removal reasoning (Issue Create ios.yml #6)

🎯 Overall Assessment

This is a high-quality PR that represents a significant architectural improvement. The event-driven approach is the correct design, and the RBF handling logic is well thought out. The main concerns are around cache consistency and potential race conditions in RBF processing, which should be addressed before merge.

The removal of AddressChecker and polling-based syncing will improve app performance and responsiveness.

Recommendation: Request changes to address cache consistency and race condition handling, then approve after fixes.


Great work on this PR! The demos showing reorg and mempool eviction handling are particularly impressive. Once the cache and race condition issues are resolved, this will be a solid improvement to the codebase.

@ovitrifovitrif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

tAck

now it really shows the boosted TX update instantly every time

@pwltrpwltr linked an issue Nov 28, 2025 that may be closed by this pull request
@ovitrif

Copy link
Copy Markdown
Collaborator

Android PR should succeed e2e once Electrum on staging is fixed. Merging this.

1 similar comment
@ovitrif

Copy link
Copy Markdown
Collaborator

Android PR should succeed e2e once Electrum on staging is fixed. Merging this.

@ovitrif
ovitrif merged commit 4b25919 into masterNov 28, 2025
10 checks passed
@ovitrif
ovitrif deleted the feat/onchain-events branch November 28, 2025 23:14
BitcoinErrorLog pushed a commit to BitcoinErrorLog/bitkit-ios that referenced this pull request Dec 15, 2025
BitcoinErrorLog pushed a commit to BitcoinErrorLog/bitkit-ios that referenced this pull request Feb 7, 2026
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.

[Bug]: Polish RBF & CPFP activity list behaviour

6 participants

@ben-kaufman@ovitrif@piotr-iohk@pwltr@jvsena42
, '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

Integrate LDK onchain events - #245

Merged
ovitrif merged 20 commits into
masterfrom
feat/onchain-events
Nov 28, 2025
Merged

Integrate LDK onchain events#245
ovitrif merged 20 commits into
masterfrom
feat/onchain-events

Conversation

@ben-kaufman

@ben-kaufmanben-kaufman commented Nov 26, 2025

Copy link
Copy Markdown
Contributor

This PR integrates LDK node on chain events to replace the previous pollin and full syncs.

It also removes Address Checker, and improves handling on RBFed transactions to be recognized on both sides.

Testing:
Should test all actions, send, receive (onchain and LN), transfer to savings, transfer to spendings, etc.

Screen.Recording.2025-11-26.at.9.05.11.AM.mov

Reorg removes tx from blocks demo:

Screen.Recording.2025-11-26.at.5.13.45.PM.mov

Transaction removed from mempool demo:

Screen.Recording.2025-11-26.at.5.38.36.PM.mov

@claude

This comment was marked as outdated.

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 integrates LDK node onchain events to replace previous polling and full sync mechanisms for Bitcoin transaction management. It removes the AddressChecker utility class that relied on external API calls, and improves handling of RBF (Replace-By-Fee) transactions to properly distinguish them from CPFP (Child-Pays-For-Parent) transactions on both sender and receiver sides.

Key changes:

  • Event-driven architecture for onchain transaction updates (received, confirmed, replaced, reorged, evicted)
  • Removal of 1-second polling timer in favor of real-time LDK events
  • Enhanced RBF transaction detection using doesExist flag to differentiate from CPFP

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
BitkitNotification/NotificationService.swiftAdded notification handling for new onchain transaction events
Bitkit/Views/Wallets/Sheets/BoostSheet.swiftRemoved redundant LDK node payment sync after boost operation
Bitkit/Views/Wallets/Activity/ActivityItemView.swiftEnhanced boost button logic to differentiate between RBF and CPFP transactions
Bitkit/Views/Wallets/Activity/ActivityExplorerView.swiftReplaced AddressChecker API calls with LDK node transaction details
Bitkit/ViewModels/WalletViewModel.swiftRemoved polling mechanism and added event-driven state updates for onchain transactions
Bitkit/ViewModels/AppViewModel.swiftAdded comprehensive event handling for onchain transaction lifecycle
Bitkit/Utilities/AddressChecker.swiftRemoved external API-dependent utility in favor of native LDK functionality
Bitkit/Services/LightningService.swiftAdded transaction detail methods and event handlers for onchain events
Bitkit/Services/CoreService.swiftRefactored payment processing to handle LDK events and removed replacement tracking maps
Bitkit/AppScene.swiftRemoved balance change observer and event-based activity sync
Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolvedUpdated bitkit-core and ldk-node dependencies
Bitkit.xcodeproj/project.pbxprojUpdated project references for LDK package

Comment threadBitkit/Services/CoreService.swift
Comment threadBitkit/Services/CoreService.swift
Comment threadBitkit/Services/CoreService.swift
@claude

This comment was marked as outdated.

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

@piotr-iohk please note we will need to update the e2e tests to close the onchain received sheet.

@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

ovitrif
ovitrif previously approved these changes Nov 26, 2025

@ovitrifovitrif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

utAck

Amazing work, I've been waiting for over a year to finally have this setup in place: event-based sync for transactions 🎉

Added a few small nit comments, the one about the localizable strings applies to all cases where we added new toast messages, not only the code where I added it.

I wanted to give you the time to already make some small updates as I am starting to test this, hence me approving without testing; but I trust you'll wait for my testing 🙏🏻

Comment threadBitkit/ViewModels/AppViewModel.swift Outdated
Comment threadBitkitNotification/NotificationService.swift
@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

Comment threadBitkit/Resources/Localization/en.lproj/Localizable.strings Outdated
@claude

This comment was marked as outdated.

@piotr-iohk

Copy link
Copy Markdown
Collaborator

Ok, updated e2e tests for current state (synonymdev/bitkit-e2e-tests#56). Re-running 🤞
It would be good to coordinate merging with Android (synonymdev/bitkit-android#475)

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

@pwltr it's not due to race condition, it's just because the event of the new tx takes a few seconds to come in from the LDK node.

We can solve it by immediately caching the "intermediate" state in memory until the event comes in.

@pwltr

Copy link
Copy Markdown
Contributor

@pwltr it's not due to race condition, it's just because the event of the new tx takes a few seconds to come in from the LDK node.

We can solve it by immediately caching the "intermediate" state in memory until the event comes in.

Sounds good yeah I think an optimistic UI like this would be the way to go. Assume everything went right and correct if needed when the event(s) come in from LDK. Still feel free to postpone for now.

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

Actually had a simpler solution, fixed in: #247

pwltr
pwltr previously approved these changes Nov 27, 2025

@pwltrpwltr 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.

tACK with follow-up task here

Also note @piotr-iohk's request to merge this together with the Android counterpart.

@ben-kaufman
ben-kaufman dismissed stale reviews from pwltr, ovitrif, and jvsena42 via ab36651November 27, 2025 17:01
@claude

claudeBot commented Nov 27, 2025

Copy link
Copy Markdown
Contributor

PR Review: Integrate LDK onchain events

This is an excellent architectural improvement that modernizes the onchain transaction handling by leveraging LDK's event-driven architecture. The change from polling to event-driven updates should significantly improve performance and user experience.


Summary of Changes

Major Improvements:

  • Event-driven onchain transaction handling via LDK Node events
  • Removed AddressChecker utility (110 lines removed)
  • Enhanced RBF transaction tracking and handling
  • Added comprehensive onchain event handling (received, confirmed, replaced, reorged, evicted)
  • Improved user notifications for transaction state changes
  • Better handling of edge cases (reorgs, mempool evictions)

Impact:

  • 1,031 additions, 495 deletions across 20 files
  • Core architectural change in CoreService.swift (+529/-273)
  • Enhanced event handling in AppViewModel.swift (+108/-4)
  • Improved LightningService.swift integration (+120/-3)

Code Quality & Best Practices

Strengths

  1. Event-Driven Architecture - Proper use of LDK onchain events eliminates polling overhead
  2. Comprehensive Error Handling - Good use of try-catch blocks with descriptive logging
  3. Service Queue Pattern - Correctly uses ServiceQueue.background(.core) for Core operations
  4. MainActor Compliance - Proper async/await patterns with @mainactor for UI updates
  5. Caching Strategy - cachedTxIdsInBoostTxIds improves performance for boost transaction lookups
  6. Detailed Logging - Excellent context-aware logging throughout
  7. Accessibility - Added accessibilityIdentifier to Toast model and components

Code Organization

The new event handlers follow a clean pattern:

  • handleOnchainTransactionReceived
  • handleOnchainTransactionConfirmed
  • handleOnchainTransactionReplaced
  • handleOnchainTransactionReorged
  • handleOnchainTransactionEvicted

Each handler properly delegates to ServiceQueue.background(.core) and updates the activity list appropriately.


Potential Bugs & Issues

1. Race Condition in handleOnchainTransactionReplaced

Location: CoreService.swift:346-433

Issue: When processing replacement transactions, there is a potential race condition between checking if replacementActivity exists and processing the payment. Another event handler could process the same transaction concurrently.

Recommendation: Add a lock or transaction ID tracking to prevent concurrent processing of the same transaction.

2. Cache Invalidation Issue

Location: CoreService.swift:34-62

Issue: The cachedTxIdsInBoostTxIds cache is updated in insert and bulk refreshed in upsertList, but individual update operations don't update the cache. This could lead to stale cache data.

Recommendation: Add cache update to the update method to keep cache consistent.

3. Potential Memory Leak in Event Handlers

Location: AppScene.swift:213-215

Issue: The event handler closure captures app weakly, but the closure itself is never removed when the scene is dismissed.

Recommendation: Ensure event handlers are removed when the scene is dismissed. The addOnEvent API should support removal, or use a cleanup mechanism in deinit or onDisappear.

4. Missing nil-check in processOnchainTransaction

Location: CoreService.swift:315-332

Issue: The function logs a warning if payment is not found but doesn't throw an error. The calling code may not handle this gracefully.

Recommendation: Consider throwing a specific error so callers can decide how to handle missing payments, or add metrics to track how often this occurs.


Performance Considerations

Strengths

  1. Eliminated Polling - Removing AddressChecker and full syncs significantly reduces CPU/network overhead
  2. Efficient Caching - cachedTxIdsInBoostTxIds prevents repeated database queries
  3. Lazy Cache Initialization - Cache is only built when first accessed

Concerns

  1. Cache Refresh Performance - CoreService.swift:47-62 loads ALL onchain activities from the database, which could be slow for users with large transaction histories (1000+ transactions). Consider a more efficient query that only fetches boostTxIds without loading full activity objects.

  2. Delay in Received Sheet - AppViewModel.swift:399 has a 500ms delay. Is this necessary? It could make the UI feel sluggish. Consider removing or reducing if not strictly required.


Security Concerns

Good Practices

  1. Proper use of ServiceQueue prevents concurrent access to Core/LDK
  2. Transactions marked as doesExist = false when replaced (prevents double-counting)
  3. RBF detection prevents showing duplicate received sheets

Recommendations

  1. Transaction Validation - Consider adding validation that conflicts array in handleOnchainTransactionReplaced actually contains valid transaction IDs to prevent malformed data corruption.

  2. Timestamp Validation - In processOnchainPayment, ensure timestamps are within reasonable ranges to prevent time-based attacks.


Test Coverage

Missing Test Scenarios

  1. RBF Handling - Need tests for single replacement, multiple conflicts, chain of replacements, replacement arriving before original

  2. Reorg Handling - Need tests for confirmed tx becoming unconfirmed, multiple txs affected by reorg, reorg depth > 1 block

  3. Edge Cases - Need tests for tx evicted from mempool that was not replaced, race conditions, cache invalidation scenarios

Testing Recommendations

Add integration tests that simulate these event sequences:

  • onchainTransactionReceived → onchainTransactionReplaced → onchainTransactionConfirmed
  • onchainTransactionConfirmed → onchainTransactionReorged
  • onchainTransactionReceived → onchainTransactionEvicted

Final Recommendations

Must Fix (Before Merge)

  1. Run swiftformat . to ensure code formatting compliance
  2. Fix cache invalidation in update method
  3. Add race condition protection in handleOnchainTransactionReplaced
  4. Verify event handler cleanup to prevent memory leaks

Should Fix (High Priority)

  1. Optimize refreshBoostTxIdsCache for large transaction histories
  2. Add comprehensive tests for RBF and reorg scenarios
  3. Review the 500ms delay in received sheet display

Nice to Have

  1. Add transaction ID truncation to user-facing messages
  2. Improve user-friendly language for technical events (reorg, eviction)
  3. Add metrics/telemetry for tracking event frequencies

Conclusion

This is a high-quality PR that represents a significant architectural improvement. The event-driven approach is the correct design pattern and will eliminate polling overhead. The code quality is generally excellent with proper error handling, logging, and async patterns.

Recommendation:Approve with minor changes. Address the cache invalidation and race condition issues before merge, and plan follow-up work for comprehensive testing and performance optimization.

Great work! The videos demonstrating reorg and eviction handling are particularly impressive.

pwltr
pwltr previously approved these changes Nov 27, 2025
@ovitrif
ovitrif enabled auto-merge November 27, 2025 18:26
@ovitrif
ovitrif disabled auto-merge November 27, 2025 18:26
ovitrif
ovitrif previously approved these changes Nov 27, 2025

@ovitrifovitrif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

tAck

now it really shows the boosted TX update instantly every time I try it

@ovitrif
ovitrif dismissed stale reviews from pwltr and themself via 4b9083dNovember 27, 2025 22:39
@claude

claudeBot commented Nov 27, 2025

Copy link
Copy Markdown
Contributor

PR Review: Integrate LDK onchain events

Summary

This PR successfully replaces polling-based transaction monitoring with event-driven LDK onchain events, removes the AddressChecker, and improves RBF transaction handling. This is a significant architectural improvement that makes the app more reactive and efficient.

✅ Strengths

Architecture & Design

  1. Event-driven architecture: The transition from polling to LDK events (onchainTransactionReceived, onchainTransactionConfirmed, onchainTransactionReplaced, onchainTransactionReorged, onchainTransactionEvicted) is excellent. This provides real-time updates and reduces unnecessary syncs.

  2. Comprehensive RBF handling: The implementation properly tracks replacement transactions through boostTxIds and distinguishes between RBF (replaced transactions with doesExist=false) and CPFP (child transactions with doesExist=true).

  3. Clean separation of concerns: Event handlers in LightningService.swift (708-757) delegate to ActivityService methods, maintaining good separation between Lightning layer and activity management.

  4. Improved user feedback: New toast notifications with accessibility identifiers for transaction states (replaced, reorged, evicted) provide better UX.

  5. Removal of polling: Removing the onChange(of: wallet.totalBalanceSats) trigger in AppScene.swift:217 and the event-based sync in AppScene.swift:217-220 eliminates redundant activity syncing.

Code Quality

  1. Good error handling: All event handlers wrap operations in try-catch blocks with proper logging.
  2. Proper concurrency: Uses ServiceQueue.background(.core) for all database operations.
  3. Cache optimization: The cachedTxIdsInBoostTxIds cache improves performance for checking replaced transactions.

⚠️ Issues & Concerns

Critical

1. Potential cache inconsistency in CoreService.swift:36-43

func getTxIdsInBoostTxIds()async->Set<String>{if cachedTxIdsInBoostTxIds.isEmpty {awaitrefreshBoostTxIdsCache()}return cachedTxIdsInBoostTxIds
}

Issue: The cache is only refreshed when empty, but individual updates via updateBoostTxIdsCache(for:) might not be called for all updates. The update() method at line 195 doesn't update the cache.

Recommendation:

  • Update the cache in the update() method, or
  • Implement a TTL-based cache invalidation strategy, or
  • Document why the cache only needs to be refreshed when empty

2. Race condition in RBF handling (CoreService.swift:355-441)

When handleOnchainTransactionReplaced is called, it:

  1. Marks the replaced transaction as doesExist=false
  2. Processes replacement transactions that might not exist yet in the payment store
  3. Waits for them via processOnchainPayment

Issue: There's a timing window where a replacement transaction event arrives before LDK updates the payment store. The code handles this (line 385-403) by processing the payment, but there's a potential race if the transaction hasn't been added to the payments list yet.

Recommendation: Consider adding retry logic or ensuring LDK guarantees payment store updates before emitting replacement events.

Moderate

3. Missing cache update in update() method (CoreService.swift:193-199)

func update(id:String, activity:Activity)asyncthrows{tryawaitServiceQueue.background(.core){tryupdateActivity(activityId: id, activity: activity)self.metadataChangedSubject.send()}}

This doesn't call updateBoostTxIdsCache(for: activity) like insert() does at line 172.

Recommendation: Add cache update to maintain consistency.

4. Incomplete validation in shouldShowReceivedSheet (CoreService.swift:76-107)

The method checks if a replacement transaction has the same value as the replaced transaction to avoid showing duplicate sheets. However:

  • It only checks exact value matches
  • Doesn't account for fee differences in RBF
  • Could show sheets for CPFP where the child transaction might have a different value

Recommendation: Consider checking isBoosted flag and the nature of the boost (RBF vs CPFP) for more precise logic.

5. Error handling in handleOnchainTransactionReplaced (CoreService.swift:355-441)

When processing replacement transactions, errors are caught and logged (line 401-405), but the loop continues. If multiple replacement transactions fail to process, the relationship tracking could be incomplete.

Recommendation: Consider collecting errors and either throwing or returning them to the caller for better error visibility.

6. CI workflow changes remove caching (integration-tests.yml, unit-tests.yml)

The PR removes DerivedData caching and restore-keys from SPM cache. While this ensures clean builds, it will increase build times significantly.

Recommendation:

  • Document why caching was removed (likely due to cache invalidation issues)
  • Consider using Xcode Cloud or other CI optimizations to offset the performance loss
  • The -onlyUsePackageVersionsFromResolvedFile flag is good for reproducibility

Minor

7. Potential N+1 query in getBoostTxDoesExist (CoreService.swift:130-138)

func getBoostTxDoesExist(boostTxIds:[String])async->[String:Bool]{vardoesExistMap:[String:Bool]=[:]forboostTxIdin boostTxIds {
if let boostActivity =try?awaitgetOnchainActivityByTxId(txid: boostTxId){doesExistMap[boostTxId]= boostActivity.doesExist
}}return doesExistMap
}

Issue: Makes individual database queries for each boost transaction.

Recommendation: Consider adding a batch query method to BitkitCore to fetch multiple activities by txId in one query.

8. Magic number delay in AppViewModel.swift:399

try?awaitTask.sleep(nanoseconds:500_000_000) // 500ms delay

Recommendation: Extract to a named constant with documentation explaining why the delay is necessary.

9. Missing test coverage

While TxBumpingTests.swift and ActivityListTest.swift exist, there don't appear to be specific tests for:

  • Onchain event handlers
  • RBF replacement tracking with boostTxIds
  • Reorg and eviction scenarios
  • Cache consistency

Recommendation: Add integration tests for the new event handlers.

🔒 Security

No major security concerns identified. The code properly:

  • Uses ServiceQueue for thread safety
  • Doesn't expose sensitive transaction data in logs beyond txids
  • Maintains Bitcoin/Lightning operation isolation in the service layer

Minor: Logging in LightningService.swift:711-757 includes transaction amounts. Consider whether this is acceptable for production logging.

🚀 Performance

Positive impacts:

  1. Eliminates redundant polling and full activity list syncs
  2. Event-driven updates only process changed transactions
  3. Cache for boostTxIds lookups

Potential concerns:

  1. Cache not updated consistently (see issue Project setup #1, Migrate channels from LDK to LDK-node #3)
  2. N+1 queries in getBoostTxDoesExist (see issue feat: keychain #7)

🧪 Testing

Existing test coverage:

  • ActivityListTest.swift: Good coverage of basic activity CRUD operations
  • TxBumpingTests.swift: Tests RBF scenarios

Missing:

  • Tests for new onchain event handlers
  • Tests for handleOnchainTransactionReplaced, handleOnchainTransactionReorged, handleOnchainTransactionEvicted
  • Tests for cache consistency
  • Integration tests for the complete event flow

Recommendation: Add integration tests that simulate the event sequences shown in the PR videos.

📝 Recommendations

Must Fix Before Merge

  1. Fix cache consistency issues (Issues Project setup #1, Migrate channels from LDK to LDK-node #3)
  2. Add error handling for race conditions in RBF handling (Issue Push service setup and service queues  #2)

Should Fix Before Merge

  1. Improve shouldShowReceivedSheet validation logic (Issue Create swift.yml #4)
  2. Better error aggregation in handleOnchainTransactionReplaced (Issue Create ios.yml #5)

Consider for Follow-up

  1. Add batch query support for getBoostTxDoesExist (Issue feat: keychain #7)
  2. Add integration tests for event handlers (Issue Use SwiftFormat #9)
  3. Extract magic constants (Issue BDK 1.0.0-alpha.11 #8)
  4. Document CI caching removal reasoning (Issue Create ios.yml #6)

🎯 Overall Assessment

This is a high-quality PR that represents a significant architectural improvement. The event-driven approach is the correct design, and the RBF handling logic is well thought out. The main concerns are around cache consistency and potential race conditions in RBF processing, which should be addressed before merge.

The removal of AddressChecker and polling-based syncing will improve app performance and responsiveness.

Recommendation: Request changes to address cache consistency and race condition handling, then approve after fixes.


Great work on this PR! The demos showing reorg and mempool eviction handling are particularly impressive. Once the cache and race condition issues are resolved, this will be a solid improvement to the codebase.

@ovitrifovitrif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

tAck

now it really shows the boosted TX update instantly every time

@pwltrpwltr linked an issue Nov 28, 2025 that may be closed by this pull request
@ovitrif

Copy link
Copy Markdown
Collaborator

Android PR should succeed e2e once Electrum on staging is fixed. Merging this.

1 similar comment
@ovitrif

Copy link
Copy Markdown
Collaborator

Android PR should succeed e2e once Electrum on staging is fixed. Merging this.

@ovitrif
ovitrif merged commit 4b25919 into masterNov 28, 2025
10 checks passed
@ovitrif
ovitrif deleted the feat/onchain-events branch November 28, 2025 23:14
BitcoinErrorLog pushed a commit to BitcoinErrorLog/bitkit-ios that referenced this pull request Dec 15, 2025
BitcoinErrorLog pushed a commit to BitcoinErrorLog/bitkit-ios that referenced this pull request Feb 7, 2026
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.

[Bug]: Polish RBF & CPFP activity list behaviour

6 participants

@ben-kaufman@ovitrif@piotr-iohk@pwltr@jvsena42
, '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

Integrate LDK onchain events - #245

Merged
ovitrif merged 20 commits into
masterfrom
feat/onchain-events
Nov 28, 2025
Merged

Integrate LDK onchain events#245
ovitrif merged 20 commits into
masterfrom
feat/onchain-events

Conversation

@ben-kaufman

@ben-kaufmanben-kaufman commented Nov 26, 2025

Copy link
Copy Markdown
Contributor

This PR integrates LDK node on chain events to replace the previous pollin and full syncs.

It also removes Address Checker, and improves handling on RBFed transactions to be recognized on both sides.

Testing:
Should test all actions, send, receive (onchain and LN), transfer to savings, transfer to spendings, etc.

Screen.Recording.2025-11-26.at.9.05.11.AM.mov

Reorg removes tx from blocks demo:

Screen.Recording.2025-11-26.at.5.13.45.PM.mov

Transaction removed from mempool demo:

Screen.Recording.2025-11-26.at.5.38.36.PM.mov

@claude

This comment was marked as outdated.

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 integrates LDK node onchain events to replace previous polling and full sync mechanisms for Bitcoin transaction management. It removes the AddressChecker utility class that relied on external API calls, and improves handling of RBF (Replace-By-Fee) transactions to properly distinguish them from CPFP (Child-Pays-For-Parent) transactions on both sender and receiver sides.

Key changes:

  • Event-driven architecture for onchain transaction updates (received, confirmed, replaced, reorged, evicted)
  • Removal of 1-second polling timer in favor of real-time LDK events
  • Enhanced RBF transaction detection using doesExist flag to differentiate from CPFP

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
BitkitNotification/NotificationService.swiftAdded notification handling for new onchain transaction events
Bitkit/Views/Wallets/Sheets/BoostSheet.swiftRemoved redundant LDK node payment sync after boost operation
Bitkit/Views/Wallets/Activity/ActivityItemView.swiftEnhanced boost button logic to differentiate between RBF and CPFP transactions
Bitkit/Views/Wallets/Activity/ActivityExplorerView.swiftReplaced AddressChecker API calls with LDK node transaction details
Bitkit/ViewModels/WalletViewModel.swiftRemoved polling mechanism and added event-driven state updates for onchain transactions
Bitkit/ViewModels/AppViewModel.swiftAdded comprehensive event handling for onchain transaction lifecycle
Bitkit/Utilities/AddressChecker.swiftRemoved external API-dependent utility in favor of native LDK functionality
Bitkit/Services/LightningService.swiftAdded transaction detail methods and event handlers for onchain events
Bitkit/Services/CoreService.swiftRefactored payment processing to handle LDK events and removed replacement tracking maps
Bitkit/AppScene.swiftRemoved balance change observer and event-based activity sync
Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolvedUpdated bitkit-core and ldk-node dependencies
Bitkit.xcodeproj/project.pbxprojUpdated project references for LDK package

Comment threadBitkit/Services/CoreService.swift
Comment threadBitkit/Services/CoreService.swift
Comment threadBitkit/Services/CoreService.swift
@claude

This comment was marked as outdated.

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

@piotr-iohk please note we will need to update the e2e tests to close the onchain received sheet.

@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

ovitrif
ovitrif previously approved these changes Nov 26, 2025

@ovitrifovitrif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

utAck

Amazing work, I've been waiting for over a year to finally have this setup in place: event-based sync for transactions 🎉

Added a few small nit comments, the one about the localizable strings applies to all cases where we added new toast messages, not only the code where I added it.

I wanted to give you the time to already make some small updates as I am starting to test this, hence me approving without testing; but I trust you'll wait for my testing 🙏🏻

Comment threadBitkit/ViewModels/AppViewModel.swift Outdated
Comment threadBitkitNotification/NotificationService.swift
@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

Comment threadBitkit/Resources/Localization/en.lproj/Localizable.strings Outdated
@claude

This comment was marked as outdated.

@piotr-iohk

Copy link
Copy Markdown
Collaborator

Ok, updated e2e tests for current state (synonymdev/bitkit-e2e-tests#56). Re-running 🤞
It would be good to coordinate merging with Android (synonymdev/bitkit-android#475)

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

@pwltr it's not due to race condition, it's just because the event of the new tx takes a few seconds to come in from the LDK node.

We can solve it by immediately caching the "intermediate" state in memory until the event comes in.

@pwltr

Copy link
Copy Markdown
Contributor

@pwltr it's not due to race condition, it's just because the event of the new tx takes a few seconds to come in from the LDK node.

We can solve it by immediately caching the "intermediate" state in memory until the event comes in.

Sounds good yeah I think an optimistic UI like this would be the way to go. Assume everything went right and correct if needed when the event(s) come in from LDK. Still feel free to postpone for now.

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

Actually had a simpler solution, fixed in: #247

pwltr
pwltr previously approved these changes Nov 27, 2025

@pwltrpwltr 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.

tACK with follow-up task here

Also note @piotr-iohk's request to merge this together with the Android counterpart.

@ben-kaufman
ben-kaufman dismissed stale reviews from pwltr, ovitrif, and jvsena42 via ab36651November 27, 2025 17:01
@claude

claudeBot commented Nov 27, 2025

Copy link
Copy Markdown
Contributor

PR Review: Integrate LDK onchain events

This is an excellent architectural improvement that modernizes the onchain transaction handling by leveraging LDK's event-driven architecture. The change from polling to event-driven updates should significantly improve performance and user experience.


Summary of Changes

Major Improvements:

  • Event-driven onchain transaction handling via LDK Node events
  • Removed AddressChecker utility (110 lines removed)
  • Enhanced RBF transaction tracking and handling
  • Added comprehensive onchain event handling (received, confirmed, replaced, reorged, evicted)
  • Improved user notifications for transaction state changes
  • Better handling of edge cases (reorgs, mempool evictions)

Impact:

  • 1,031 additions, 495 deletions across 20 files
  • Core architectural change in CoreService.swift (+529/-273)
  • Enhanced event handling in AppViewModel.swift (+108/-4)
  • Improved LightningService.swift integration (+120/-3)

Code Quality & Best Practices

Strengths

  1. Event-Driven Architecture - Proper use of LDK onchain events eliminates polling overhead
  2. Comprehensive Error Handling - Good use of try-catch blocks with descriptive logging
  3. Service Queue Pattern - Correctly uses ServiceQueue.background(.core) for Core operations
  4. MainActor Compliance - Proper async/await patterns with @mainactor for UI updates
  5. Caching Strategy - cachedTxIdsInBoostTxIds improves performance for boost transaction lookups
  6. Detailed Logging - Excellent context-aware logging throughout
  7. Accessibility - Added accessibilityIdentifier to Toast model and components

Code Organization

The new event handlers follow a clean pattern:

  • handleOnchainTransactionReceived
  • handleOnchainTransactionConfirmed
  • handleOnchainTransactionReplaced
  • handleOnchainTransactionReorged
  • handleOnchainTransactionEvicted

Each handler properly delegates to ServiceQueue.background(.core) and updates the activity list appropriately.


Potential Bugs & Issues

1. Race Condition in handleOnchainTransactionReplaced

Location: CoreService.swift:346-433

Issue: When processing replacement transactions, there is a potential race condition between checking if replacementActivity exists and processing the payment. Another event handler could process the same transaction concurrently.

Recommendation: Add a lock or transaction ID tracking to prevent concurrent processing of the same transaction.

2. Cache Invalidation Issue

Location: CoreService.swift:34-62

Issue: The cachedTxIdsInBoostTxIds cache is updated in insert and bulk refreshed in upsertList, but individual update operations don't update the cache. This could lead to stale cache data.

Recommendation: Add cache update to the update method to keep cache consistent.

3. Potential Memory Leak in Event Handlers

Location: AppScene.swift:213-215

Issue: The event handler closure captures app weakly, but the closure itself is never removed when the scene is dismissed.

Recommendation: Ensure event handlers are removed when the scene is dismissed. The addOnEvent API should support removal, or use a cleanup mechanism in deinit or onDisappear.

4. Missing nil-check in processOnchainTransaction

Location: CoreService.swift:315-332

Issue: The function logs a warning if payment is not found but doesn't throw an error. The calling code may not handle this gracefully.

Recommendation: Consider throwing a specific error so callers can decide how to handle missing payments, or add metrics to track how often this occurs.


Performance Considerations

Strengths

  1. Eliminated Polling - Removing AddressChecker and full syncs significantly reduces CPU/network overhead
  2. Efficient Caching - cachedTxIdsInBoostTxIds prevents repeated database queries
  3. Lazy Cache Initialization - Cache is only built when first accessed

Concerns

  1. Cache Refresh Performance - CoreService.swift:47-62 loads ALL onchain activities from the database, which could be slow for users with large transaction histories (1000+ transactions). Consider a more efficient query that only fetches boostTxIds without loading full activity objects.

  2. Delay in Received Sheet - AppViewModel.swift:399 has a 500ms delay. Is this necessary? It could make the UI feel sluggish. Consider removing or reducing if not strictly required.


Security Concerns

Good Practices

  1. Proper use of ServiceQueue prevents concurrent access to Core/LDK
  2. Transactions marked as doesExist = false when replaced (prevents double-counting)
  3. RBF detection prevents showing duplicate received sheets

Recommendations

  1. Transaction Validation - Consider adding validation that conflicts array in handleOnchainTransactionReplaced actually contains valid transaction IDs to prevent malformed data corruption.

  2. Timestamp Validation - In processOnchainPayment, ensure timestamps are within reasonable ranges to prevent time-based attacks.


Test Coverage

Missing Test Scenarios

  1. RBF Handling - Need tests for single replacement, multiple conflicts, chain of replacements, replacement arriving before original

  2. Reorg Handling - Need tests for confirmed tx becoming unconfirmed, multiple txs affected by reorg, reorg depth > 1 block

  3. Edge Cases - Need tests for tx evicted from mempool that was not replaced, race conditions, cache invalidation scenarios

Testing Recommendations

Add integration tests that simulate these event sequences:

  • onchainTransactionReceived → onchainTransactionReplaced → onchainTransactionConfirmed
  • onchainTransactionConfirmed → onchainTransactionReorged
  • onchainTransactionReceived → onchainTransactionEvicted

Final Recommendations

Must Fix (Before Merge)

  1. Run swiftformat . to ensure code formatting compliance
  2. Fix cache invalidation in update method
  3. Add race condition protection in handleOnchainTransactionReplaced
  4. Verify event handler cleanup to prevent memory leaks

Should Fix (High Priority)

  1. Optimize refreshBoostTxIdsCache for large transaction histories
  2. Add comprehensive tests for RBF and reorg scenarios
  3. Review the 500ms delay in received sheet display

Nice to Have

  1. Add transaction ID truncation to user-facing messages
  2. Improve user-friendly language for technical events (reorg, eviction)
  3. Add metrics/telemetry for tracking event frequencies

Conclusion

This is a high-quality PR that represents a significant architectural improvement. The event-driven approach is the correct design pattern and will eliminate polling overhead. The code quality is generally excellent with proper error handling, logging, and async patterns.

Recommendation:Approve with minor changes. Address the cache invalidation and race condition issues before merge, and plan follow-up work for comprehensive testing and performance optimization.

Great work! The videos demonstrating reorg and eviction handling are particularly impressive.

pwltr
pwltr previously approved these changes Nov 27, 2025
@ovitrif
ovitrif enabled auto-merge November 27, 2025 18:26
@ovitrif
ovitrif disabled auto-merge November 27, 2025 18:26
ovitrif
ovitrif previously approved these changes Nov 27, 2025

@ovitrifovitrif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

tAck

now it really shows the boosted TX update instantly every time I try it

@ovitrif
ovitrif dismissed stale reviews from pwltr and themself via 4b9083dNovember 27, 2025 22:39
@claude

claudeBot commented Nov 27, 2025

Copy link
Copy Markdown
Contributor

PR Review: Integrate LDK onchain events

Summary

This PR successfully replaces polling-based transaction monitoring with event-driven LDK onchain events, removes the AddressChecker, and improves RBF transaction handling. This is a significant architectural improvement that makes the app more reactive and efficient.

✅ Strengths

Architecture & Design

  1. Event-driven architecture: The transition from polling to LDK events (onchainTransactionReceived, onchainTransactionConfirmed, onchainTransactionReplaced, onchainTransactionReorged, onchainTransactionEvicted) is excellent. This provides real-time updates and reduces unnecessary syncs.

  2. Comprehensive RBF handling: The implementation properly tracks replacement transactions through boostTxIds and distinguishes between RBF (replaced transactions with doesExist=false) and CPFP (child transactions with doesExist=true).

  3. Clean separation of concerns: Event handlers in LightningService.swift (708-757) delegate to ActivityService methods, maintaining good separation between Lightning layer and activity management.

  4. Improved user feedback: New toast notifications with accessibility identifiers for transaction states (replaced, reorged, evicted) provide better UX.

  5. Removal of polling: Removing the onChange(of: wallet.totalBalanceSats) trigger in AppScene.swift:217 and the event-based sync in AppScene.swift:217-220 eliminates redundant activity syncing.

Code Quality

  1. Good error handling: All event handlers wrap operations in try-catch blocks with proper logging.
  2. Proper concurrency: Uses ServiceQueue.background(.core) for all database operations.
  3. Cache optimization: The cachedTxIdsInBoostTxIds cache improves performance for checking replaced transactions.

⚠️ Issues & Concerns

Critical

1. Potential cache inconsistency in CoreService.swift:36-43

func getTxIdsInBoostTxIds()async->Set<String>{if cachedTxIdsInBoostTxIds.isEmpty {awaitrefreshBoostTxIdsCache()}return cachedTxIdsInBoostTxIds
}

Issue: The cache is only refreshed when empty, but individual updates via updateBoostTxIdsCache(for:) might not be called for all updates. The update() method at line 195 doesn't update the cache.

Recommendation:

  • Update the cache in the update() method, or
  • Implement a TTL-based cache invalidation strategy, or
  • Document why the cache only needs to be refreshed when empty

2. Race condition in RBF handling (CoreService.swift:355-441)

When handleOnchainTransactionReplaced is called, it:

  1. Marks the replaced transaction as doesExist=false
  2. Processes replacement transactions that might not exist yet in the payment store
  3. Waits for them via processOnchainPayment

Issue: There's a timing window where a replacement transaction event arrives before LDK updates the payment store. The code handles this (line 385-403) by processing the payment, but there's a potential race if the transaction hasn't been added to the payments list yet.

Recommendation: Consider adding retry logic or ensuring LDK guarantees payment store updates before emitting replacement events.

Moderate

3. Missing cache update in update() method (CoreService.swift:193-199)

func update(id:String, activity:Activity)asyncthrows{tryawaitServiceQueue.background(.core){tryupdateActivity(activityId: id, activity: activity)self.metadataChangedSubject.send()}}

This doesn't call updateBoostTxIdsCache(for: activity) like insert() does at line 172.

Recommendation: Add cache update to maintain consistency.

4. Incomplete validation in shouldShowReceivedSheet (CoreService.swift:76-107)

The method checks if a replacement transaction has the same value as the replaced transaction to avoid showing duplicate sheets. However:

  • It only checks exact value matches
  • Doesn't account for fee differences in RBF
  • Could show sheets for CPFP where the child transaction might have a different value

Recommendation: Consider checking isBoosted flag and the nature of the boost (RBF vs CPFP) for more precise logic.

5. Error handling in handleOnchainTransactionReplaced (CoreService.swift:355-441)

When processing replacement transactions, errors are caught and logged (line 401-405), but the loop continues. If multiple replacement transactions fail to process, the relationship tracking could be incomplete.

Recommendation: Consider collecting errors and either throwing or returning them to the caller for better error visibility.

6. CI workflow changes remove caching (integration-tests.yml, unit-tests.yml)

The PR removes DerivedData caching and restore-keys from SPM cache. While this ensures clean builds, it will increase build times significantly.

Recommendation:

  • Document why caching was removed (likely due to cache invalidation issues)
  • Consider using Xcode Cloud or other CI optimizations to offset the performance loss
  • The -onlyUsePackageVersionsFromResolvedFile flag is good for reproducibility

Minor

7. Potential N+1 query in getBoostTxDoesExist (CoreService.swift:130-138)

func getBoostTxDoesExist(boostTxIds:[String])async->[String:Bool]{vardoesExistMap:[String:Bool]=[:]forboostTxIdin boostTxIds {
if let boostActivity =try?awaitgetOnchainActivityByTxId(txid: boostTxId){doesExistMap[boostTxId]= boostActivity.doesExist
}}return doesExistMap
}

Issue: Makes individual database queries for each boost transaction.

Recommendation: Consider adding a batch query method to BitkitCore to fetch multiple activities by txId in one query.

8. Magic number delay in AppViewModel.swift:399

try?awaitTask.sleep(nanoseconds:500_000_000) // 500ms delay

Recommendation: Extract to a named constant with documentation explaining why the delay is necessary.

9. Missing test coverage

While TxBumpingTests.swift and ActivityListTest.swift exist, there don't appear to be specific tests for:

  • Onchain event handlers
  • RBF replacement tracking with boostTxIds
  • Reorg and eviction scenarios
  • Cache consistency

Recommendation: Add integration tests for the new event handlers.

🔒 Security

No major security concerns identified. The code properly:

  • Uses ServiceQueue for thread safety
  • Doesn't expose sensitive transaction data in logs beyond txids
  • Maintains Bitcoin/Lightning operation isolation in the service layer

Minor: Logging in LightningService.swift:711-757 includes transaction amounts. Consider whether this is acceptable for production logging.

🚀 Performance

Positive impacts:

  1. Eliminates redundant polling and full activity list syncs
  2. Event-driven updates only process changed transactions
  3. Cache for boostTxIds lookups

Potential concerns:

  1. Cache not updated consistently (see issue Project setup #1, Migrate channels from LDK to LDK-node #3)
  2. N+1 queries in getBoostTxDoesExist (see issue feat: keychain #7)

🧪 Testing

Existing test coverage:

  • ActivityListTest.swift: Good coverage of basic activity CRUD operations
  • TxBumpingTests.swift: Tests RBF scenarios

Missing:

  • Tests for new onchain event handlers
  • Tests for handleOnchainTransactionReplaced, handleOnchainTransactionReorged, handleOnchainTransactionEvicted
  • Tests for cache consistency
  • Integration tests for the complete event flow

Recommendation: Add integration tests that simulate the event sequences shown in the PR videos.

📝 Recommendations

Must Fix Before Merge

  1. Fix cache consistency issues (Issues Project setup #1, Migrate channels from LDK to LDK-node #3)
  2. Add error handling for race conditions in RBF handling (Issue Push service setup and service queues  #2)

Should Fix Before Merge

  1. Improve shouldShowReceivedSheet validation logic (Issue Create swift.yml #4)
  2. Better error aggregation in handleOnchainTransactionReplaced (Issue Create ios.yml #5)

Consider for Follow-up

  1. Add batch query support for getBoostTxDoesExist (Issue feat: keychain #7)
  2. Add integration tests for event handlers (Issue Use SwiftFormat #9)
  3. Extract magic constants (Issue BDK 1.0.0-alpha.11 #8)
  4. Document CI caching removal reasoning (Issue Create ios.yml #6)

🎯 Overall Assessment

This is a high-quality PR that represents a significant architectural improvement. The event-driven approach is the correct design, and the RBF handling logic is well thought out. The main concerns are around cache consistency and potential race conditions in RBF processing, which should be addressed before merge.

The removal of AddressChecker and polling-based syncing will improve app performance and responsiveness.

Recommendation: Request changes to address cache consistency and race condition handling, then approve after fixes.


Great work on this PR! The demos showing reorg and mempool eviction handling are particularly impressive. Once the cache and race condition issues are resolved, this will be a solid improvement to the codebase.

@ovitrifovitrif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

tAck

now it really shows the boosted TX update instantly every time

@pwltrpwltr linked an issue Nov 28, 2025 that may be closed by this pull request
@ovitrif

Copy link
Copy Markdown
Collaborator

Android PR should succeed e2e once Electrum on staging is fixed. Merging this.

1 similar comment
@ovitrif

Copy link
Copy Markdown
Collaborator

Android PR should succeed e2e once Electrum on staging is fixed. Merging this.

@ovitrif
ovitrif merged commit 4b25919 into masterNov 28, 2025
10 checks passed
@ovitrif
ovitrif deleted the feat/onchain-events branch November 28, 2025 23:14
BitcoinErrorLog pushed a commit to BitcoinErrorLog/bitkit-ios that referenced this pull request Dec 15, 2025
BitcoinErrorLog pushed a commit to BitcoinErrorLog/bitkit-ios that referenced this pull request Feb 7, 2026
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.

[Bug]: Polish RBF & CPFP activity list behaviour

6 participants

@ben-kaufman@ovitrif@piotr-iohk@pwltr@jvsena42
, '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

Integrate LDK onchain events - #245

Merged
ovitrif merged 20 commits into
masterfrom
feat/onchain-events
Nov 28, 2025
Merged

Integrate LDK onchain events#245
ovitrif merged 20 commits into
masterfrom
feat/onchain-events

Conversation

@ben-kaufman

@ben-kaufmanben-kaufman commented Nov 26, 2025

Copy link
Copy Markdown
Contributor

This PR integrates LDK node on chain events to replace the previous pollin and full syncs.

It also removes Address Checker, and improves handling on RBFed transactions to be recognized on both sides.

Testing:
Should test all actions, send, receive (onchain and LN), transfer to savings, transfer to spendings, etc.

Screen.Recording.2025-11-26.at.9.05.11.AM.mov

Reorg removes tx from blocks demo:

Screen.Recording.2025-11-26.at.5.13.45.PM.mov

Transaction removed from mempool demo:

Screen.Recording.2025-11-26.at.5.38.36.PM.mov

@claude

This comment was marked as outdated.

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 integrates LDK node onchain events to replace previous polling and full sync mechanisms for Bitcoin transaction management. It removes the AddressChecker utility class that relied on external API calls, and improves handling of RBF (Replace-By-Fee) transactions to properly distinguish them from CPFP (Child-Pays-For-Parent) transactions on both sender and receiver sides.

Key changes:

  • Event-driven architecture for onchain transaction updates (received, confirmed, replaced, reorged, evicted)
  • Removal of 1-second polling timer in favor of real-time LDK events
  • Enhanced RBF transaction detection using doesExist flag to differentiate from CPFP

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
BitkitNotification/NotificationService.swiftAdded notification handling for new onchain transaction events
Bitkit/Views/Wallets/Sheets/BoostSheet.swiftRemoved redundant LDK node payment sync after boost operation
Bitkit/Views/Wallets/Activity/ActivityItemView.swiftEnhanced boost button logic to differentiate between RBF and CPFP transactions
Bitkit/Views/Wallets/Activity/ActivityExplorerView.swiftReplaced AddressChecker API calls with LDK node transaction details
Bitkit/ViewModels/WalletViewModel.swiftRemoved polling mechanism and added event-driven state updates for onchain transactions
Bitkit/ViewModels/AppViewModel.swiftAdded comprehensive event handling for onchain transaction lifecycle
Bitkit/Utilities/AddressChecker.swiftRemoved external API-dependent utility in favor of native LDK functionality
Bitkit/Services/LightningService.swiftAdded transaction detail methods and event handlers for onchain events
Bitkit/Services/CoreService.swiftRefactored payment processing to handle LDK events and removed replacement tracking maps
Bitkit/AppScene.swiftRemoved balance change observer and event-based activity sync
Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolvedUpdated bitkit-core and ldk-node dependencies
Bitkit.xcodeproj/project.pbxprojUpdated project references for LDK package

Comment threadBitkit/Services/CoreService.swift
Comment threadBitkit/Services/CoreService.swift
Comment threadBitkit/Services/CoreService.swift
@claude

This comment was marked as outdated.

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

@piotr-iohk please note we will need to update the e2e tests to close the onchain received sheet.

@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

ovitrif
ovitrif previously approved these changes Nov 26, 2025

@ovitrifovitrif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

utAck

Amazing work, I've been waiting for over a year to finally have this setup in place: event-based sync for transactions 🎉

Added a few small nit comments, the one about the localizable strings applies to all cases where we added new toast messages, not only the code where I added it.

I wanted to give you the time to already make some small updates as I am starting to test this, hence me approving without testing; but I trust you'll wait for my testing 🙏🏻

Comment threadBitkit/ViewModels/AppViewModel.swift Outdated
Comment threadBitkitNotification/NotificationService.swift
@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

Comment threadBitkit/Resources/Localization/en.lproj/Localizable.strings Outdated
@claude

This comment was marked as outdated.

@piotr-iohk

Copy link
Copy Markdown
Collaborator

Ok, updated e2e tests for current state (synonymdev/bitkit-e2e-tests#56). Re-running 🤞
It would be good to coordinate merging with Android (synonymdev/bitkit-android#475)

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

@pwltr it's not due to race condition, it's just because the event of the new tx takes a few seconds to come in from the LDK node.

We can solve it by immediately caching the "intermediate" state in memory until the event comes in.

@pwltr

Copy link
Copy Markdown
Contributor

@pwltr it's not due to race condition, it's just because the event of the new tx takes a few seconds to come in from the LDK node.

We can solve it by immediately caching the "intermediate" state in memory until the event comes in.

Sounds good yeah I think an optimistic UI like this would be the way to go. Assume everything went right and correct if needed when the event(s) come in from LDK. Still feel free to postpone for now.

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

Actually had a simpler solution, fixed in: #247

pwltr
pwltr previously approved these changes Nov 27, 2025

@pwltrpwltr 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.

tACK with follow-up task here

Also note @piotr-iohk's request to merge this together with the Android counterpart.

@ben-kaufman
ben-kaufman dismissed stale reviews from pwltr, ovitrif, and jvsena42 via ab36651November 27, 2025 17:01
@claude

claudeBot commented Nov 27, 2025

Copy link
Copy Markdown
Contributor

PR Review: Integrate LDK onchain events

This is an excellent architectural improvement that modernizes the onchain transaction handling by leveraging LDK's event-driven architecture. The change from polling to event-driven updates should significantly improve performance and user experience.


Summary of Changes

Major Improvements:

  • Event-driven onchain transaction handling via LDK Node events
  • Removed AddressChecker utility (110 lines removed)
  • Enhanced RBF transaction tracking and handling
  • Added comprehensive onchain event handling (received, confirmed, replaced, reorged, evicted)
  • Improved user notifications for transaction state changes
  • Better handling of edge cases (reorgs, mempool evictions)

Impact:

  • 1,031 additions, 495 deletions across 20 files
  • Core architectural change in CoreService.swift (+529/-273)
  • Enhanced event handling in AppViewModel.swift (+108/-4)
  • Improved LightningService.swift integration (+120/-3)

Code Quality & Best Practices

Strengths

  1. Event-Driven Architecture - Proper use of LDK onchain events eliminates polling overhead
  2. Comprehensive Error Handling - Good use of try-catch blocks with descriptive logging
  3. Service Queue Pattern - Correctly uses ServiceQueue.background(.core) for Core operations
  4. MainActor Compliance - Proper async/await patterns with @mainactor for UI updates
  5. Caching Strategy - cachedTxIdsInBoostTxIds improves performance for boost transaction lookups
  6. Detailed Logging - Excellent context-aware logging throughout
  7. Accessibility - Added accessibilityIdentifier to Toast model and components

Code Organization

The new event handlers follow a clean pattern:

  • handleOnchainTransactionReceived
  • handleOnchainTransactionConfirmed
  • handleOnchainTransactionReplaced
  • handleOnchainTransactionReorged
  • handleOnchainTransactionEvicted

Each handler properly delegates to ServiceQueue.background(.core) and updates the activity list appropriately.


Potential Bugs & Issues

1. Race Condition in handleOnchainTransactionReplaced

Location: CoreService.swift:346-433

Issue: When processing replacement transactions, there is a potential race condition between checking if replacementActivity exists and processing the payment. Another event handler could process the same transaction concurrently.

Recommendation: Add a lock or transaction ID tracking to prevent concurrent processing of the same transaction.

2. Cache Invalidation Issue

Location: CoreService.swift:34-62

Issue: The cachedTxIdsInBoostTxIds cache is updated in insert and bulk refreshed in upsertList, but individual update operations don't update the cache. This could lead to stale cache data.

Recommendation: Add cache update to the update method to keep cache consistent.

3. Potential Memory Leak in Event Handlers

Location: AppScene.swift:213-215

Issue: The event handler closure captures app weakly, but the closure itself is never removed when the scene is dismissed.

Recommendation: Ensure event handlers are removed when the scene is dismissed. The addOnEvent API should support removal, or use a cleanup mechanism in deinit or onDisappear.

4. Missing nil-check in processOnchainTransaction

Location: CoreService.swift:315-332

Issue: The function logs a warning if payment is not found but doesn't throw an error. The calling code may not handle this gracefully.

Recommendation: Consider throwing a specific error so callers can decide how to handle missing payments, or add metrics to track how often this occurs.


Performance Considerations

Strengths

  1. Eliminated Polling - Removing AddressChecker and full syncs significantly reduces CPU/network overhead
  2. Efficient Caching - cachedTxIdsInBoostTxIds prevents repeated database queries
  3. Lazy Cache Initialization - Cache is only built when first accessed

Concerns

  1. Cache Refresh Performance - CoreService.swift:47-62 loads ALL onchain activities from the database, which could be slow for users with large transaction histories (1000+ transactions). Consider a more efficient query that only fetches boostTxIds without loading full activity objects.

  2. Delay in Received Sheet - AppViewModel.swift:399 has a 500ms delay. Is this necessary? It could make the UI feel sluggish. Consider removing or reducing if not strictly required.


Security Concerns

Good Practices

  1. Proper use of ServiceQueue prevents concurrent access to Core/LDK
  2. Transactions marked as doesExist = false when replaced (prevents double-counting)
  3. RBF detection prevents showing duplicate received sheets

Recommendations

  1. Transaction Validation - Consider adding validation that conflicts array in handleOnchainTransactionReplaced actually contains valid transaction IDs to prevent malformed data corruption.

  2. Timestamp Validation - In processOnchainPayment, ensure timestamps are within reasonable ranges to prevent time-based attacks.


Test Coverage

Missing Test Scenarios

  1. RBF Handling - Need tests for single replacement, multiple conflicts, chain of replacements, replacement arriving before original

  2. Reorg Handling - Need tests for confirmed tx becoming unconfirmed, multiple txs affected by reorg, reorg depth > 1 block

  3. Edge Cases - Need tests for tx evicted from mempool that was not replaced, race conditions, cache invalidation scenarios

Testing Recommendations

Add integration tests that simulate these event sequences:

  • onchainTransactionReceived → onchainTransactionReplaced → onchainTransactionConfirmed
  • onchainTransactionConfirmed → onchainTransactionReorged
  • onchainTransactionReceived → onchainTransactionEvicted

Final Recommendations

Must Fix (Before Merge)

  1. Run swiftformat . to ensure code formatting compliance
  2. Fix cache invalidation in update method
  3. Add race condition protection in handleOnchainTransactionReplaced
  4. Verify event handler cleanup to prevent memory leaks

Should Fix (High Priority)

  1. Optimize refreshBoostTxIdsCache for large transaction histories
  2. Add comprehensive tests for RBF and reorg scenarios
  3. Review the 500ms delay in received sheet display

Nice to Have

  1. Add transaction ID truncation to user-facing messages
  2. Improve user-friendly language for technical events (reorg, eviction)
  3. Add metrics/telemetry for tracking event frequencies

Conclusion

This is a high-quality PR that represents a significant architectural improvement. The event-driven approach is the correct design pattern and will eliminate polling overhead. The code quality is generally excellent with proper error handling, logging, and async patterns.

Recommendation:Approve with minor changes. Address the cache invalidation and race condition issues before merge, and plan follow-up work for comprehensive testing and performance optimization.

Great work! The videos demonstrating reorg and eviction handling are particularly impressive.

pwltr
pwltr previously approved these changes Nov 27, 2025
@ovitrif
ovitrif enabled auto-merge November 27, 2025 18:26
@ovitrif
ovitrif disabled auto-merge November 27, 2025 18:26
ovitrif
ovitrif previously approved these changes Nov 27, 2025

@ovitrifovitrif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

tAck

now it really shows the boosted TX update instantly every time I try it

@ovitrif
ovitrif dismissed stale reviews from pwltr and themself via 4b9083dNovember 27, 2025 22:39
@claude

claudeBot commented Nov 27, 2025

Copy link
Copy Markdown
Contributor

PR Review: Integrate LDK onchain events

Summary

This PR successfully replaces polling-based transaction monitoring with event-driven LDK onchain events, removes the AddressChecker, and improves RBF transaction handling. This is a significant architectural improvement that makes the app more reactive and efficient.

✅ Strengths

Architecture & Design

  1. Event-driven architecture: The transition from polling to LDK events (onchainTransactionReceived, onchainTransactionConfirmed, onchainTransactionReplaced, onchainTransactionReorged, onchainTransactionEvicted) is excellent. This provides real-time updates and reduces unnecessary syncs.

  2. Comprehensive RBF handling: The implementation properly tracks replacement transactions through boostTxIds and distinguishes between RBF (replaced transactions with doesExist=false) and CPFP (child transactions with doesExist=true).

  3. Clean separation of concerns: Event handlers in LightningService.swift (708-757) delegate to ActivityService methods, maintaining good separation between Lightning layer and activity management.

  4. Improved user feedback: New toast notifications with accessibility identifiers for transaction states (replaced, reorged, evicted) provide better UX.

  5. Removal of polling: Removing the onChange(of: wallet.totalBalanceSats) trigger in AppScene.swift:217 and the event-based sync in AppScene.swift:217-220 eliminates redundant activity syncing.

Code Quality

  1. Good error handling: All event handlers wrap operations in try-catch blocks with proper logging.
  2. Proper concurrency: Uses ServiceQueue.background(.core) for all database operations.
  3. Cache optimization: The cachedTxIdsInBoostTxIds cache improves performance for checking replaced transactions.

⚠️ Issues & Concerns

Critical

1. Potential cache inconsistency in CoreService.swift:36-43

func getTxIdsInBoostTxIds()async->Set<String>{if cachedTxIdsInBoostTxIds.isEmpty {awaitrefreshBoostTxIdsCache()}return cachedTxIdsInBoostTxIds
}

Issue: The cache is only refreshed when empty, but individual updates via updateBoostTxIdsCache(for:) might not be called for all updates. The update() method at line 195 doesn't update the cache.

Recommendation:

  • Update the cache in the update() method, or
  • Implement a TTL-based cache invalidation strategy, or
  • Document why the cache only needs to be refreshed when empty

2. Race condition in RBF handling (CoreService.swift:355-441)

When handleOnchainTransactionReplaced is called, it:

  1. Marks the replaced transaction as doesExist=false
  2. Processes replacement transactions that might not exist yet in the payment store
  3. Waits for them via processOnchainPayment

Issue: There's a timing window where a replacement transaction event arrives before LDK updates the payment store. The code handles this (line 385-403) by processing the payment, but there's a potential race if the transaction hasn't been added to the payments list yet.

Recommendation: Consider adding retry logic or ensuring LDK guarantees payment store updates before emitting replacement events.

Moderate

3. Missing cache update in update() method (CoreService.swift:193-199)

func update(id:String, activity:Activity)asyncthrows{tryawaitServiceQueue.background(.core){tryupdateActivity(activityId: id, activity: activity)self.metadataChangedSubject.send()}}

This doesn't call updateBoostTxIdsCache(for: activity) like insert() does at line 172.

Recommendation: Add cache update to maintain consistency.

4. Incomplete validation in shouldShowReceivedSheet (CoreService.swift:76-107)

The method checks if a replacement transaction has the same value as the replaced transaction to avoid showing duplicate sheets. However:

  • It only checks exact value matches
  • Doesn't account for fee differences in RBF
  • Could show sheets for CPFP where the child transaction might have a different value

Recommendation: Consider checking isBoosted flag and the nature of the boost (RBF vs CPFP) for more precise logic.

5. Error handling in handleOnchainTransactionReplaced (CoreService.swift:355-441)

When processing replacement transactions, errors are caught and logged (line 401-405), but the loop continues. If multiple replacement transactions fail to process, the relationship tracking could be incomplete.

Recommendation: Consider collecting errors and either throwing or returning them to the caller for better error visibility.

6. CI workflow changes remove caching (integration-tests.yml, unit-tests.yml)

The PR removes DerivedData caching and restore-keys from SPM cache. While this ensures clean builds, it will increase build times significantly.

Recommendation:

  • Document why caching was removed (likely due to cache invalidation issues)
  • Consider using Xcode Cloud or other CI optimizations to offset the performance loss
  • The -onlyUsePackageVersionsFromResolvedFile flag is good for reproducibility

Minor

7. Potential N+1 query in getBoostTxDoesExist (CoreService.swift:130-138)

func getBoostTxDoesExist(boostTxIds:[String])async->[String:Bool]{vardoesExistMap:[String:Bool]=[:]forboostTxIdin boostTxIds {
if let boostActivity =try?awaitgetOnchainActivityByTxId(txid: boostTxId){doesExistMap[boostTxId]= boostActivity.doesExist
}}return doesExistMap
}

Issue: Makes individual database queries for each boost transaction.

Recommendation: Consider adding a batch query method to BitkitCore to fetch multiple activities by txId in one query.

8. Magic number delay in AppViewModel.swift:399

try?awaitTask.sleep(nanoseconds:500_000_000) // 500ms delay

Recommendation: Extract to a named constant with documentation explaining why the delay is necessary.

9. Missing test coverage

While TxBumpingTests.swift and ActivityListTest.swift exist, there don't appear to be specific tests for:

  • Onchain event handlers
  • RBF replacement tracking with boostTxIds
  • Reorg and eviction scenarios
  • Cache consistency

Recommendation: Add integration tests for the new event handlers.

🔒 Security

No major security concerns identified. The code properly:

  • Uses ServiceQueue for thread safety
  • Doesn't expose sensitive transaction data in logs beyond txids
  • Maintains Bitcoin/Lightning operation isolation in the service layer

Minor: Logging in LightningService.swift:711-757 includes transaction amounts. Consider whether this is acceptable for production logging.

🚀 Performance

Positive impacts:

  1. Eliminates redundant polling and full activity list syncs
  2. Event-driven updates only process changed transactions
  3. Cache for boostTxIds lookups

Potential concerns:

  1. Cache not updated consistently (see issue Project setup #1, Migrate channels from LDK to LDK-node #3)
  2. N+1 queries in getBoostTxDoesExist (see issue feat: keychain #7)

🧪 Testing

Existing test coverage:

  • ActivityListTest.swift: Good coverage of basic activity CRUD operations
  • TxBumpingTests.swift: Tests RBF scenarios

Missing:

  • Tests for new onchain event handlers
  • Tests for handleOnchainTransactionReplaced, handleOnchainTransactionReorged, handleOnchainTransactionEvicted
  • Tests for cache consistency
  • Integration tests for the complete event flow

Recommendation: Add integration tests that simulate the event sequences shown in the PR videos.

📝 Recommendations

Must Fix Before Merge

  1. Fix cache consistency issues (Issues Project setup #1, Migrate channels from LDK to LDK-node #3)
  2. Add error handling for race conditions in RBF handling (Issue Push service setup and service queues  #2)

Should Fix Before Merge

  1. Improve shouldShowReceivedSheet validation logic (Issue Create swift.yml #4)
  2. Better error aggregation in handleOnchainTransactionReplaced (Issue Create ios.yml #5)

Consider for Follow-up

  1. Add batch query support for getBoostTxDoesExist (Issue feat: keychain #7)
  2. Add integration tests for event handlers (Issue Use SwiftFormat #9)
  3. Extract magic constants (Issue BDK 1.0.0-alpha.11 #8)
  4. Document CI caching removal reasoning (Issue Create ios.yml #6)

🎯 Overall Assessment

This is a high-quality PR that represents a significant architectural improvement. The event-driven approach is the correct design, and the RBF handling logic is well thought out. The main concerns are around cache consistency and potential race conditions in RBF processing, which should be addressed before merge.

The removal of AddressChecker and polling-based syncing will improve app performance and responsiveness.

Recommendation: Request changes to address cache consistency and race condition handling, then approve after fixes.


Great work on this PR! The demos showing reorg and mempool eviction handling are particularly impressive. Once the cache and race condition issues are resolved, this will be a solid improvement to the codebase.

@ovitrifovitrif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

tAck

now it really shows the boosted TX update instantly every time

@pwltrpwltr linked an issue Nov 28, 2025 that may be closed by this pull request
@ovitrif

Copy link
Copy Markdown
Collaborator

Android PR should succeed e2e once Electrum on staging is fixed. Merging this.

1 similar comment
@ovitrif

Copy link
Copy Markdown
Collaborator

Android PR should succeed e2e once Electrum on staging is fixed. Merging this.

@ovitrif
ovitrif merged commit 4b25919 into masterNov 28, 2025
10 checks passed
@ovitrif
ovitrif deleted the feat/onchain-events branch November 28, 2025 23:14
BitcoinErrorLog pushed a commit to BitcoinErrorLog/bitkit-ios that referenced this pull request Dec 15, 2025
BitcoinErrorLog pushed a commit to BitcoinErrorLog/bitkit-ios that referenced this pull request Feb 7, 2026
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.

[Bug]: Polish RBF & CPFP activity list behaviour

6 participants

@ben-kaufman@ovitrif@piotr-iohk@pwltr@jvsena42
, '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

Integrate LDK onchain events - #245

Merged
ovitrif merged 20 commits into
masterfrom
feat/onchain-events
Nov 28, 2025
Merged

Integrate LDK onchain events#245
ovitrif merged 20 commits into
masterfrom
feat/onchain-events

Conversation

@ben-kaufman

@ben-kaufmanben-kaufman commented Nov 26, 2025

Copy link
Copy Markdown
Contributor

This PR integrates LDK node on chain events to replace the previous pollin and full syncs.

It also removes Address Checker, and improves handling on RBFed transactions to be recognized on both sides.

Testing:
Should test all actions, send, receive (onchain and LN), transfer to savings, transfer to spendings, etc.

Screen.Recording.2025-11-26.at.9.05.11.AM.mov

Reorg removes tx from blocks demo:

Screen.Recording.2025-11-26.at.5.13.45.PM.mov

Transaction removed from mempool demo:

Screen.Recording.2025-11-26.at.5.38.36.PM.mov

@claude

This comment was marked as outdated.

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 integrates LDK node onchain events to replace previous polling and full sync mechanisms for Bitcoin transaction management. It removes the AddressChecker utility class that relied on external API calls, and improves handling of RBF (Replace-By-Fee) transactions to properly distinguish them from CPFP (Child-Pays-For-Parent) transactions on both sender and receiver sides.

Key changes:

  • Event-driven architecture for onchain transaction updates (received, confirmed, replaced, reorged, evicted)
  • Removal of 1-second polling timer in favor of real-time LDK events
  • Enhanced RBF transaction detection using doesExist flag to differentiate from CPFP

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
BitkitNotification/NotificationService.swiftAdded notification handling for new onchain transaction events
Bitkit/Views/Wallets/Sheets/BoostSheet.swiftRemoved redundant LDK node payment sync after boost operation
Bitkit/Views/Wallets/Activity/ActivityItemView.swiftEnhanced boost button logic to differentiate between RBF and CPFP transactions
Bitkit/Views/Wallets/Activity/ActivityExplorerView.swiftReplaced AddressChecker API calls with LDK node transaction details
Bitkit/ViewModels/WalletViewModel.swiftRemoved polling mechanism and added event-driven state updates for onchain transactions
Bitkit/ViewModels/AppViewModel.swiftAdded comprehensive event handling for onchain transaction lifecycle
Bitkit/Utilities/AddressChecker.swiftRemoved external API-dependent utility in favor of native LDK functionality
Bitkit/Services/LightningService.swiftAdded transaction detail methods and event handlers for onchain events
Bitkit/Services/CoreService.swiftRefactored payment processing to handle LDK events and removed replacement tracking maps
Bitkit/AppScene.swiftRemoved balance change observer and event-based activity sync
Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolvedUpdated bitkit-core and ldk-node dependencies
Bitkit.xcodeproj/project.pbxprojUpdated project references for LDK package

Comment threadBitkit/Services/CoreService.swift
Comment threadBitkit/Services/CoreService.swift
Comment threadBitkit/Services/CoreService.swift
@claude

This comment was marked as outdated.

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

@piotr-iohk please note we will need to update the e2e tests to close the onchain received sheet.

@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

ovitrif
ovitrif previously approved these changes Nov 26, 2025

@ovitrifovitrif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

utAck

Amazing work, I've been waiting for over a year to finally have this setup in place: event-based sync for transactions 🎉

Added a few small nit comments, the one about the localizable strings applies to all cases where we added new toast messages, not only the code where I added it.

I wanted to give you the time to already make some small updates as I am starting to test this, hence me approving without testing; but I trust you'll wait for my testing 🙏🏻

Comment threadBitkit/ViewModels/AppViewModel.swift Outdated
Comment threadBitkitNotification/NotificationService.swift
@claude

This comment was marked as outdated.

@claude

This comment was marked as outdated.

Comment threadBitkit/Resources/Localization/en.lproj/Localizable.strings Outdated
@claude

This comment was marked as outdated.

@piotr-iohk

Copy link
Copy Markdown
Collaborator

Ok, updated e2e tests for current state (synonymdev/bitkit-e2e-tests#56). Re-running 🤞
It would be good to coordinate merging with Android (synonymdev/bitkit-android#475)

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

@pwltr it's not due to race condition, it's just because the event of the new tx takes a few seconds to come in from the LDK node.

We can solve it by immediately caching the "intermediate" state in memory until the event comes in.

@pwltr

Copy link
Copy Markdown
Contributor

@pwltr it's not due to race condition, it's just because the event of the new tx takes a few seconds to come in from the LDK node.

We can solve it by immediately caching the "intermediate" state in memory until the event comes in.

Sounds good yeah I think an optimistic UI like this would be the way to go. Assume everything went right and correct if needed when the event(s) come in from LDK. Still feel free to postpone for now.

@ben-kaufman

Copy link
Copy Markdown
ContributorAuthor

Actually had a simpler solution, fixed in: #247

pwltr
pwltr previously approved these changes Nov 27, 2025

@pwltrpwltr 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.

tACK with follow-up task here

Also note @piotr-iohk's request to merge this together with the Android counterpart.

@ben-kaufman
ben-kaufman dismissed stale reviews from pwltr, ovitrif, and jvsena42 via ab36651November 27, 2025 17:01
@claude

claudeBot commented Nov 27, 2025

Copy link
Copy Markdown
Contributor

PR Review: Integrate LDK onchain events

This is an excellent architectural improvement that modernizes the onchain transaction handling by leveraging LDK's event-driven architecture. The change from polling to event-driven updates should significantly improve performance and user experience.


Summary of Changes

Major Improvements:

  • Event-driven onchain transaction handling via LDK Node events
  • Removed AddressChecker utility (110 lines removed)
  • Enhanced RBF transaction tracking and handling
  • Added comprehensive onchain event handling (received, confirmed, replaced, reorged, evicted)
  • Improved user notifications for transaction state changes
  • Better handling of edge cases (reorgs, mempool evictions)

Impact:

  • 1,031 additions, 495 deletions across 20 files
  • Core architectural change in CoreService.swift (+529/-273)
  • Enhanced event handling in AppViewModel.swift (+108/-4)
  • Improved LightningService.swift integration (+120/-3)

Code Quality & Best Practices

Strengths

  1. Event-Driven Architecture - Proper use of LDK onchain events eliminates polling overhead
  2. Comprehensive Error Handling - Good use of try-catch blocks with descriptive logging
  3. Service Queue Pattern - Correctly uses ServiceQueue.background(.core) for Core operations
  4. MainActor Compliance - Proper async/await patterns with @mainactor for UI updates
  5. Caching Strategy - cachedTxIdsInBoostTxIds improves performance for boost transaction lookups
  6. Detailed Logging - Excellent context-aware logging throughout
  7. Accessibility - Added accessibilityIdentifier to Toast model and components

Code Organization

The new event handlers follow a clean pattern:

  • handleOnchainTransactionReceived
  • handleOnchainTransactionConfirmed
  • handleOnchainTransactionReplaced
  • handleOnchainTransactionReorged
  • handleOnchainTransactionEvicted

Each handler properly delegates to ServiceQueue.background(.core) and updates the activity list appropriately.


Potential Bugs & Issues

1. Race Condition in handleOnchainTransactionReplaced

Location: CoreService.swift:346-433

Issue: When processing replacement transactions, there is a potential race condition between checking if replacementActivity exists and processing the payment. Another event handler could process the same transaction concurrently.

Recommendation: Add a lock or transaction ID tracking to prevent concurrent processing of the same transaction.

2. Cache Invalidation Issue

Location: CoreService.swift:34-62

Issue: The cachedTxIdsInBoostTxIds cache is updated in insert and bulk refreshed in upsertList, but individual update operations don't update the cache. This could lead to stale cache data.

Recommendation: Add cache update to the update method to keep cache consistent.

3. Potential Memory Leak in Event Handlers

Location: AppScene.swift:213-215

Issue: The event handler closure captures app weakly, but the closure itself is never removed when the scene is dismissed.

Recommendation: Ensure event handlers are removed when the scene is dismissed. The addOnEvent API should support removal, or use a cleanup mechanism in deinit or onDisappear.

4. Missing nil-check in processOnchainTransaction

Location: CoreService.swift:315-332

Issue: The function logs a warning if payment is not found but doesn't throw an error. The calling code may not handle this gracefully.

Recommendation: Consider throwing a specific error so callers can decide how to handle missing payments, or add metrics to track how often this occurs.


Performance Considerations

Strengths

  1. Eliminated Polling - Removing AddressChecker and full syncs significantly reduces CPU/network overhead
  2. Efficient Caching - cachedTxIdsInBoostTxIds prevents repeated database queries
  3. Lazy Cache Initialization - Cache is only built when first accessed

Concerns

  1. Cache Refresh Performance - CoreService.swift:47-62 loads ALL onchain activities from the database, which could be slow for users with large transaction histories (1000+ transactions). Consider a more efficient query that only fetches boostTxIds without loading full activity objects.

  2. Delay in Received Sheet - AppViewModel.swift:399 has a 500ms delay. Is this necessary? It could make the UI feel sluggish. Consider removing or reducing if not strictly required.


Security Concerns

Good Practices

  1. Proper use of ServiceQueue prevents concurrent access to Core/LDK
  2. Transactions marked as doesExist = false when replaced (prevents double-counting)
  3. RBF detection prevents showing duplicate received sheets

Recommendations

  1. Transaction Validation - Consider adding validation that conflicts array in handleOnchainTransactionReplaced actually contains valid transaction IDs to prevent malformed data corruption.

  2. Timestamp Validation - In processOnchainPayment, ensure timestamps are within reasonable ranges to prevent time-based attacks.


Test Coverage

Missing Test Scenarios

  1. RBF Handling - Need tests for single replacement, multiple conflicts, chain of replacements, replacement arriving before original

  2. Reorg Handling - Need tests for confirmed tx becoming unconfirmed, multiple txs affected by reorg, reorg depth > 1 block

  3. Edge Cases - Need tests for tx evicted from mempool that was not replaced, race conditions, cache invalidation scenarios

Testing Recommendations

Add integration tests that simulate these event sequences:

  • onchainTransactionReceived → onchainTransactionReplaced → onchainTransactionConfirmed
  • onchainTransactionConfirmed → onchainTransactionReorged
  • onchainTransactionReceived → onchainTransactionEvicted

Final Recommendations

Must Fix (Before Merge)

  1. Run swiftformat . to ensure code formatting compliance
  2. Fix cache invalidation in update method
  3. Add race condition protection in handleOnchainTransactionReplaced
  4. Verify event handler cleanup to prevent memory leaks

Should Fix (High Priority)

  1. Optimize refreshBoostTxIdsCache for large transaction histories
  2. Add comprehensive tests for RBF and reorg scenarios
  3. Review the 500ms delay in received sheet display

Nice to Have

  1. Add transaction ID truncation to user-facing messages
  2. Improve user-friendly language for technical events (reorg, eviction)
  3. Add metrics/telemetry for tracking event frequencies

Conclusion

This is a high-quality PR that represents a significant architectural improvement. The event-driven approach is the correct design pattern and will eliminate polling overhead. The code quality is generally excellent with proper error handling, logging, and async patterns.

Recommendation:Approve with minor changes. Address the cache invalidation and race condition issues before merge, and plan follow-up work for comprehensive testing and performance optimization.

Great work! The videos demonstrating reorg and eviction handling are particularly impressive.

pwltr
pwltr previously approved these changes Nov 27, 2025
@ovitrif
ovitrif enabled auto-merge November 27, 2025 18:26
@ovitrif
ovitrif disabled auto-merge November 27, 2025 18:26
ovitrif
ovitrif previously approved these changes Nov 27, 2025

@ovitrifovitrif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

tAck

now it really shows the boosted TX update instantly every time I try it

@ovitrif
ovitrif dismissed stale reviews from pwltr and themself via 4b9083dNovember 27, 2025 22:39
@claude

claudeBot commented Nov 27, 2025

Copy link
Copy Markdown
Contributor

PR Review: Integrate LDK onchain events

Summary

This PR successfully replaces polling-based transaction monitoring with event-driven LDK onchain events, removes the AddressChecker, and improves RBF transaction handling. This is a significant architectural improvement that makes the app more reactive and efficient.

✅ Strengths

Architecture & Design

  1. Event-driven architecture: The transition from polling to LDK events (onchainTransactionReceived, onchainTransactionConfirmed, onchainTransactionReplaced, onchainTransactionReorged, onchainTransactionEvicted) is excellent. This provides real-time updates and reduces unnecessary syncs.

  2. Comprehensive RBF handling: The implementation properly tracks replacement transactions through boostTxIds and distinguishes between RBF (replaced transactions with doesExist=false) and CPFP (child transactions with doesExist=true).

  3. Clean separation of concerns: Event handlers in LightningService.swift (708-757) delegate to ActivityService methods, maintaining good separation between Lightning layer and activity management.

  4. Improved user feedback: New toast notifications with accessibility identifiers for transaction states (replaced, reorged, evicted) provide better UX.

  5. Removal of polling: Removing the onChange(of: wallet.totalBalanceSats) trigger in AppScene.swift:217 and the event-based sync in AppScene.swift:217-220 eliminates redundant activity syncing.

Code Quality

  1. Good error handling: All event handlers wrap operations in try-catch blocks with proper logging.
  2. Proper concurrency: Uses ServiceQueue.background(.core) for all database operations.
  3. Cache optimization: The cachedTxIdsInBoostTxIds cache improves performance for checking replaced transactions.

⚠️ Issues & Concerns

Critical

1. Potential cache inconsistency in CoreService.swift:36-43

func getTxIdsInBoostTxIds()async->Set<String>{if cachedTxIdsInBoostTxIds.isEmpty {awaitrefreshBoostTxIdsCache()}return cachedTxIdsInBoostTxIds
}

Issue: The cache is only refreshed when empty, but individual updates via updateBoostTxIdsCache(for:) might not be called for all updates. The update() method at line 195 doesn't update the cache.

Recommendation:

  • Update the cache in the update() method, or
  • Implement a TTL-based cache invalidation strategy, or
  • Document why the cache only needs to be refreshed when empty

2. Race condition in RBF handling (CoreService.swift:355-441)

When handleOnchainTransactionReplaced is called, it:

  1. Marks the replaced transaction as doesExist=false
  2. Processes replacement transactions that might not exist yet in the payment store
  3. Waits for them via processOnchainPayment

Issue: There's a timing window where a replacement transaction event arrives before LDK updates the payment store. The code handles this (line 385-403) by processing the payment, but there's a potential race if the transaction hasn't been added to the payments list yet.

Recommendation: Consider adding retry logic or ensuring LDK guarantees payment store updates before emitting replacement events.

Moderate

3. Missing cache update in update() method (CoreService.swift:193-199)

func update(id:String, activity:Activity)asyncthrows{tryawaitServiceQueue.background(.core){tryupdateActivity(activityId: id, activity: activity)self.metadataChangedSubject.send()}}

This doesn't call updateBoostTxIdsCache(for: activity) like insert() does at line 172.

Recommendation: Add cache update to maintain consistency.

4. Incomplete validation in shouldShowReceivedSheet (CoreService.swift:76-107)

The method checks if a replacement transaction has the same value as the replaced transaction to avoid showing duplicate sheets. However:

  • It only checks exact value matches
  • Doesn't account for fee differences in RBF
  • Could show sheets for CPFP where the child transaction might have a different value

Recommendation: Consider checking isBoosted flag and the nature of the boost (RBF vs CPFP) for more precise logic.

5. Error handling in handleOnchainTransactionReplaced (CoreService.swift:355-441)

When processing replacement transactions, errors are caught and logged (line 401-405), but the loop continues. If multiple replacement transactions fail to process, the relationship tracking could be incomplete.

Recommendation: Consider collecting errors and either throwing or returning them to the caller for better error visibility.

6. CI workflow changes remove caching (integration-tests.yml, unit-tests.yml)

The PR removes DerivedData caching and restore-keys from SPM cache. While this ensures clean builds, it will increase build times significantly.

Recommendation:

  • Document why caching was removed (likely due to cache invalidation issues)
  • Consider using Xcode Cloud or other CI optimizations to offset the performance loss
  • The -onlyUsePackageVersionsFromResolvedFile flag is good for reproducibility

Minor

7. Potential N+1 query in getBoostTxDoesExist (CoreService.swift:130-138)

func getBoostTxDoesExist(boostTxIds:[String])async->[String:Bool]{vardoesExistMap:[String:Bool]=[:]forboostTxIdin boostTxIds {
if let boostActivity =try?awaitgetOnchainActivityByTxId(txid: boostTxId){doesExistMap[boostTxId]= boostActivity.doesExist
}}return doesExistMap
}

Issue: Makes individual database queries for each boost transaction.

Recommendation: Consider adding a batch query method to BitkitCore to fetch multiple activities by txId in one query.

8. Magic number delay in AppViewModel.swift:399

try?awaitTask.sleep(nanoseconds:500_000_000) // 500ms delay

Recommendation: Extract to a named constant with documentation explaining why the delay is necessary.

9. Missing test coverage

While TxBumpingTests.swift and ActivityListTest.swift exist, there don't appear to be specific tests for:

  • Onchain event handlers
  • RBF replacement tracking with boostTxIds
  • Reorg and eviction scenarios
  • Cache consistency

Recommendation: Add integration tests for the new event handlers.

🔒 Security

No major security concerns identified. The code properly:

  • Uses ServiceQueue for thread safety
  • Doesn't expose sensitive transaction data in logs beyond txids
  • Maintains Bitcoin/Lightning operation isolation in the service layer

Minor: Logging in LightningService.swift:711-757 includes transaction amounts. Consider whether this is acceptable for production logging.

🚀 Performance

Positive impacts:

  1. Eliminates redundant polling and full activity list syncs
  2. Event-driven updates only process changed transactions
  3. Cache for boostTxIds lookups

Potential concerns:

  1. Cache not updated consistently (see issue Project setup #1, Migrate channels from LDK to LDK-node #3)
  2. N+1 queries in getBoostTxDoesExist (see issue feat: keychain #7)

🧪 Testing

Existing test coverage:

  • ActivityListTest.swift: Good coverage of basic activity CRUD operations
  • TxBumpingTests.swift: Tests RBF scenarios

Missing:

  • Tests for new onchain event handlers
  • Tests for handleOnchainTransactionReplaced, handleOnchainTransactionReorged, handleOnchainTransactionEvicted
  • Tests for cache consistency
  • Integration tests for the complete event flow

Recommendation: Add integration tests that simulate the event sequences shown in the PR videos.

📝 Recommendations

Must Fix Before Merge

  1. Fix cache consistency issues (Issues Project setup #1, Migrate channels from LDK to LDK-node #3)
  2. Add error handling for race conditions in RBF handling (Issue Push service setup and service queues  #2)

Should Fix Before Merge

  1. Improve shouldShowReceivedSheet validation logic (Issue Create swift.yml #4)
  2. Better error aggregation in handleOnchainTransactionReplaced (Issue Create ios.yml #5)

Consider for Follow-up

  1. Add batch query support for getBoostTxDoesExist (Issue feat: keychain #7)
  2. Add integration tests for event handlers (Issue Use SwiftFormat #9)
  3. Extract magic constants (Issue BDK 1.0.0-alpha.11 #8)
  4. Document CI caching removal reasoning (Issue Create ios.yml #6)

🎯 Overall Assessment

This is a high-quality PR that represents a significant architectural improvement. The event-driven approach is the correct design, and the RBF handling logic is well thought out. The main concerns are around cache consistency and potential race conditions in RBF processing, which should be addressed before merge.

The removal of AddressChecker and polling-based syncing will improve app performance and responsiveness.

Recommendation: Request changes to address cache consistency and race condition handling, then approve after fixes.


Great work on this PR! The demos showing reorg and mempool eviction handling are particularly impressive. Once the cache and race condition issues are resolved, this will be a solid improvement to the codebase.

@ovitrifovitrif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

tAck

now it really shows the boosted TX update instantly every time

@pwltrpwltr linked an issue Nov 28, 2025 that may be closed by this pull request
@ovitrif

Copy link
Copy Markdown
Collaborator

Android PR should succeed e2e once Electrum on staging is fixed. Merging this.

1 similar comment
@ovitrif

Copy link
Copy Markdown
Collaborator

Android PR should succeed e2e once Electrum on staging is fixed. Merging this.

@ovitrif
ovitrif merged commit 4b25919 into masterNov 28, 2025
10 checks passed
@ovitrif
ovitrif deleted the feat/onchain-events branch November 28, 2025 23:14
BitcoinErrorLog pushed a commit to BitcoinErrorLog/bitkit-ios that referenced this pull request Dec 15, 2025
BitcoinErrorLog pushed a commit to BitcoinErrorLog/bitkit-ios that referenced this pull request Feb 7, 2026
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.

[Bug]: Polish RBF & CPFP activity list behaviour

6 participants

@ben-kaufman@ovitrif@piotr-iohk@pwltr@jvsena42