From eb3af19b8efcdb93cc62a6d8b3867c990015f4f0 Mon Sep 17 00:00:00 2001 From: Jurvis Tan Date: Thu, 15 Dec 2022 22:39:16 -0800 Subject: [PATCH 1/9] Show menu when selecting peer to choose appropriate action --- Shared/Peers List/PeersListView.swift | 18 +++++++-- Shared/Peers List/PeersListViewModel.swift | 46 ++++++++++++++++++++++ 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/Shared/Peers List/PeersListView.swift b/Shared/Peers List/PeersListView.swift index ac1cffc..528e139 100644 --- a/Shared/Peers List/PeersListView.swift +++ b/Shared/Peers List/PeersListView.swift @@ -46,6 +46,20 @@ struct PeersListView: View { viewModel.addPeerView() } } + .confirmationDialog("Action", isPresented: viewModel.showConfirmationDialog) { + Button("Connect Peer") { + Task { + await viewModel.connectFocusedPeer() + viewModel.dismissFocusedPeer() + } + } + Button("Open Channel") { + Task { + await viewModel.openChannelWithFocusedPeer() + } + } + Button("Cancel", role: .cancel) { viewModel.dismissFocusedPeer() } + } } } @@ -54,9 +68,7 @@ struct PeersListView: View { let isPeerActive = viewModel.isNodeActive(nodeId: peer.peerPubKey) PeerCell(viewModel: PeerCellViewModel(name: peer.name), connectionStatus: isPeerActive ? .connected : .unconnected) .onTapGesture { - Task { - await self.viewModel.connectPeer(peer) - } + self.viewModel.focusPeer(peer: peer) } } diff --git a/Shared/Peers List/PeersListViewModel.swift b/Shared/Peers List/PeersListViewModel.swift index 4b27751..deb801c 100644 --- a/Shared/Peers List/PeersListViewModel.swift +++ b/Shared/Peers List/PeersListViewModel.swift @@ -13,6 +13,22 @@ class PeersListViewModel: ObservableObject { @Published var sheetToShow: Sheet? @Published var peersToShow: [Peer] = [] @Published var activePeerNodeIds: [String] = [] + @Published private var focusedPeer: Peer? + + var showConfirmationDialog: Binding { + Binding { + if let _ = self.focusedPeer { + return true + } else { + return false + } + + } set: { shouldShowConfirmationDialog in + if !shouldShowConfirmationDialog { + self.focusedPeer = nil + } + } + } private var cancellables = Set() @@ -42,6 +58,26 @@ class PeersListViewModel: ObservableObject { } } + func focusPeer(peer: Peer) { + focusedPeer = peer + } + + func dismissFocusedPeer() { + focusedPeer = nil + } + + func connectFocusedPeer() async { + guard let peer = focusedPeer else { + return + } + + do { + try await LightningNodeService.shared.connectPeer(peer) + } catch { + print("Error connecting to peer") + } + } + func deletePeer(at offsets: IndexSet) { let oldPeerCount = peersToShow.count @@ -51,6 +87,16 @@ class PeersListViewModel: ObservableObject { saveCurrentListofPeersToDisk() } } + + func openChannelWithFocusedPeer() async throws { + guard let focusedPeer = focusedPeer else { return } + do { + let channelOpenInfo = try await LightningNodeService.shared.requestChannelOpen(focusedPeer.peerPubKey, channelValue: 1_300_000, reserveAmount: 1000) + let fundingScriptPubKey + } catch { + + } + } } // MARK: Helper Methods From f5234cd627cd6cf4db22f1d7b19f944c36ed37d8 Mon Sep 17 00:00:00 2001 From: Jurvis Tan Date: Sat, 24 Dec 2022 21:00:16 -0800 Subject: [PATCH 2/9] Add decodeScript to RpcChainManager protocol --- .../BitcoinCoreChainManager.swift | 26 +++++++++---------- .../Protocols/RpcChainManager.swift | 7 +++++ 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/Lightning/Sources/Lightning/Chain Sync/Bitcoin Core/BitcoinCoreChainManager.swift b/Lightning/Sources/Lightning/Chain Sync/Bitcoin Core/BitcoinCoreChainManager.swift index 1ac0100..e42de9a 100644 --- a/Lightning/Sources/Lightning/Chain Sync/Bitcoin Core/BitcoinCoreChainManager.swift +++ b/Lightning/Sources/Lightning/Chain Sync/Bitcoin Core/BitcoinCoreChainManager.swift @@ -226,6 +226,19 @@ extension BitcoinCoreChainManager: RpcChainManager { let result = response["result"] as! String return result } + + /** + Decode an arbitary script. Can be an output script, a redeem script, or anything else + - Parameter script: byte array serialization of script + - Returns: Object with various possible interpretations of the script + - Throws: + */ + public func decodeScript(script: [UInt8]) async throws -> [String: Any] { + let scriptHex = bytesToHexString(bytes: script) + let response = try await self.callRpcMethod(method: "decodescript", params: [scriptHex]) + let result = response["result"] as! [String: Any] + return result + } } // MARK: RPC Calls @@ -287,19 +300,6 @@ extension BitcoinCoreChainManager { return transaction } - /** - Decode an arbitary script. Can be an output script, a redeem script, or anything else - - Parameter script: byte array serialization of script - - Returns: Object with various possible interpretations of the script - - Throws: - */ - public func decodeScript(script: [UInt8]) async throws -> [String: Any] { - let scriptHex = bytesToHexString(bytes: script) - let response = try await self.callRpcMethod(method: "decodescript", params: [scriptHex]) - let result = response["result"] as! [String: Any] - return result - } - /** Mine regtest blocks - Parameters: diff --git a/Lightning/Sources/Lightning/Chain Sync/Protocols/RpcChainManager.swift b/Lightning/Sources/Lightning/Chain Sync/Protocols/RpcChainManager.swift index ad1494e..1d7d57d 100644 --- a/Lightning/Sources/Lightning/Chain Sync/Protocols/RpcChainManager.swift +++ b/Lightning/Sources/Lightning/Chain Sync/Protocols/RpcChainManager.swift @@ -17,4 +17,11 @@ protocol RpcChainManager { func isMonitoring() async -> Bool func getTransaction(with hash: String) async throws -> [UInt8] + /** + Decode an arbitary script. Can be an output script, a redeem script, or anything else + - Parameter script: byte array serialization of script + - Returns: Object with various possible interpretations of the script + - Throws: + */ + func decodeScript(script: [UInt8]) async throws -> [String: Any] } From 7275d6d1ff632880b2d457c84f107e27b0efdb3b Mon Sep 17 00:00:00 2001 From: Jurvis Tan Date: Sat, 24 Dec 2022 21:01:17 -0800 Subject: [PATCH 3/9] Add node API for requesting channel open --- Shared/LightningNodeService.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Shared/LightningNodeService.swift b/Shared/LightningNodeService.swift index a2cc53c..c29b028 100644 --- a/Shared/LightningNodeService.swift +++ b/Shared/LightningNodeService.swift @@ -56,6 +56,20 @@ class LightningNodeService { func connectPeer(_ peer: Peer) async throws { try await instance.connectPeer(pubKey: peer.peerPubKey, hostname: peer.connectionInformation.hostname, port: peer.connectionInformation.port) } + + func requestChannelOpen(_ pubKeyHex: String, channelValue: UInt64, reserveAmount: UInt64) async throws -> Lightning.Node.ChannelOpenInfo { + do { + let channelOpenInfo = try await instance.requestChannelOpen( + pubKeyHex, + channelValue: channelValue, + reserveAmount: reserveAmount + ) + + return channelOpenInfo + } catch { + throw ServiceError.cannotOpenChannel + } + } } // MARK: Helpers From 66b1e6f03615f180ec6173440ef6edb46ea37ff0 Mon Sep 17 00:00:00 2001 From: Jurvis Tan Date: Sat, 24 Dec 2022 21:01:51 -0800 Subject: [PATCH 4/9] Rework Peer-administration screens --- Shared/Model/Peer.swift | 21 ++++++--- Shared/Peer View/PeerView.swift | 50 ++++++++++++++++++++++ Shared/Peer View/PeerViewModel.swift | 39 +++++++++++++++++ Shared/Peers List/PeersListView.swift | 39 +++++++++-------- Shared/Peers List/PeersListViewModel.swift | 21 +++++---- Surge.xcodeproj/project.pbxproj | 20 +++++++++ 6 files changed, 156 insertions(+), 34 deletions(-) create mode 100644 Shared/Peer View/PeerView.swift create mode 100644 Shared/Peer View/PeerViewModel.swift diff --git a/Shared/Model/Peer.swift b/Shared/Model/Peer.swift index 31fbcc5..5852645 100644 --- a/Shared/Model/Peer.swift +++ b/Shared/Model/Peer.swift @@ -7,7 +7,7 @@ import Foundation -struct Peer: Identifiable, Codable, Equatable { +struct Peer: Codable, Equatable { let id: UUID let peerPubKey: String let name: String @@ -19,10 +19,6 @@ struct Peer: Identifiable, Codable, Equatable { self.name = name self.connectionInformation = connectionInformation } - - static func == (lhs: Peer, rhs: Peer) -> Bool { - return lhs.id == rhs.id - } } // MARK: Helper Models @@ -32,3 +28,18 @@ extension Peer { let port: UInt16 } } + +extension Peer: Identifiable, Hashable { + var identifier: String { + return peerPubKey + } + + public func hash(into hasher: inout Hasher) { + return hasher.combine(identifier) + } + + public static func == (lhs: Peer, rhs: Peer) -> Bool { + return lhs.peerPubKey == rhs.peerPubKey + } +} + diff --git a/Shared/Peer View/PeerView.swift b/Shared/Peer View/PeerView.swift new file mode 100644 index 0000000..a367409 --- /dev/null +++ b/Shared/Peer View/PeerView.swift @@ -0,0 +1,50 @@ +// +// PeerView.swift +// Surge (iOS) +// +// Created by Jurvis on 12/17/22. +// + +import SwiftUI + +struct PeerView: View { + @StateObject var viewModel: PeerViewModel + + var body: some View { + NavigationView { + List { + Section(header: Text("Action")) { + if viewModel.isPeerConnected { + Text("Peer Connected!") + .foregroundColor(.green) + } else { + Button("Connect Peer") { + Task { + await viewModel.connectPeer() + } + } + } + + Button("Request Channel Open") { + print("") + } + } + Section(header: Text("Pending Funding Scripts")) { + Text("abcdef") + } + Section(header: Text("Active Channels")) { + Text("asbdvasd") + } + } + .navigationTitle(viewModel.peer.name) + .listStyle(.grouped) + } + + } +} + +struct PeerView_Previews: PreviewProvider { + static var previews: some View { + PeerView(viewModel: PeerViewModel(peer: Peer(peerPubKey: "abc", name: "Alice", connectionInformation: .init(hostname: "abc.com", port: 245)))) + } +} diff --git a/Shared/Peer View/PeerViewModel.swift b/Shared/Peer View/PeerViewModel.swift new file mode 100644 index 0000000..09bb98f --- /dev/null +++ b/Shared/Peer View/PeerViewModel.swift @@ -0,0 +1,39 @@ +// +// PeerViewModel.swift +// Surge (iOS) +// +// Created by Jurvis on 12/24/22. +// + +import Foundation +import Combine + +class PeerViewModel: ObservableObject { + @Published var peer: Peer + @Published var activePeerNodeIds: [String] = [] + + var isPeerConnected: Bool { + return activePeerNodeIds.contains(peer.peerPubKey) + } + + private var cancellables = Set() + + init(peer: Peer) { + self.peer = peer + setup() + } + + func connectPeer() async { + do { + try await LightningNodeService.shared.connectPeer(peer) + } catch { + print("Error connecting to peer") + } + } + + private func setup() { + LightningNodeService.shared.activePeersPublisher + .assign(to: \.activePeerNodeIds, on: self) + .store(in: &cancellables) + } +} diff --git a/Shared/Peers List/PeersListView.swift b/Shared/Peers List/PeersListView.swift index 528e139..73844aa 100644 --- a/Shared/Peers List/PeersListView.swift +++ b/Shared/Peers List/PeersListView.swift @@ -44,32 +44,35 @@ struct PeersListView: View { switch sheet { case .addPeer: viewModel.addPeerView() + case .showPeer(let peer): + viewModel.showPeerView(peer: peer) } } - .confirmationDialog("Action", isPresented: viewModel.showConfirmationDialog) { - Button("Connect Peer") { - Task { - await viewModel.connectFocusedPeer() - viewModel.dismissFocusedPeer() - } - } - Button("Open Channel") { - Task { - await viewModel.openChannelWithFocusedPeer() - } - } - Button("Cancel", role: .cancel) { viewModel.dismissFocusedPeer() } - } +// .confirmationDialog("Action", isPresented: viewModel.showConfirmationDialog) { +// Button("Connect Peer") { +// Task { +// await viewModel.connectFocusedPeer() +// viewModel.dismissFocusedPeer() +// } +// } +// Button("Open Channel") { +// Task { +// try await viewModel.openChannelWithFocusedPeer() +// } +// } +// Button("Cancel", role: .cancel) { viewModel.dismissFocusedPeer() } +// } } } @ViewBuilder func peerCell(peer: Peer) -> some View { let isPeerActive = viewModel.isNodeActive(nodeId: peer.peerPubKey) - PeerCell(viewModel: PeerCellViewModel(name: peer.name), connectionStatus: isPeerActive ? .connected : .unconnected) - .onTapGesture { - self.viewModel.focusPeer(peer: peer) - } + Button { + self.viewModel.showPeerScreen(peer: peer) + } label: { + PeerCell(viewModel: PeerCellViewModel(name: peer.name), connectionStatus: isPeerActive ? .connected : .unconnected) + } } @ViewBuilder diff --git a/Shared/Peers List/PeersListViewModel.swift b/Shared/Peers List/PeersListViewModel.swift index deb801c..a5bda2f 100644 --- a/Shared/Peers List/PeersListViewModel.swift +++ b/Shared/Peers List/PeersListViewModel.swift @@ -36,9 +36,10 @@ class PeersListViewModel: ObservableObject { peersToShow.count == 0 } - enum Sheet: Identifiable { + enum Sheet: Hashable, Identifiable { var id: Self { self } case addPeer + case showPeer(Peer) } internal init(peers: [Peer] = []) { @@ -87,16 +88,6 @@ class PeersListViewModel: ObservableObject { saveCurrentListofPeersToDisk() } } - - func openChannelWithFocusedPeer() async throws { - guard let focusedPeer = focusedPeer else { return } - do { - let channelOpenInfo = try await LightningNodeService.shared.requestChannelOpen(focusedPeer.peerPubKey, channelValue: 1_300_000, reserveAmount: 1000) - let fundingScriptPubKey - } catch { - - } - } } // MARK: Helper Methods @@ -149,4 +140,12 @@ extension PeersListViewModel { return AddPeerView(viewModel: addPeerViewModel) } + + func showPeerScreen(peer: Peer) { + self.sheetToShow = .showPeer(peer) + } + + func showPeerView(peer: Peer) -> some View { + return PeerView(viewModel: PeerViewModel(peer: peer)) + } } diff --git a/Surge.xcodeproj/project.pbxproj b/Surge.xcodeproj/project.pbxproj index 43a5673..847bec8 100644 --- a/Surge.xcodeproj/project.pbxproj +++ b/Surge.xcodeproj/project.pbxproj @@ -8,6 +8,10 @@ /* Begin PBXBuildFile section */ 755617CE28C5C6EF00831531 /* Lightning in Frameworks */ = {isa = PBXBuildFile; productRef = 755617CD28C5C6EF00831531 /* Lightning */; }; + 7562ED60294E995D0055FB06 /* PeerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7562ED5F294E995D0055FB06 /* PeerView.swift */; }; + 7562ED622957B6D10055FB06 /* PeerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7562ED612957B6D10055FB06 /* PeerViewModel.swift */; }; + 7562ED632957B6D70055FB06 /* PeerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7562ED612957B6D10055FB06 /* PeerViewModel.swift */; }; + 7562ED642957B6D70055FB06 /* PeerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7562ED5F294E995D0055FB06 /* PeerView.swift */; }; 7571CBE228C5BEAB00529718 /* Tests_iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7571CBE128C5BEAB00529718 /* Tests_iOS.swift */; }; 7571CBE428C5BEAB00529718 /* Tests_iOSLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7571CBE328C5BEAB00529718 /* Tests_iOSLaunchTests.swift */; }; 7571CBEE28C5BEAB00529718 /* Tests_macOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7571CBED28C5BEAB00529718 /* Tests_macOS.swift */; }; @@ -65,6 +69,8 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 7562ED5F294E995D0055FB06 /* PeerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerView.swift; sourceTree = ""; }; + 7562ED612957B6D10055FB06 /* PeerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerViewModel.swift; sourceTree = ""; }; 7571CBC928C5BEAA00529718 /* SurgeApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurgeApp.swift; sourceTree = ""; }; 7571CBCA28C5BEAA00529718 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; 7571CBCB28C5BEAB00529718 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -129,6 +135,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 7562ED5E294E99450055FB06 /* Peer View */ = { + isa = PBXGroup; + children = ( + 7562ED5F294E995D0055FB06 /* PeerView.swift */, + 7562ED612957B6D10055FB06 /* PeerViewModel.swift */, + ); + path = "Peer View"; + sourceTree = ""; + }; 7571CBC328C5BEAA00529718 = { isa = PBXGroup; children = ( @@ -149,6 +164,7 @@ 75D85C1828CE927600461E80 /* Model */, 75D85C1728CE927100461E80 /* Stores */, 75D85C0728CE80A700461E80 /* Peers List */, + 7562ED5E294E99450055FB06 /* Peer View */, 7571CBC928C5BEAA00529718 /* SurgeApp.swift */, 7571CBCA28C5BEAA00529718 /* HomeView.swift */, 7571CBCB28C5BEAB00529718 /* Assets.xcassets */, @@ -416,9 +432,11 @@ 75AA489A28C683EE0032A3DC /* LightningNodeService.swift in Sources */, 75D85C1228CE8B5300461E80 /* AddPeerView.swift in Sources */, 75D85C0F28CE811C00461E80 /* PeerCell.swift in Sources */, + 7562ED622957B6D10055FB06 /* PeerViewModel.swift in Sources */, 75D85C0928CE80BA00461E80 /* PeersListViewModel.swift in Sources */, 75D85C1528CE8F5700461E80 /* AddPeerViewModel.swift in Sources */, 75F3FBC928CD978E00FB6D61 /* HomeViewModel.swift in Sources */, + 7562ED60294E995D0055FB06 /* PeerView.swift in Sources */, 75D85C2028CE974900461E80 /* PeerConnectionStatus.swift in Sources */, 75D85C1D28CE92C200461E80 /* PeerStore.swift in Sources */, 75D85C1A28CE927F00461E80 /* Peer.swift in Sources */, @@ -431,10 +449,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 7562ED642957B6D70055FB06 /* PeerView.swift in Sources */, 75F3FBC728CD95A000FB6D61 /* TransactionListView.swift in Sources */, 7571CBF428C5BEAB00529718 /* HomeView.swift in Sources */, 75D85C0D28CE811200461E80 /* PeerCellViewModel.swift in Sources */, 75AA489B28C683EE0032A3DC /* LightningNodeService.swift in Sources */, + 7562ED632957B6D70055FB06 /* PeerViewModel.swift in Sources */, 75D85C1328CE8B5300461E80 /* AddPeerView.swift in Sources */, 75D85C1028CE811C00461E80 /* PeerCell.swift in Sources */, 75D85C0A28CE80BA00461E80 /* PeersListViewModel.swift in Sources */, From 78b095276ea5f97ac2ca7491e18b143cbb9d2769 Mon Sep 17 00:00:00 2001 From: Jurvis Tan Date: Sat, 24 Dec 2022 21:07:39 -0800 Subject: [PATCH 5/9] Make public interface for requesting funding TX script pubkey --- Lightning/Sources/Lightning/Node.swift | 10 ++++++++++ Shared/LightningNodeService.swift | 8 ++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Lightning/Sources/Lightning/Node.swift b/Lightning/Sources/Lightning/Node.swift index a7e2922..8494eff 100644 --- a/Lightning/Sources/Lightning/Node.swift +++ b/Lightning/Sources/Lightning/Node.swift @@ -192,6 +192,16 @@ public class Node { throw NodeError.Channels.unknown } + public func getFundingTransactionScriptPubKey(outputScript: [UInt8]) async -> String? { + guard let rpcInterface = rpcInterface, + let decodedScript = try? await rpcInterface.decodeScript(script: outputScript), + let addresses = decodedScript["addresses"] as? [String] else { + return nil + } + + return addresses.first + } + public func getFundingTransaction(fundingTxid: String) async -> [UInt8] { // FIXME: We can probably not force unwrap here if we can carefully intialize rpcInterface in the Node's initializer return try! await rpcInterface!.getTransaction(with: fundingTxid) diff --git a/Shared/LightningNodeService.swift b/Shared/LightningNodeService.swift index c29b028..bdcef28 100644 --- a/Shared/LightningNodeService.swift +++ b/Shared/LightningNodeService.swift @@ -57,7 +57,7 @@ class LightningNodeService { try await instance.connectPeer(pubKey: peer.peerPubKey, hostname: peer.connectionInformation.hostname, port: peer.connectionInformation.port) } - func requestChannelOpen(_ pubKeyHex: String, channelValue: UInt64, reserveAmount: UInt64) async throws -> Lightning.Node.ChannelOpenInfo { + func requestChannelOpen(_ pubKeyHex: String, channelValue: UInt64, reserveAmount: UInt64) async throws -> String { do { let channelOpenInfo = try await instance.requestChannelOpen( pubKeyHex, @@ -65,7 +65,11 @@ class LightningNodeService { reserveAmount: reserveAmount ) - return channelOpenInfo + if let scriptPubKey = await instance.getFundingTransactionScriptPubKey(outputScript: channelOpenInfo.fundingOutputScript) { + return scriptPubKey + } else { + throw ServiceError.cannotOpenChannel + } } catch { throw ServiceError.cannotOpenChannel } From e0eee6a4e584a83ab5ed3ab94033894569bd62c5 Mon Sep 17 00:00:00 2001 From: Jurvis Tan Date: Sat, 24 Dec 2022 21:16:19 -0800 Subject: [PATCH 6/9] Force publisher to fire on connection without needing to wait 5s This is helpful in the PeerList screen, where we will not have to wait 5 seconds before getting an accurate representation of the peer connection status --- Lightning/Sources/Lightning/Node.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Lightning/Sources/Lightning/Node.swift b/Lightning/Sources/Lightning/Node.swift index 8494eff..4aa3b01 100644 --- a/Lightning/Sources/Lightning/Node.swift +++ b/Lightning/Sources/Lightning/Node.swift @@ -255,6 +255,7 @@ extension Node { public var connectedPeers: AnyPublisher<[String], Never> { Timer.publish(every: 5, on: .main, in: .default) .autoconnect() + .prepend(Date()) .filter { [weak self] _ in self?.peerManager != nil } .flatMap { [weak self] _ -> AnyPublisher<[String], Never> in let peers = self?.peerManager!.get_peer_node_ids().compactMap { $0.toHexString() } From 7092835a1f878b19f710b75642831e85d66a7f94 Mon Sep 17 00:00:00 2001 From: Jurvis Tan Date: Sat, 24 Dec 2022 21:16:32 -0800 Subject: [PATCH 7/9] Automatically connect to peers on start --- Shared/LightningNodeService.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Shared/LightningNodeService.swift b/Shared/LightningNodeService.swift index bdcef28..9b45ac3 100644 --- a/Shared/LightningNodeService.swift +++ b/Shared/LightningNodeService.swift @@ -48,6 +48,18 @@ class LightningNodeService { do { try await instance.start() + PeerStore.load { [unowned self] result in + switch result { + case .success(let peers): + for peer in peers { + Task { + try! await instance.connectPeer(pubKey: peer.peerPubKey, hostname: peer.connectionInformation.hostname, port: peer.connectionInformation.port) + } + } + case .failure: + print("Error loading peers from disk.") + } + } } catch { throw error } From 9167a6549dccc7d13507e4835d9010719bc385fb Mon Sep 17 00:00:00 2001 From: Jurvis Tan Date: Sun, 25 Dec 2022 21:52:42 -0800 Subject: [PATCH 8/9] Add screens for requesting channel open --- Lightning/Sources/Lightning/Node.swift | 4 +- Shared/Model/Peer.swift | 22 ++++++- Shared/Peer View/PeerRequestChannelView.swift | 61 +++++++++++++++++++ .../PeerRequestChannelViewModel.swift | 44 +++++++++++++ Shared/Peer View/PeerView.swift | 17 ++++-- Shared/Peer View/PeerViewModel.swift | 3 +- Shared/Peers List/PeersListViewModel.swift | 4 +- Surge.xcodeproj/project.pbxproj | 12 ++++ 8 files changed, 157 insertions(+), 10 deletions(-) create mode 100644 Shared/Peer View/PeerRequestChannelView.swift create mode 100644 Shared/Peer View/PeerRequestChannelViewModel.swift diff --git a/Lightning/Sources/Lightning/Node.swift b/Lightning/Sources/Lightning/Node.swift index 4aa3b01..d17e297 100644 --- a/Lightning/Sources/Lightning/Node.swift +++ b/Lightning/Sources/Lightning/Node.swift @@ -195,11 +195,11 @@ public class Node { public func getFundingTransactionScriptPubKey(outputScript: [UInt8]) async -> String? { guard let rpcInterface = rpcInterface, let decodedScript = try? await rpcInterface.decodeScript(script: outputScript), - let addresses = decodedScript["addresses"] as? [String] else { + let address = decodedScript["address"] as? String else { return nil } - return addresses.first + return address } public func getFundingTransaction(fundingTxid: String) async -> [UInt8] { diff --git a/Shared/Model/Peer.swift b/Shared/Model/Peer.swift index 5852645..805bdea 100644 --- a/Shared/Model/Peer.swift +++ b/Shared/Model/Peer.swift @@ -7,11 +7,12 @@ import Foundation -struct Peer: Codable, Equatable { +class Peer: ObservableObject, Codable, Equatable { let id: UUID let peerPubKey: String let name: String let connectionInformation: PeerConnectionInformation + private var pendingFundingTransactionPubKeys: [String] = [] internal init(id: UUID = UUID(), peerPubKey: String, name: String, connectionInformation: PeerConnectionInformation) { self.id = id @@ -19,6 +20,25 @@ struct Peer: Codable, Equatable { self.name = name self.connectionInformation = connectionInformation } + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + do { + self.pendingFundingTransactionPubKeys = try container.decode([String].self, forKey: .pendingFundingTransactionPubKeys) + } catch { + self.pendingFundingTransactionPubKeys = [] + } + + self.id = try container.decode(UUID.self, forKey: .id) + self.peerPubKey = try container.decode(String.self, forKey: .peerPubKey) + self.name = try container.decode(String.self, forKey: .name) + self.connectionInformation = try container.decode(Peer.PeerConnectionInformation.self, forKey: .connectionInformation) + } + + func addFundingTransactionPubkey(pubkey: String) { + pendingFundingTransactionPubKeys.append(pubkey) + } } // MARK: Helper Models diff --git a/Shared/Peer View/PeerRequestChannelView.swift b/Shared/Peer View/PeerRequestChannelView.swift new file mode 100644 index 0000000..253121b --- /dev/null +++ b/Shared/Peer View/PeerRequestChannelView.swift @@ -0,0 +1,61 @@ +// +// PeerRequestChannelView.swift +// Surge +// +// Created by Jurvis on 12/25/22. +// + +import SwiftUI + +struct PeerRequestChannelView: View { + @EnvironmentObject var peer: Peer + @StateObject var viewModel: PeerRequestChannelViewModel + + var body: some View { + NavigationView { + ZStack { + Color(.systemGray6) + .ignoresSafeArea() + + VStack(alignment: .leading) { + TextField("Channel Value", text: $viewModel.channelValue) + .keyboardType(.numberPad) + .font(.subheadline) + .padding(.leading) + .frame(height: 44) + + Divider().padding(.leading, 12) + + TextField("Reserve Amount", text: $viewModel.reserveAmount) + .keyboardType(.numberPad) + .font(.subheadline) + .padding(.leading) + .frame(height: 44) + } + .background(Color.white) + .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) + .padding(.horizontal, 16) + } + } + .navigationTitle("Channel Details") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button { + Task { + await viewModel.createNewFundingTransaction(for: peer) + } + } label: { + Text("Open Channel") + } + .disabled(!viewModel.isSaveButtonEnabled) + } + } + } +} + +struct PeerRequestChannelView_Previews: PreviewProvider { + static var previews: some View { + PeerRequestChannelView(viewModel: PeerRequestChannelViewModel(isViewActive: .constant(true))) + } +} diff --git a/Shared/Peer View/PeerRequestChannelViewModel.swift b/Shared/Peer View/PeerRequestChannelViewModel.swift new file mode 100644 index 0000000..52bb48a --- /dev/null +++ b/Shared/Peer View/PeerRequestChannelViewModel.swift @@ -0,0 +1,44 @@ +// +// PeerRequestChannelViewModel.swift +// Surge +// +// Created by Jurvis on 12/25/22. +// + +import Foundation +import SwiftUI + +class PeerRequestChannelViewModel: ObservableObject { + @Published var channelValue: String = "" + @Published var reserveAmount: String = "" + + var isViewActive: Binding + + internal init(isViewActive: Binding) { + self.isViewActive = isViewActive + } + + var isSaveButtonEnabled: Bool { + return !channelValue.isEmpty && !reserveAmount.isEmpty + } + + func createNewFundingTransaction(for peer: Peer) async { + do { + let scriptPubKey = try await getFundingTransactionScriptPubkey(peer: peer) + peer.addFundingTransactionPubkey(pubkey: scriptPubKey) + DispatchQueue.main.async { [weak self] in + self?.isViewActive.wrappedValue.toggle() + } + } catch { + print("Unable to create funding script pub key") + } + } + + private func getFundingTransactionScriptPubkey(peer: Peer) async throws -> String { + return try await LightningNodeService.shared.requestChannelOpen( + peer.peerPubKey, + channelValue: UInt64(channelValue)!, + reserveAmount: UInt64(reserveAmount)! + ) + } +} diff --git a/Shared/Peer View/PeerView.swift b/Shared/Peer View/PeerView.swift index a367409..315c759 100644 --- a/Shared/Peer View/PeerView.swift +++ b/Shared/Peer View/PeerView.swift @@ -20,13 +20,20 @@ struct PeerView: View { } else { Button("Connect Peer") { Task { - await viewModel.connectPeer() + await viewModel.connectPeer() } } } - - Button("Request Channel Open") { - print("") + + NavigationLink( + destination: PeerRequestChannelView( + viewModel: PeerRequestChannelViewModel( + isViewActive: $viewModel.isShowingEdit + ) + ), + isActive: $viewModel.isShowingEdit + ) { + Text("Request Channel Open") } } Section(header: Text("Pending Funding Scripts")) { @@ -39,7 +46,7 @@ struct PeerView: View { .navigationTitle(viewModel.peer.name) .listStyle(.grouped) } - + .environmentObject(viewModel.peer) } } diff --git a/Shared/Peer View/PeerViewModel.swift b/Shared/Peer View/PeerViewModel.swift index 09bb98f..c745977 100644 --- a/Shared/Peer View/PeerViewModel.swift +++ b/Shared/Peer View/PeerViewModel.swift @@ -12,6 +12,8 @@ class PeerViewModel: ObservableObject { @Published var peer: Peer @Published var activePeerNodeIds: [String] = [] + @Published var isShowingEdit = false + var isPeerConnected: Bool { return activePeerNodeIds.contains(peer.peerPubKey) } @@ -30,7 +32,6 @@ class PeerViewModel: ObservableObject { print("Error connecting to peer") } } - private func setup() { LightningNodeService.shared.activePeersPublisher .assign(to: \.activePeerNodeIds, on: self) diff --git a/Shared/Peers List/PeersListViewModel.swift b/Shared/Peers List/PeersListViewModel.swift index a5bda2f..892bcf4 100644 --- a/Shared/Peers List/PeersListViewModel.swift +++ b/Shared/Peers List/PeersListViewModel.swift @@ -142,7 +142,9 @@ extension PeersListViewModel { } func showPeerScreen(peer: Peer) { - self.sheetToShow = .showPeer(peer) + DispatchQueue.main.async { [unowned self] in + self.sheetToShow = .showPeer(peer) + } } func showPeerView(peer: Peer) -> some View { diff --git a/Surge.xcodeproj/project.pbxproj b/Surge.xcodeproj/project.pbxproj index 847bec8..fbdf312 100644 --- a/Surge.xcodeproj/project.pbxproj +++ b/Surge.xcodeproj/project.pbxproj @@ -27,6 +27,10 @@ 75AA489B28C683EE0032A3DC /* LightningNodeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75AA489928C683EE0032A3DC /* LightningNodeService.swift */; }; 75AA489E28C685A30032A3DC /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 75AA489D28C685A30032A3DC /* CryptoSwift */; }; 75AA48A028C685AA0032A3DC /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 75AA489F28C685AA0032A3DC /* CryptoSwift */; }; + 75C1AD8D295924290086D37A /* PeerRequestChannelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75C1AD8C295924290086D37A /* PeerRequestChannelView.swift */; }; + 75C1AD8E295924290086D37A /* PeerRequestChannelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75C1AD8C295924290086D37A /* PeerRequestChannelView.swift */; }; + 75C1AD90295924C50086D37A /* PeerRequestChannelViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75C1AD8F295924C50086D37A /* PeerRequestChannelViewModel.swift */; }; + 75C1AD91295924C50086D37A /* PeerRequestChannelViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75C1AD8F295924C50086D37A /* PeerRequestChannelViewModel.swift */; }; 75D85C0928CE80BA00461E80 /* PeersListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75D85C0828CE80BA00461E80 /* PeersListViewModel.swift */; }; 75D85C0A28CE80BA00461E80 /* PeersListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75D85C0828CE80BA00461E80 /* PeersListViewModel.swift */; }; 75D85C0C28CE811200461E80 /* PeerCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75D85C0B28CE811200461E80 /* PeerCellViewModel.swift */; }; @@ -86,6 +90,8 @@ 7571CC0A28C5C66300529718 /* Lightning */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Lightning; sourceTree = ""; }; 7571CC0B28C5C68700529718 /* Surge--iOS--Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "Surge--iOS--Info.plist"; sourceTree = ""; }; 75AA489928C683EE0032A3DC /* LightningNodeService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LightningNodeService.swift; sourceTree = ""; }; + 75C1AD8C295924290086D37A /* PeerRequestChannelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerRequestChannelView.swift; sourceTree = ""; }; + 75C1AD8F295924C50086D37A /* PeerRequestChannelViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerRequestChannelViewModel.swift; sourceTree = ""; }; 75D85C0828CE80BA00461E80 /* PeersListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeersListViewModel.swift; sourceTree = ""; }; 75D85C0B28CE811200461E80 /* PeerCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerCellViewModel.swift; sourceTree = ""; }; 75D85C0E28CE811C00461E80 /* PeerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerCell.swift; sourceTree = ""; }; @@ -140,6 +146,8 @@ children = ( 7562ED5F294E995D0055FB06 /* PeerView.swift */, 7562ED612957B6D10055FB06 /* PeerViewModel.swift */, + 75C1AD8C295924290086D37A /* PeerRequestChannelView.swift */, + 75C1AD8F295924C50086D37A /* PeerRequestChannelViewModel.swift */, ); path = "Peer View"; sourceTree = ""; @@ -432,11 +440,13 @@ 75AA489A28C683EE0032A3DC /* LightningNodeService.swift in Sources */, 75D85C1228CE8B5300461E80 /* AddPeerView.swift in Sources */, 75D85C0F28CE811C00461E80 /* PeerCell.swift in Sources */, + 75C1AD90295924C50086D37A /* PeerRequestChannelViewModel.swift in Sources */, 7562ED622957B6D10055FB06 /* PeerViewModel.swift in Sources */, 75D85C0928CE80BA00461E80 /* PeersListViewModel.swift in Sources */, 75D85C1528CE8F5700461E80 /* AddPeerViewModel.swift in Sources */, 75F3FBC928CD978E00FB6D61 /* HomeViewModel.swift in Sources */, 7562ED60294E995D0055FB06 /* PeerView.swift in Sources */, + 75C1AD8D295924290086D37A /* PeerRequestChannelView.swift in Sources */, 75D85C2028CE974900461E80 /* PeerConnectionStatus.swift in Sources */, 75D85C1D28CE92C200461E80 /* PeerStore.swift in Sources */, 75D85C1A28CE927F00461E80 /* Peer.swift in Sources */, @@ -455,11 +465,13 @@ 75D85C0D28CE811200461E80 /* PeerCellViewModel.swift in Sources */, 75AA489B28C683EE0032A3DC /* LightningNodeService.swift in Sources */, 7562ED632957B6D70055FB06 /* PeerViewModel.swift in Sources */, + 75C1AD91295924C50086D37A /* PeerRequestChannelViewModel.swift in Sources */, 75D85C1328CE8B5300461E80 /* AddPeerView.swift in Sources */, 75D85C1028CE811C00461E80 /* PeerCell.swift in Sources */, 75D85C0A28CE80BA00461E80 /* PeersListViewModel.swift in Sources */, 75D85C1628CE8F5700461E80 /* AddPeerViewModel.swift in Sources */, 75F3FBCA28CD978E00FB6D61 /* HomeViewModel.swift in Sources */, + 75C1AD8E295924290086D37A /* PeerRequestChannelView.swift in Sources */, 75D85C2128CE974900461E80 /* PeerConnectionStatus.swift in Sources */, 75D85C1E28CE92C200461E80 /* PeerStore.swift in Sources */, 75D85C1B28CE927F00461E80 /* Peer.swift in Sources */, From 10cc8348be31c48471e6b72ccec9072f19ef7a3b Mon Sep 17 00:00:00 2001 From: Jurvis Tan Date: Sun, 25 Dec 2022 22:40:00 -0800 Subject: [PATCH 9/9] Store and view pending funding tx script pubkeys --- Shared/LightningNodeService.swift | 2 +- Shared/Model/Peer.swift | 2 +- .../PeerRequestChannelViewModel.swift | 9 +++++ Shared/Peer View/PeerView.swift | 4 ++- Shared/Peers List/PeersListViewModel.swift | 2 +- Shared/Stores/PeerStore.swift | 33 +++++++++++++++---- 6 files changed, 41 insertions(+), 11 deletions(-) diff --git a/Shared/LightningNodeService.swift b/Shared/LightningNodeService.swift index 9b45ac3..771c8db 100644 --- a/Shared/LightningNodeService.swift +++ b/Shared/LightningNodeService.swift @@ -51,7 +51,7 @@ class LightningNodeService { PeerStore.load { [unowned self] result in switch result { case .success(let peers): - for peer in peers { + for peer in peers.values { Task { try! await instance.connectPeer(pubKey: peer.peerPubKey, hostname: peer.connectionInformation.hostname, port: peer.connectionInformation.port) } diff --git a/Shared/Model/Peer.swift b/Shared/Model/Peer.swift index 805bdea..a8db4d5 100644 --- a/Shared/Model/Peer.swift +++ b/Shared/Model/Peer.swift @@ -12,7 +12,7 @@ class Peer: ObservableObject, Codable, Equatable { let peerPubKey: String let name: String let connectionInformation: PeerConnectionInformation - private var pendingFundingTransactionPubKeys: [String] = [] + var pendingFundingTransactionPubKeys: [String] = [] internal init(id: UUID = UUID(), peerPubKey: String, name: String, connectionInformation: PeerConnectionInformation) { self.id = id diff --git a/Shared/Peer View/PeerRequestChannelViewModel.swift b/Shared/Peer View/PeerRequestChannelViewModel.swift index 52bb48a..6bab284 100644 --- a/Shared/Peer View/PeerRequestChannelViewModel.swift +++ b/Shared/Peer View/PeerRequestChannelViewModel.swift @@ -26,6 +26,15 @@ class PeerRequestChannelViewModel: ObservableObject { do { let scriptPubKey = try await getFundingTransactionScriptPubkey(peer: peer) peer.addFundingTransactionPubkey(pubkey: scriptPubKey) + PeerStore.update(peer: peer) { result in + switch result { + case .success(_): + print("Saved peer: \(peer.peerPubKey)") + case .failure(_): + // TOODO: Handle saving new funding transaction pubkey error + print("Error persisting new pub key") + } + } DispatchQueue.main.async { [weak self] in self?.isViewActive.wrappedValue.toggle() } diff --git a/Shared/Peer View/PeerView.swift b/Shared/Peer View/PeerView.swift index 315c759..1f7f236 100644 --- a/Shared/Peer View/PeerView.swift +++ b/Shared/Peer View/PeerView.swift @@ -37,7 +37,9 @@ struct PeerView: View { } } Section(header: Text("Pending Funding Scripts")) { - Text("abcdef") + ForEach(viewModel.peer.pendingFundingTransactionPubKeys, id: \.self) { pubKey in + Text(pubKey) + } } Section(header: Text("Active Channels")) { Text("asbdvasd") diff --git a/Shared/Peers List/PeersListViewModel.swift b/Shared/Peers List/PeersListViewModel.swift index 892bcf4..b38d81b 100644 --- a/Shared/Peers List/PeersListViewModel.swift +++ b/Shared/Peers List/PeersListViewModel.swift @@ -110,7 +110,7 @@ extension PeersListViewModel { PeerStore.load { [unowned self] result in switch result { case .success(let peers): - self.peersToShow = peers + self.peersToShow = Array(peers.values) case .failure: self.peersToShow = [] } diff --git a/Shared/Stores/PeerStore.swift b/Shared/Stores/PeerStore.swift index 0232e10..8d33b30 100644 --- a/Shared/Stores/PeerStore.swift +++ b/Shared/Stores/PeerStore.swift @@ -7,21 +7,19 @@ import Foundation -class PeerStore: ObservableObject { - @Published var peers: [Peer] = [] - - static func load(completion: @escaping (Result<[Peer], Error>) -> Void) { +class PeerStore: ObservableObject { + static func load(completion: @escaping (Result<[String:Peer], Error>) -> Void) { DispatchQueue.global(qos: .background).async { do { let fileURL = try fileUrl() guard let file = try? FileHandle(forReadingFrom: fileURL) else { DispatchQueue.main.async { - completion(.success([])) + completion(.success([:])) } return } - let peers = try JSONDecoder().decode([Peer].self, from: file.availableData) + let peers = try JSONDecoder().decode([String:Peer].self, from: file.availableData) DispatchQueue.main.async { completion(.success(peers)) } @@ -34,9 +32,11 @@ class PeerStore: ObservableObject { } static func save(peers: [Peer], completion: @escaping (Result)->Void) { + let dict = Dictionary(uniqueKeysWithValues: peers.map{ ($0.peerPubKey, $0) }) + DispatchQueue.global(qos: .background).async { do { - let data = try JSONEncoder().encode(peers) + let data = try JSONEncoder().encode(dict) let outfile = try fileUrl() try data.write(to: outfile) DispatchQueue.main.async { @@ -51,6 +51,25 @@ class PeerStore: ObservableObject { } } + static func update(peer: Peer, completion: @escaping(Result) -> Void) { + PeerStore.load { result in + switch result { + case .success(var peers): + peers.updateValue(peer, forKey: peer.peerPubKey) + PeerStore.save(peers: Array(peers.values)) { result in + switch result { + case .success(_): + print("Updated Peer Information: \(peer.peerPubKey)") + case .failure(_): + print("Error saving peer information \(peer.peerPubKey)") + } + } + case .failure(let error): + print("Error: \(error)") + } + } + } + private static func fileUrl() throws -> URL { try FileManager.default.url(for: .documentDirectory, in: .userDomainMask,