From 0ec14f13d8800f2c72782ef8aeadf1931931d532 Mon Sep 17 00:00:00 2001 From: Philipp Walter Date: Wed, 29 Oct 2025 21:27:42 +0100 Subject: [PATCH] fix: persist channel locally in case remote backup fails --- .../reactnativeldk/classes/LdkPersister.kt | 50 +++++++++++++---- lib/ios/Classes/LdkPersist.swift | 56 ++++++++++++++----- lib/package.json | 2 +- 3 files changed, 83 insertions(+), 25 deletions(-) diff --git a/lib/android/src/main/java/com/reactnativeldk/classes/LdkPersister.kt b/lib/android/src/main/java/com/reactnativeldk/classes/LdkPersister.kt index 687febd9..713190c3 100644 --- a/lib/android/src/main/java/com/reactnativeldk/classes/LdkPersister.kt +++ b/lib/android/src/main/java/com/reactnativeldk/classes/LdkPersister.kt @@ -22,37 +22,65 @@ class LdkPersister { val file = File(LdkModule.channelStoragePath + "/" + channelId + ".bin") val isNew = !file.exists() + val serialized = data.write() + // If we're not remotely backing up, write locally and return if (BackupClient.skipRemoteBackup) { - file.writeBytes(data.write()) + file.writeBytes(serialized) if (isNew) { LdkEventEmitter.send(EventTypes.new_channel, body) } return ChannelMonitorUpdateStatus.LDKChannelMonitorUpdateStatus_Completed } - BackupClient.addToPersistQueue(BackupClient.Label.CHANNEL_MONITOR(channelId=channelId), data.write()) { error -> + // For new channels: write locally first to prevent loss if app is killed during backup + // Then try remote backup asynchronously + if (isNew) { + file.writeBytes(serialized) + + // Update chain monitor on main thread + LdkModule.reactContext?.runOnUiThread { + val res = LdkModule.chainMonitor?.channel_monitor_updated(channelFundingOutpoint, data._latest_update_id) + if (res == null || !res.is_ok) { + LdkEventEmitter.send(EventTypes.native_log, "Failed to update chain monitor with persisted channel (${channelId})") + } else { + LdkEventEmitter.send(EventTypes.native_log, "Persisted channel (${channelId}) to disk") + LdkEventEmitter.send(EventTypes.new_channel, body) + } + } + + // Kick off remote backup asynchronously (non-blocking) + BackupClient.addToPersistQueue(BackupClient.Label.CHANNEL_MONITOR(channelId=channelId), serialized) { error -> + if (error != null) { + LdkEventEmitter.send(EventTypes.native_log, "Warning. Remote backup failed for new channel (${channelId}), but channel was persisted locally. $error") + } + } + + return ChannelMonitorUpdateStatus.LDKChannelMonitorUpdateStatus_Completed + } + + // For updates: try remote backup first, then write locally on success (original behavior) + BackupClient.addToPersistQueue(BackupClient.Label.CHANNEL_MONITOR(channelId=channelId), serialized) { error -> if (error != null) { LdkEventEmitter.send(EventTypes.native_log, "Failed to persist channel (${channelId}) to remote backup: $error") return@addToPersistQueue } try { - file.writeBytes(data.write()) + file.writeBytes(serialized) } catch (e: Exception) { //If this fails we can't do much but LDK will retry on startup LdkEventEmitter.send(EventTypes.native_log, "Failed to locally persist channel (${channelId}) to disk") return@addToPersistQueue } - //Update chain monitor with successful persist - val res = LdkModule.chainMonitor?.channel_monitor_updated(channelFundingOutpoint, data._latest_update_id) - if (res == null || !res.is_ok) { - LdkEventEmitter.send(EventTypes.native_log, "Failed to update chain monitor with persisted channel (${channelId})") - } else { - LdkEventEmitter.send(EventTypes.native_log, "Persisted channel (${channelId}) to disk") - if (isNew) { - LdkEventEmitter.send(EventTypes.new_channel, body) + //Update chain monitor with successful persist on main thread + LdkModule.reactContext?.runOnUiThread { + val res = LdkModule.chainMonitor?.channel_monitor_updated(channelFundingOutpoint, data._latest_update_id) + if (res == null || !res.is_ok) { + LdkEventEmitter.send(EventTypes.native_log, "Failed to update chain monitor with persisted channel (${channelId})") + } else { + LdkEventEmitter.send(EventTypes.native_log, "Persisted channel (${channelId}) to disk") } } } diff --git a/lib/ios/Classes/LdkPersist.swift b/lib/ios/Classes/LdkPersist.swift index 40c0b542..18667e12 100644 --- a/lib/ios/Classes/LdkPersist.swift +++ b/lib/ios/Classes/LdkPersist.swift @@ -27,39 +27,69 @@ class LdkPersister: Persist { } let isNew = !FileManager().fileExists(atPath: channelStoragePath.path) + let serialized = Data(data.write()) // If we're not remotely backing up no need to update status later if BackupClient.skipRemoteBackup { - try Data(data.write()).write(to: channelStoragePath) + try serialized.write(to: channelStoragePath) if isNew { LdkEventEmitter.shared.send(withEvent: .new_channel, body: body) } return ChannelMonitorUpdateStatus.Completed } - - BackupClient.addToPersistQueue(.channelMonitor(id: channelIdHex), data.write()) { error in + + // For new channels: write locally first to prevent loss if app is killed during backup + // Then try remote backup asynchronously + if isNew { + try serialized.write(to: channelStoragePath) + + // Notify chain monitor on main thread + DispatchQueue.main.async { + let res = Ldk.chainMonitor?.channelMonitorUpdated( + fundingTxo: channelFundingOutpoint, + completedUpdateId: data.getLatestUpdateId() + ) + if let error = res?.getError() { + LdkEventEmitter.shared.send(withEvent: .native_log, body: "Error. Failed to update chain monitor for channel (\(channelIdHex)) Error \(error.getValueType()).") + } else { + LdkEventEmitter.shared.send(withEvent: .native_log, body: "Persisted channel \(channelIdHex). Update ID: \(data.getLatestUpdateId())") + LdkEventEmitter.shared.send(withEvent: .new_channel, body: body) + } + } + + // Kick off remote backup asynchronously (non-blocking) + BackupClient.addToPersistQueue(.channelMonitor(id: channelIdHex), [UInt8](serialized)) { error in + if let error { + LdkEventEmitter.shared.send(withEvent: .native_log, body: "Warning. Remote backup failed for new channel (\(channelIdHex)), but channel was persisted locally. \(error.localizedDescription)") + } + } + + return ChannelMonitorUpdateStatus.Completed + } + + // For updates: try remote backup first, then write locally on success (original behavior) + BackupClient.addToPersistQueue(.channelMonitor(id: channelIdHex), [UInt8](serialized)) { error in if let error { LdkEventEmitter.shared.send(withEvent: .native_log, body: "Error. Failed persist channel on remote server (\(channelIdHex)). \(error.localizedDescription)") return } - // Callback for when the persist queue queue entry is processed + // Callback for when the persist queue entry is processed do { - try Data(data.write()).write(to: channelStoragePath) + try serialized.write(to: channelStoragePath) } catch { // If this fails we can't do much but LDK will retry on startup LdkEventEmitter.shared.send(withEvent: .native_log, body: "Error. Failed to locally persist channel (\(channelIdHex)). \(error.localizedDescription)") return } - // Update chainmonitor with successful persist - let res = Ldk.chainMonitor?.channelMonitorUpdated(fundingTxo: channelFundingOutpoint, completedUpdateId: data.getLatestUpdateId()) - if let error = res?.getError() { - LdkEventEmitter.shared.send(withEvent: .native_log, body: "Error. Failed to update chain monitor for channel (\(channelIdHex)) Error \(error.getValueType()).") - } else { - LdkEventEmitter.shared.send(withEvent: .native_log, body: "Persisted channel \(channelIdHex). Update ID: \(data.getLatestUpdateId())") - if isNew { - LdkEventEmitter.shared.send(withEvent: .new_channel, body: body) + // Update chainmonitor with successful persist on main thread + DispatchQueue.main.async { + let res = Ldk.chainMonitor?.channelMonitorUpdated(fundingTxo: channelFundingOutpoint, completedUpdateId: data.getLatestUpdateId()) + if let error = res?.getError() { + LdkEventEmitter.shared.send(withEvent: .native_log, body: "Error. Failed to update chain monitor for channel (\(channelIdHex)) Error \(error.getValueType()).") + } else { + LdkEventEmitter.shared.send(withEvent: .native_log, body: "Persisted channel \(channelIdHex). Update ID: \(data.getLatestUpdateId())") } } } diff --git a/lib/package.json b/lib/package.json index fed8f707..8bf4b949 100644 --- a/lib/package.json +++ b/lib/package.json @@ -1,7 +1,7 @@ { "name": "@synonymdev/react-native-ldk", "title": "React Native LDK", - "version": "0.0.160", + "version": "0.0.161", "description": "React Native wrapper for LDK", "main": "./dist/index.js", "types": "./dist/index.d.ts",