Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
}
11 changes: 11 additions & 0 deletions Lightning/Sources/Lightning/Node.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 address = decodedScript["address"] as? String else {
return nil
}

return address
}

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)
Expand Down Expand Up @@ -245,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() }
Expand Down
30 changes: 30 additions & 0 deletions Shared/LightningNodeService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.values {
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
}
Expand All @@ -56,6 +68,24 @@ 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 -> String {
do {
let channelOpenInfo = try await instance.requestChannelOpen(
pubKeyHex,
channelValue: channelValue,
reserveAmount: reserveAmount
)

if let scriptPubKey = await instance.getFundingTransactionScriptPubKey(outputScript: channelOpenInfo.fundingOutputScript) {
return scriptPubKey
} else {
throw ServiceError.cannotOpenChannel
}
} catch {
throw ServiceError.cannotOpenChannel
}
}
}

// MARK: Helpers
Expand Down
37 changes: 34 additions & 3 deletions Shared/Model/Peer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@

import Foundation

struct Peer: Identifiable, Codable, Equatable {
class Peer: ObservableObject, Codable, Equatable {
let id: UUID
let peerPubKey: String
let name: String
let connectionInformation: PeerConnectionInformation
var pendingFundingTransactionPubKeys: [String] = []

internal init(id: UUID = UUID(), peerPubKey: String, name: String, connectionInformation: PeerConnectionInformation) {
self.id = id
Expand All @@ -20,8 +21,23 @@ struct Peer: Identifiable, Codable, Equatable {
self.connectionInformation = connectionInformation
}

static func == (lhs: Peer, rhs: Peer) -> Bool {
return lhs.id == rhs.id
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)
}
}

Expand All @@ -32,3 +48,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
}
}

61 changes: 61 additions & 0 deletions Shared/Peer View/PeerRequestChannelView.swift
Original file line number Diff line number Diff line change
@@ -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)))
}
}
53 changes: 53 additions & 0 deletions Shared/Peer View/PeerRequestChannelViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//
// 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<Bool>

internal init(isViewActive: Binding<Bool>) {
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)
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()
}
} 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)!
)
}
}
59 changes: 59 additions & 0 deletions Shared/Peer View/PeerView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//
// 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()
}
}
}

NavigationLink(
destination: PeerRequestChannelView(
viewModel: PeerRequestChannelViewModel(
isViewActive: $viewModel.isShowingEdit
)
),
isActive: $viewModel.isShowingEdit
) {
Text("Request Channel Open")
}
}
Section(header: Text("Pending Funding Scripts")) {
ForEach(viewModel.peer.pendingFundingTransactionPubKeys, id: \.self) { pubKey in
Text(pubKey)
}
}
Section(header: Text("Active Channels")) {
Text("asbdvasd")
}
}
.navigationTitle(viewModel.peer.name)
.listStyle(.grouped)
}
.environmentObject(viewModel.peer)
}
}

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))))
}
}
40 changes: 40 additions & 0 deletions Shared/Peer View/PeerViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//
// 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] = []

@Published var isShowingEdit = false

var isPeerConnected: Bool {
return activePeerNodeIds.contains(peer.peerPubKey)
}

private var cancellables = Set<AnyCancellable>()

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)
}
}
Loading