From be4a848f2e53d620c3a1fe1b586c704362c5127a Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 11 Jun 2024 15:36:56 +0200 Subject: [PATCH] feat: restart debounce, dropped peer connection handler --- example/ios/Podfile.lock | 4 +- .../main/java/com/reactnativeldk/LdkModule.kt | 2 +- lib/ios/Ldk.swift | 145 +++++++++++++++++- 3 files changed, 140 insertions(+), 11 deletions(-) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index b0216d7d..d52f6ad9 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -316,7 +316,7 @@ PODS: - React-jsinspector (0.72.4) - React-logger (0.72.4): - glog - - react-native-ldk (0.0.141): + - react-native-ldk (0.0.143): - React - react-native-randombytes (3.6.1): - React-Core @@ -621,7 +621,7 @@ SPEC CHECKSUMS: React-jsiexecutor: c7f826e40fa9cab5d37cab6130b1af237332b594 React-jsinspector: aaed4cf551c4a1c98092436518c2d267b13a673f React-logger: da1ebe05ae06eb6db4b162202faeafac4b435e77 - react-native-ldk: 58b28973bedc64333c350b9074554f814ba3daec + react-native-ldk: 12d78fe1141ad4343a2842340f7ebf8539dcc3b0 react-native-randombytes: 421f1c7d48c0af8dbcd471b0324393ebf8fe7846 react-native-tcp-socket: c1b7297619616b4c9caae6889bcb0aba78086989 React-NativeModulesApple: edb5ace14f73f4969df6e7b1f3e41bef0012740f diff --git a/lib/android/src/main/java/com/reactnativeldk/LdkModule.kt b/lib/android/src/main/java/com/reactnativeldk/LdkModule.kt index 6531aa47..696859e9 100644 --- a/lib/android/src/main/java/com/reactnativeldk/LdkModule.kt +++ b/lib/android/src/main/java/com/reactnativeldk/LdkModule.kt @@ -641,7 +641,7 @@ class LdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMod peerHandler!!.connect(pubKey.hexa(), InetSocketAddress(address, port.toInt()), 3000) LdkEventEmitter.send(EventTypes.native_log, "Connection to peer $pubKey re-established by handleDroppedPeers().") } catch (e: Exception) { - LdkEventEmitter.send(EventTypes.native_log, "Error connecting peer $pubKey. Error: $e") + LdkEventEmitter.send(EventTypes.native_log, "Error connecting peer from handleDroppedPeers() $pubKey. Error: $e") } finally { currentlyConnectingPeers.remove(pubKey) } diff --git a/lib/ios/Ldk.swift b/lib/ios/Ldk.swift index 0708ca6a..1579c5fa 100644 --- a/lib/ios/Ldk.swift +++ b/lib/ios/Ldk.swift @@ -86,6 +86,9 @@ enum LdkCallbackResponses: String { case config_init_success = "config_init_success" case network_graph_init_success = "network_graph_init_success" case add_peer_success = "add_peer_success" + case add_peer_skipped = "add_peer_skipped" + case peer_already_connected = "peer_already_connected" + case peer_currently_connecting = "peer_currently_connecting" case chain_sync_success = "chain_sync_success" case invoice_payment_success = "invoice_payment_success" case tx_set_confirmed = "tx_set_confirmed" @@ -143,6 +146,12 @@ class Ldk: NSObject { var currentBlockchainTipHash: NSString? var currentBlockchainHeight: NSInteger? + //Peer connection checks + var backgroundedAt: Date? = nil + var addedPeers: [(String, Int, String)] = [] + var currentlyConnectingPeers: [String] = [] + var droppedPeerTimer: Timer? = nil + //Static to be accessed from other classes static var accountStoragePath: URL? static var channelStoragePath: URL? @@ -495,25 +504,45 @@ class Ldk: NSObject { currentBlockchainTipHash = blockHash currentBlockchainHeight = blockHeight addForegroundObserver() - + startDroppedPeerTimer() + return handleResolve(resolve, .channel_manager_init_success) } func addForegroundObserver() { removeForegroundObserver() + backgroundedAt = nil NotificationCenter.default.addObserver(self, selector: #selector(restartOnForeground), name: UIApplication.didBecomeActiveNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(onBackground), name: UIApplication.willResignActiveNotification, object: nil) } func removeForegroundObserver() { NotificationCenter.default.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil) + NotificationCenter.default.removeObserver(self, name: UIApplication.willResignActiveNotification, object: nil) } /// Used by event listener so responses are not handled @objc func restartOnForeground() { + let secondsSinceBackgrounded = Date().timeIntervalSince(backgroundedAt ?? .distantPast) + guard secondsSinceBackgrounded > 5 else { + LdkEventEmitter.shared.send(withEvent: .native_log, body: "Skipping restart. App was only backgrounded \(Int(secondsSinceBackgrounded))s ago") + return + } + + LdkEventEmitter.shared.send(withEvent: .native_log, body: "Restarting LDK on move to foreground. App was backgrounded \(Int(secondsSinceBackgrounded))s ago") + + backgroundedAt = nil restart { res in } reject: { code, message, error in } } + @objc + func onBackground() { + LdkEventEmitter.shared.send(withEvent: .native_log, body: "App moved to background") + + backgroundedAt = Date() + } + /// Restarts channel manager constructor to get a new TCP peer handler @objc func restart(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { @@ -559,6 +588,7 @@ class Ldk: NSObject { return handleResolve(resolve, .ldk_stop) } + stopStartDroppedPeerTimer() removeForegroundObserver() //LDK was intentionally stopped and we shouldn't attempt a restart cm.interrupt() channelManagerConstructor = nil @@ -571,6 +601,7 @@ class Ldk: NSObject { peerHandler = nil ldkNetwork = nil ldkCurrency = nil + backgroundedAt = nil return handleResolve(resolve, .ldk_stop) } @@ -617,16 +648,114 @@ class Ldk: NSObject { return handleResolve(resolve, .chain_sync_success) } + func startDroppedPeerTimer() { + guard droppedPeerTimer == nil else { + return + } + + DispatchQueue.main.async { [weak self] in + guard let self else { return } + + LdkEventEmitter.shared.send(withEvent: .native_log, body: "Starting timer to check for dropped peers") + + droppedPeerTimer = Timer.scheduledTimer( + timeInterval: 5.0, + target: self, + selector: #selector(handleDroppedPeers), + userInfo: nil, + repeats: true + ) + } + } + + func stopStartDroppedPeerTimer() { + droppedPeerTimer?.invalidate() + droppedPeerTimer = nil + } + + @objc func handleDroppedPeers() { + guard backgroundedAt == nil else { + LdkEventEmitter.shared.send(withEvent: .native_log, body: "App was backgrounded, skipping handleDroppedPeers()") + return + } + + guard channelManagerConstructor != nil else { + LdkEventEmitter.shared.send(withEvent: .native_log, body: "channelManagerConstructor not intialized, skipping handleDroppedPeers()") + return + } + + guard let peerHandler else { + LdkEventEmitter.shared.send(withEvent: .native_log, body: "peerHandler not intialized, skipping handleDroppedPeers()") + return + } + + guard let peerManager else { + LdkEventEmitter.shared.send(withEvent: .native_log, body: "peerManager not intialized, skipping handleDroppedPeers()") + return + } + + LdkEventEmitter.shared.send(withEvent: .native_log, body: "Checking for dropped peers") + + let currentList = peerManager.getPeerNodeIds().map { Data($0.0).hexEncodedString() } + + addedPeers.forEach { (address, port, pubKey) in + guard !currentList.contains(pubKey) else { + return + } + + currentlyConnectingPeers.append(String(pubKey)) + let res = peerHandler.connect(address: String(address), port: UInt16(port), theirNodeId: String(pubKey).hexaBytes) + currentlyConnectingPeers.removeAll { $0 == String(pubKey) } + + if res { + LdkEventEmitter.shared.send(withEvent: .native_log, body: "Connection to peer \(pubKey) re-established by handleDroppedPeers().") + } else { + LdkEventEmitter.shared.send(withEvent: .native_log, body: "Error connecting peer \(pubKey) from handleDroppedPeers().") + } + } + } + @objc func addPeer(_ address: NSString, port: NSInteger, pubKey: NSString, timeout: NSInteger, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { //timeout param not used. Only for android. - //Sync ChannelMonitors and ChannelManager to chain tip + guard backgroundedAt == nil else { + LdkEventEmitter.shared.send(withEvent: .native_log, body: "App was backgrounded, skipping addPeer()") + return handleResolve(resolve, .add_peer_skipped) + } + guard let peerHandler = peerHandler else { return handleReject(reject, .init_peer_handler) } + guard let peerManager = peerManager else { + return handleReject(reject, .init_peer_manager) + } + + //If peer is already connected don't add again + let currentList = peerManager.getPeerNodeIds().map { Data($0.0).hexEncodedString() } + guard !currentList.contains(String(pubKey)) else { + LdkEventEmitter.shared.send(withEvent: .native_log, body: "Skipping new peer connection, already connected to \(pubKey)") + return handleResolve(resolve, .peer_already_connected) + } + + guard !currentlyConnectingPeers.contains(String(pubKey)) else { + LdkEventEmitter.shared.send(withEvent: .native_log, body: "Skipping additional peer connection, already busy connecting to \(pubKey)") + return handleResolve(resolve, .peer_currently_connecting) + } + + //Add to retry list if peers are dropped + + currentlyConnectingPeers.append(String(pubKey)) let res = peerHandler.connect(address: String(address), port: UInt16(port), theirNodeId: String(pubKey).hexaBytes) + currentlyConnectingPeers.removeAll { $0 == String(pubKey) } + + if !addedPeers.contains(where: { (_, _, pk) in + pk == String(pubKey) + }) { + addedPeers.append((String(address), Int(port), String(pubKey))) + } + if !res { return handleReject(reject, .add_peer_fail) } @@ -846,7 +975,7 @@ class Ldk: NSObject { guard !(amountSats > 0 && !isZeroValueInvoice) else { return handleReject(reject, .invoice_payment_fail_must_not_specify_amount) } - + let paymentId = invoice.paymentHash()! let (paymentHash, recipientOnion, routeParameters) = isZeroValueInvoice ? Bindings.paymentParametersFromZeroAmountInvoice(invoice: invoice, amountMsat: UInt64(amountSats*1000)).getValue()! : Bindings.paymentParametersFromInvoice(invoice: invoice).getValue()! @@ -1038,7 +1167,7 @@ class Ldk: NSObject { } let excludeChannelIds = ignoreOpenChannels ? channelManager.listChannels().map { Data($0.getChannelId() ?? []).hexEncodedString() }.filter { $0 != "" } : [] - + let channelFiles = try! FileManager.default.contentsOfDirectory(at: channelStoragePath, includingPropertiesForKeys: nil) var result: [[String: Any?]] = [] @@ -1261,9 +1390,9 @@ class Ldk: NSObject { } let openChannelIds = channelManager.listChannels().map { Data($0.getChannelId() ?? []).hexEncodedString() }.filter { $0 != "" } - + let channelFiles = try! FileManager.default.contentsOfDirectory(at: channelStoragePath, includingPropertiesForKeys: nil) - + var txs: [String] = [] for channelFile in channelFiles { @@ -1296,7 +1425,7 @@ class Ldk: NSObject { LdkEventEmitter.shared.send(withEvent: .native_log, body: "No spendable outputs found in \(channelId)") continue } - + let res = keysManager.spendSpendableOutputs( descriptors: descriptors, outputs: [], @@ -1309,7 +1438,7 @@ class Ldk: NSObject { LdkEventEmitter.shared.send(withEvent: .native_log, body: "Failed to spend output from closed channel: \(channelId).") continue } - + txs.append(Data(res.getValue()!).hexEncodedString()) }