This guide covers the new event system added to LDK Node that provides real-time notifications for wallet and Lightning channel state changes. These events eliminate the need for polling and provide instant updates for better user experience and battery efficiency.
- New Event Types
- iOS/Swift Implementation
- Android/Kotlin Implementation
- Migration Guide
- Best Practices
- Testing Recommendations
These events notify you about Bitcoin transactions affecting your onchain wallet:
- When: New unconfirmed transaction detected in mempool
- Use Case: Show "Payment incoming!" notification immediately
- Fields:
txid: Transaction IDdetails:TransactionDetailsobject with comprehensive transaction information
- When: Transaction receives blockchain confirmations
- Use Case: Update transaction status from pending to confirmed
- Fields:
txid: Transaction IDblockHash: Block hash where confirmedblockHeight: Block heightconfirmationTime: Unix timestampdetails:TransactionDetailsobject with comprehensive transaction information
- When: Transaction is replaced via Replace-By-Fee (RBF)
- Use Case: Update UI to show the transaction was replaced
- Fields:
txid: Transaction ID of the transaction that was replaced (the old transaction)conflicts: Array of transaction IDs that replaced this transaction (the replacement transactions)
- When: Previously confirmed transaction becomes unconfirmed due to blockchain reorg
- Use Case: Mark transaction as pending again
- Fields:
txid: Transaction ID
- When: Transaction is evicted from the mempool (no longer unconfirmed and not confirmed)
- Use Case: Mark transaction as evicted, potentially allow user to rebroadcast
- Fields:
txid: Transaction ID
- Note: Works with all chain sources (Esplora, Electrum, and BitcoindRpc)
Track synchronization progress and completion:
- When: Periodically during sync operations
- Use Case: Show progress bar during sync
- Fields:
syncType: What's syncing (OnchainWallet, LightningWallet, FeeRateCache)progressPercent: 0-100currentBlockHeight: Current sync positiontargetBlockHeight: Target height
- When: Sync operation finishes successfully
- Use Case: Hide progress indicators, enable UI
- Fields:
syncType: What completed syncingsyncedBlockHeight: Final synced height
- When: Onchain or Lightning balance changes
- Use Case: Update balance display immediately
- Fields:
oldSpendableOnchainBalanceSats: Previous spendable onchain balancenewSpendableOnchainBalanceSats: New spendable onchain balanceoldTotalOnchainBalanceSats: Previous total onchain (including unconfirmed)newTotalOnchainBalanceSats: New total onchainoldTotalLightningBalanceSats: Previous Lightning balancenewTotalLightningBalanceSats: New Lightning balance
The TransactionDetails struct provides comprehensive information about transactions, allowing you to analyze transaction purposes yourself:
amountSats: Net amount in satoshis (positive for incoming, negative for outgoing)inputs: Array ofTxInputobjectsoutputs: Array ofTxOutputobjects
txid: Previous transaction ID being spentvout: Output index being spentscriptsig: Script signature (hex-encoded)witness: Witness stack (array of hex-encoded strings)sequence: Sequence number
scriptpubkey: Script public key (hex-encoded)scriptpubkeyType: Script type (e.g., "p2wpkh", "p2wsh", "p2tr")scriptpubkeyAddress: Bitcoin address (if decodable)value: Value in satoshisn: Output index
Note: You can analyze transaction inputs and outputs to detect channel funding, channel closures, and other transaction types. This provides more flexibility than the previous TransactionContext enum.
You can also retrieve transaction details directly using Node::get_transaction_details():
// Get transaction details for a specific transaction ID
if let details = node.getTransactionDetails(txid: txid) {
// Analyze the transaction details
print("Transaction amount: \(details.amountSats) sats")
print("Number of inputs: \(details.inputs.count)")
print("Number of outputs: \(details.outputs.count)")
} else {
// Transaction not found in wallet
print("Transaction not found")
}This method returns nil if the transaction is not found in the wallet.
You can retrieve the current balance for any Bitcoin address using Node::get_address_balance():
// Get balance for a Bitcoin address
do {
let balance = try node.getAddressBalance(addressStr: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh")
print("Address balance: \(balance) sats")
} catch {
// Invalid address or network mismatch
print("Error: \(error)")
}// Get balance for a Bitcoin address
try {
val balance = node.getAddressBalance("bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh")
println("Address balance: $balance sats")
} catch (e: Exception) {
// Invalid address or network mismatch
println("Error: ${e.message}")
}Note: This method queries the chain source directly and returns the balance in satoshis. It throws an error if the address string cannot be parsed or doesn't match the node's network. It returns 0 if the balance cannot be queried (e.g., chain source unavailable). This method is not available when using BitcoindRpc as the chain source.
import LDKNode
class WalletEventHandler {
private let node: Node
private var eventTimer: Timer?
init(node: Node) {
self.node = node
startEventHandling()
}
func startEventHandling() {
// Check for events every 100ms
eventTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
self.processNextEvent()
}
}
func processNextEvent() {
guard let event = node.nextEvent() else { return }
handleEvent(event)
node.eventHandled()
}
func handleEvent(_ event: Event) {
switch event {
case .onchainTransactionReceived(let txid, let details):
handleIncomingTransaction(txid: txid, details: details)
case .onchainTransactionConfirmed(let txid, let blockHash, let blockHeight, let confirmationTime, let details):
handleConfirmedTransaction(txid: txid, height: blockHeight, details: details)
case .onchainTransactionReplaced(let txid, let conflicts):
handleReplacedTransaction(txid: txid, conflicts: conflicts)
case .onchainTransactionReorged(let txid):
handleReorgedTransaction(txid: txid)
case .onchainTransactionEvicted(let txid):
handleEvictedTransaction(txid: txid)
case .syncProgress(let syncType, let progressPercent, let currentBlock, let targetBlock):
updateSyncProgress(type: syncType, percent: progressPercent)
case .syncCompleted(let syncType, let syncedHeight):
handleSyncCompleted(type: syncType, height: syncedHeight)
case .balanceChanged(let oldSpendable, let newSpendable, let oldTotal, let newTotal, let oldLightning, let newLightning):
updateBalances(onchain: newSpendable, lightning: newLightning)
default:
// Handle other existing events
break
}
}
}func handleIncomingTransaction(txid: String, details: TransactionDetails) {
DispatchQueue.main.async {
if details.amountSats > 0 {
// Incoming payment
self.showNotification(
title: "Payment Received!",
body: "Incoming payment of \(details.amountSats) sats (unconfirmed)",
txid: txid
)
// Update UI to show pending transaction
self.addPendingTransaction(txid: txid, amount: details.amountSats)
// Play sound or haptic feedback
self.playPaymentReceivedSound()
} else {
// Outgoing payment
self.updateTransactionStatus(txid: txid, status: .broadcasting)
}
// Analyze transaction to detect channel-related transactions
if self.isChannelFundingTransaction(details: details) {
print("Channel funding transaction detected")
} else if self.isChannelClosureTransaction(details: details) {
print("Channel closure transaction detected")
}
}
}
func handleConfirmedTransaction(txid: String, height: UInt32, details: TransactionDetails) {
DispatchQueue.main.async {
// Update transaction status
self.updateTransactionStatus(txid: txid, status: .confirmed(height: height))
// Show confirmation notification
self.showNotification(
title: "Payment Confirmed!",
body: "Transaction confirmed at block \(height)",
txid: txid
)
// Refresh transaction list
self.refreshTransactionList()
}
}
func handleReplacedTransaction(txid: String, conflicts: [String]) {
DispatchQueue.main.async {
self.updateTransactionStatus(txid: txid, status: .replaced)
if conflicts.isEmpty {
self.showNotification(
title: "Transaction Replaced",
body: "Transaction was replaced via RBF",
txid: txid
)
} else {
self.showNotification(
title: "Transaction Replaced",
body: "Transaction was replaced by \(conflicts.count) transaction(s)",
txid: txid
)
// Track replacement transactions
for conflictTxid in conflicts {
self.trackReplacementTransaction(originalTxid: txid, replacementTxid: conflictTxid)
}
}
}
}
func handleReorgedTransaction(txid: String) {
DispatchQueue.main.async {
self.updateTransactionStatus(txid: txid, status: .pending)
self.showNotification(
title: "Transaction Unconfirmed",
body: "Transaction became unconfirmed due to reorg",
txid: txid
)
}
}
func handleEvictedTransaction(txid: String) {
DispatchQueue.main.async {
self.updateTransactionStatus(txid: txid, status: .evicted)
self.showNotification(
title: "Transaction Evicted",
body: "Transaction was evicted from mempool",
txid: txid
)
// Optionally allow user to rebroadcast
self.promptRebroadcast(txid: txid)
}
}
// Helper functions to analyze transaction details
func isChannelFundingTransaction(details: TransactionDetails) -> Bool {
// Analyze inputs/outputs to detect channel funding
// This is a simplified example - implement based on your needs
return details.outputs.count == 2 && details.amountSats > 0
}
func isChannelClosureTransaction(details: TransactionDetails) -> Bool {
// Analyze inputs/outputs to detect channel closure
// This is a simplified example - implement based on your needs
return details.inputs.count > 0 && details.outputs.count >= 2
}class SyncProgressView: UIView {
@IBOutlet weak var progressBar: UIProgressView!
@IBOutlet weak var statusLabel: UILabel!
@IBOutlet weak var blockHeightLabel: UILabel!
func updateSyncProgress(type: SyncType, percent: UInt8) {
DispatchQueue.main.async {
self.progressBar.progress = Float(percent) / 100.0
switch type {
case .onchainWallet:
self.statusLabel.text = "Syncing wallet: \(percent)%"
case .lightningWallet:
self.statusLabel.text = "Syncing Lightning: \(percent)%"
case .feeRateCache:
self.statusLabel.text = "Updating fee rates: \(percent)%"
}
self.progressBar.isHidden = false
}
}
func handleSyncCompleted(type: SyncType, height: UInt32) {
DispatchQueue.main.async {
self.progressBar.isHidden = true
self.statusLabel.text = "Synced to block \(height)"
self.blockHeightLabel.text = "\(height)"
// Enable send/receive buttons
self.enableTransactionButtons()
}
}
}class BalanceViewController: UIViewController {
@IBOutlet weak var onchainBalanceLabel: UILabel!
@IBOutlet weak var lightningBalanceLabel: UILabel!
@IBOutlet weak var totalBalanceLabel: UILabel!
func updateBalances(onchain: UInt64, lightning: UInt64) {
DispatchQueue.main.async {
// Format with thousand separators
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.groupingSeparator = ","
let onchainFormatted = formatter.string(from: NSNumber(value: onchain)) ?? "0"
let lightningFormatted = formatter.string(from: NSNumber(value: lightning)) ?? "0"
let totalFormatted = formatter.string(from: NSNumber(value: onchain + lightning)) ?? "0"
// Animate the balance change
UIView.animate(withDuration: 0.3) {
self.onchainBalanceLabel.alpha = 0.5
self.lightningBalanceLabel.alpha = 0.5
} completion: { _ in
self.onchainBalanceLabel.text = "\(onchainFormatted) sats"
self.lightningBalanceLabel.text = "\(lightningFormatted) sats"
self.totalBalanceLabel.text = "\(totalFormatted) sats"
UIView.animate(withDuration: 0.3) {
self.onchainBalanceLabel.alpha = 1.0
self.lightningBalanceLabel.alpha = 1.0
}
}
// Flash green for increase, red for decrease
if onchain > self.previousOnchainBalance {
self.flashColor(self.onchainBalanceLabel, color: .systemGreen)
}
}
}
private func flashColor(_ label: UILabel, color: UIColor) {
let originalColor = label.textColor
label.textColor = color
UIView.animate(withDuration: 1.0) {
label.textColor = originalColor
}
}
}import org.lightningdevkit.ldknode.*
import kotlinx.coroutines.*
class WalletEventHandler(private val node: Node) {
private var eventJob: Job? = null
fun startEventHandling() {
eventJob = GlobalScope.launch {
while (isActive) {
processNextEvent()
delay(100) // Check every 100ms
}
}
}
fun stopEventHandling() {
eventJob?.cancel()
}
private fun processNextEvent() {
val event = node.nextEvent() ?: return
handleEvent(event)
node.eventHandled()
}
private fun handleEvent(event: Event) {
when (event) {
is Event.OnchainTransactionReceived -> {
handleIncomingTransaction(event.txid, event.details)
}
is Event.OnchainTransactionConfirmed -> {
handleConfirmedTransaction(event.txid, event.blockHeight, event.details)
}
is Event.OnchainTransactionReplaced -> {
handleReplacedTransaction(event.txid, event.conflicts)
}
is Event.OnchainTransactionReorged -> {
handleReorgedTransaction(event.txid)
}
is Event.OnchainTransactionEvicted -> {
handleEvictedTransaction(event.txid)
}
is Event.SyncProgress -> {
updateSyncProgress(
event.syncType,
event.progressPercent
)
}
is Event.SyncCompleted -> {
handleSyncCompleted(
event.syncType,
event.syncedBlockHeight
)
}
is Event.BalanceChanged -> {
updateBalances(
event.newSpendableOnchainBalanceSats,
event.newTotalLightningBalanceSats
)
}
else -> {
// Handle other existing events
}
}
}
}class TransactionNotificationManager(private val context: Context) {
fun handleIncomingTransaction(txid: String, details: TransactionDetails) {
GlobalScope.launch(Dispatchers.Main) {
if (details.amountSats > 0) {
// Incoming payment
showNotification(
title = "Payment Received!",
message = "Incoming payment of ${details.amountSats} sats (unconfirmed)",
txid = txid
)
// Update transaction list
addPendingTransaction(txid, details.amountSats)
// Play sound
playPaymentSound()
} else {
// Outgoing payment
updateTransactionStatus(txid, TransactionStatus.BROADCASTING)
}
// Analyze transaction to detect channel-related transactions
if (isChannelFundingTransaction(details)) {
Log.d("Wallet", "Channel funding transaction detected")
} else if (isChannelClosureTransaction(details)) {
Log.d("Wallet", "Channel closure transaction detected")
}
}
}
fun handleConfirmedTransaction(txid: String, height: UInt, details: TransactionDetails) {
GlobalScope.launch(Dispatchers.Main) {
// Update status
updateTransactionStatus(txid, TransactionStatus.CONFIRMED)
// Show notification
showNotification(
title = "Payment Confirmed!",
message = "Transaction confirmed at block $height",
txid = txid
)
// Vibrate
vibrate()
// Refresh transaction list
refreshTransactionList()
}
}
fun handleReplacedTransaction(txid: String, conflicts: List<String>) {
GlobalScope.launch(Dispatchers.Main) {
updateTransactionStatus(txid, TransactionStatus.REPLACED)
if (conflicts.isEmpty()) {
showNotification(
title = "Transaction Replaced",
message = "Transaction was replaced via RBF",
txid = txid
)
} else {
showNotification(
title = "Transaction Replaced",
message = "Transaction was replaced by ${conflicts.size} transaction(s)",
txid = txid
)
// Track replacement transactions
conflicts.forEach { conflictTxid ->
trackReplacementTransaction(originalTxid = txid, replacementTxid = conflictTxid)
}
}
}
}
fun handleReorgedTransaction(txid: String) {
GlobalScope.launch(Dispatchers.Main) {
updateTransactionStatus(txid, TransactionStatus.PENDING)
showNotification(
title = "Transaction Unconfirmed",
message = "Transaction became unconfirmed due to reorg",
txid = txid
)
}
}
fun handleEvictedTransaction(txid: String) {
GlobalScope.launch(Dispatchers.Main) {
updateTransactionStatus(txid, TransactionStatus.EVICTED)
showNotification(
title = "Transaction Evicted",
message = "Transaction was evicted from mempool",
txid = txid
)
// Optionally allow user to rebroadcast
promptRebroadcast(txid)
}
}
// Retrieve transaction details directly
fun getTransactionDetails(txid: String): TransactionDetails? {
return node.getTransactionDetails(txid = txid)
}
// Helper functions to analyze transaction details
private fun isChannelFundingTransaction(details: TransactionDetails): Boolean {
// Analyze inputs/outputs to detect channel funding
// This is a simplified example - implement based on your needs
return details.outputs.size == 2 && details.amountSats > 0
}
private fun isChannelClosureTransaction(details: TransactionDetails): Boolean {
// Analyze inputs/outputs to detect channel closure
// This is a simplified example - implement based on your needs
return details.inputs.isNotEmpty() && details.outputs.size >= 2
}
}
}
private fun showNotification(title: String, message: String, txid: String) {
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_bitcoin)
.setContentTitle(title)
.setContentText(message)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
.build()
notificationManager.notify(txid.hashCode(), notification)
}
}class SyncProgressFragment : Fragment() {
private lateinit var binding: FragmentSyncProgressBinding
fun updateSyncProgress(type: SyncType, percent: UByte) {
activity?.runOnUiThread {
binding.progressBar.progress = percent.toInt()
binding.progressText.text = "$percent%"
binding.statusText.text = when (type) {
SyncType.ONCHAIN_WALLET -> "Syncing wallet..."
SyncType.LIGHTNING_WALLET -> "Syncing Lightning..."
SyncType.FEE_RATE_CACHE -> "Updating fee rates..."
}
binding.progressContainer.visibility = View.VISIBLE
}
}
fun handleSyncCompleted(type: SyncType, height: UInt) {
activity?.runOnUiThread {
binding.progressContainer.visibility = View.GONE
binding.statusText.text = "Synced to block $height"
// Enable transaction buttons
binding.sendButton.isEnabled = true
binding.receiveButton.isEnabled = true
// Show success message
Snackbar.make(
binding.root,
"Sync completed!",
Snackbar.LENGTH_SHORT
).show()
}
}
}class BalanceViewModel : ViewModel() {
private val _onchainBalance = MutableLiveData<ULong>()
val onchainBalance: LiveData<ULong> = _onchainBalance
private val _lightningBalance = MutableLiveData<ULong>()
val lightningBalance: LiveData<ULong> = _lightningBalance
fun updateBalances(onchain: ULong, lightning: ULong) {
_onchainBalance.postValue(onchain)
_lightningBalance.postValue(lightning)
}
}
class BalanceFragment : Fragment() {
private lateinit var binding: FragmentBalanceBinding
private lateinit var viewModel: BalanceViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Observe balance changes
viewModel.onchainBalance.observe(viewLifecycleOwner) { balance ->
animateBalanceUpdate(binding.onchainBalanceText, balance)
}
viewModel.lightningBalance.observe(viewLifecycleOwner) { balance ->
animateBalanceUpdate(binding.lightningBalanceText, balance)
}
}
private fun animateBalanceUpdate(textView: TextView, newBalance: ULong) {
// Format with thousand separators
val formatted = NumberFormat.getNumberInstance(Locale.US)
.format(newBalance.toLong())
// Animate the change
ValueAnimator.ofFloat(0.5f, 1.0f).apply {
duration = 300
addUpdateListener { animation ->
textView.alpha = animation.animatedValue as Float
}
start()
}
textView.text = "$formatted sats"
// Flash color for visual feedback
val originalColor = textView.currentTextColor
textView.setTextColor(Color.GREEN)
textView.postDelayed({
textView.setTextColor(originalColor)
}, 500)
}
}class ReorgHandler(private val database: TransactionDatabase) {
fun handleUnconfirmedTransaction(txid: String) {
GlobalScope.launch {
// Mark transaction as unconfirmed in database
database.updateTransactionStatus(txid, TransactionStatus.UNCONFIRMED)
// Show warning to user
withContext(Dispatchers.Main) {
AlertDialog.Builder(context)
.setTitle("Transaction Unconfirmed")
.setMessage("Transaction $txid has become unconfirmed due to a blockchain reorganization. It may confirm again soon.")
.setPositiveButton("OK", null)
.show()
}
// Log the event
Log.w("Wallet", "Transaction $txid unconfirmed due to reorg")
}
}
}// DON'T DO THIS - Wastes battery and CPU
class OldWalletManager {
fun startPolling() {
timer.schedule(object : TimerTask() {
override fun run() {
// This runs even when nothing changed!
val balances = node.listBalances()
updateUI(balances)
// Sync every time
node.syncWallets()
}
}, 0, 30000) // Every 30 seconds
}
}// DO THIS - Only runs when something actually happens
class NewWalletManager {
fun startEventHandling() {
GlobalScope.launch {
while (isActive) {
// This blocks until an event arrives - zero CPU usage while waiting!
val event = node.waitNextEvent()
when (event) {
is Event.BalanceChanged -> updateUI(event)
is Event.OnchainTransactionReceived -> notify(event)
// ... handle other events
}
node.eventHandled()
}
}
}
}- No more timers - Events arrive when things actually happen
- No more manual sync calls - Background sync is automatic with events
- Instant updates - Users see changes immediately, not after next poll
- Better battery life - CPU sleeps between events
// ALWAYS call eventHandled() after processing
val event = node.nextEvent()
if (event != null) {
processEvent(event)
node.eventHandled() // Critical - marks event as processed
}// iOS - Use background queue
DispatchQueue.global(qos: .background).async {
while self.isRunning {
if let event = self.node.nextEvent() {
self.handleEvent(event)
self.node.eventHandled()
}
Thread.sleep(forTimeInterval: 0.1)
}
}// Android - Use coroutines
GlobalScope.launch(Dispatchers.IO) {
while (isActive) {
node.nextEvent()?.let { event ->
handleEvent(event)
node.eventHandled()
}
delay(100)
}
}// iOS
DispatchQueue.main.async {
self.balanceLabel.text = "\(balance) sats"
}// Android
runOnUiThread {
balanceTextView.text = "$balance sats"
}Even if you don't use them all, handle gracefully:
when (event) {
is Event.OnchainTransactionReceived -> handleTx(event)
is Event.BalanceChanged -> updateBalance(event)
// ... other events
else -> Log.d("Events", "Unhandled event: $event")
}Events may arrive while app is in background:
fun handleBalanceChanged(event: Event.BalanceChanged) {
// Save to persistent storage
preferences.edit()
.putLong("onchain_balance", event.newSpendableOnchainBalanceSats.toLong())
.putLong("lightning_balance", event.newTotalLightningBalanceSats.toLong())
.apply()
// Then update UI if visible
if (isResumed) {
updateBalanceUI()
}
}@Test
fun testEventReception() {
// Fund the wallet
val address = node.onchainPayment().newAddress()
sendTestCoins(address, 100000)
// Sync to detect transaction
node.syncWallets()
// Should receive events
var receivedEvent = false
for (i in 0..10) {
val event = node.nextEvent()
if (event is Event.OnchainTransactionReceived) {
receivedEvent = true
assertEquals(100000, event.amountSats)
node.eventHandled()
break
}
Thread.sleep(100)
}
assertTrue("Should receive transaction event", receivedEvent)
}@Test
fun testBalanceChangeEvent() {
val initialBalance = node.listBalances().totalOnchainBalanceSats
// Trigger a balance change
fundWallet(50000)
node.syncWallets()
// Check for balance change event
var balanceChanged = false
for (i in 0..10) {
val event = node.nextEvent()
if (event is Event.BalanceChanged) {
assertEquals(initialBalance, event.oldTotalOnchainBalanceSats)
assertEquals(initialBalance + 50000, event.newTotalOnchainBalanceSats)
balanceChanged = true
node.eventHandled()
break
}
Thread.sleep(100)
}
assertTrue("Should receive balance change event", balanceChanged)
}@Test
fun testSyncCompletedEvent() {
node.syncWallets()
var syncCompleted = false
for (i in 0..20) {
val event = node.nextEvent()
if (event is Event.SyncCompleted) {
assertEquals(SyncType.ONCHAIN_WALLET, event.syncType)
assertTrue(event.syncedBlockHeight > 0u)
syncCompleted = true
node.eventHandled()
break
}
Thread.sleep(100)
}
assertTrue("Should receive sync completed event", syncCompleted)
}func testReorgHandling() {
// This is conceptual - actual reorg testing requires regtest setup
// 1. Receive transaction
let txid = receiveTestTransaction()
// 2. Wait for confirmation event
waitForEvent { event in
if case .onchainTransactionConfirmed(let id, _, _, _, _) = event {
return id == txid
}
return false
}
// 3. Simulate reorg (in regtest)
// simulateReorg()
// 4. Should receive unconfirmed event
waitForEvent { event in
if case .onchainTransactionUnconfirmed(let id) = event {
XCTAssertEqual(id, txid)
return true
}
return false
}
}- Events use significantly less battery than polling
- Event checking (100ms interval) uses minimal CPU when no events
- Background sync runs automatically every ~30 seconds
- Events are queued internally until handled
- Always call
eventHandled()to free memory - Don't accumulate events without processing
- Background sync is automatic - no need for manual sync calls
- Events arrive even when app is backgrounded (if node keeps running)
- Sync frequency is optimized for battery/data usage
- Ensure node is started:
node.start() - Check background sync is enabled (default)
- Verify event loop is running
- Check logs for sync errors
- Always call
eventHandled()after processing - Don't process the same event multiple times
- Events are queued until marked handled
- Balance events only emit when balance actually changes
- Check both onchain and Lightning balances
- Ensure wallet sync completed first
- GitHub Issues: Report bugs at https://github.com/lightningdevkit/ldk-node
- API Documentation: Full API docs for each platform
- Example Apps: Check the examples/ directory for complete implementations
- Community: LDK Discord for questions and support
The new event system provides:
- ✅ Real-time notifications without polling
- ✅ Better battery life on mobile devices
- ✅ Instant UI updates for better UX
- ✅ Automatic background sync with progress tracking
- ✅ Reorg handling for blockchain reorganizations
- ✅ Type-safe events in both Swift and Kotlin
Start with the basic event loop, handle the events you need, and enjoy a more responsive, battery-efficient Lightning wallet!