// // Lightning.swift // // // Created by Jason van den Berg on 2020/08/02. // import Foundation class Lightning { static let shared = Lightning() private var storage: URL { let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! let directory = documentsDirectory.appendingPathComponent("lnd") if !FileManager.default.fileExists(atPath: directory.path) { try! FileManager.default.createDirectory(atPath: directory.path, withIntermediateDirectories: true) } return directory } private let confName = "lnd.conf" private var confFile: URL { return storage.appendingPathComponent(confName) } //Ensure it stays a singleton private init() {} func start(_ completion: @escaping (Error?) -> Void, onRpcReady: @escaping (Error?) -> Void) { print("LND Start Request") //Delete previous config if it exists try? FileManager.default.removeItem(at: confFile) //Copy new config into LND directory do { //TODO build this config file in code let originalConf = "lnd.conf" try FileManager.default.copyItem(at: Bundle.main.bundleURL.appendingPathComponent(originalConf), to: confFile) } catch { return completion(error) } let args = "--lnddir=\(storage.path)" print(args) LndmobileStart( args, LndEmptyResponseCallback { (error) in completion(error) if error == nil { EventBus.postToMainThread(.lndStarted) } }, LndEmptyResponseCallback { (error) in onRpcReady(error) if error == nil { EventBus.postToMainThread(.lndRpcReady) } } ) } func stop(_ completion: @escaping (Error?) -> Void) { print("LND Stop Request") do { LndmobileStopDaemon( try Lnrpc_StopRequest().serializedData(), LndCallback({ (response, error) in completion(error) if error == nil { EventBus.postToMainThread(.lndStopped) } }) ) completion(nil) //TODO figure out why callback is never hit by LndGenericCallback } catch { completion(error) } } func generateSeed(_ completion: @escaping ([String], Error?) -> Void) { do { LndmobileGenSeed( try Lnrpc_GenSeedRequest().serializedData(), LndCallback { (response, error) in completion(response.cipherSeedMnemonic, error) } ) } catch { completion([], error) } } func createWallet(password: String, cipherSeedMnemonic: [String], completion: @escaping (Error?) -> Void) { guard let passwordData = password.data(using: .utf8) else { return completion(LightningError.invalidPassword) } var request = Lnrpc_InitWalletRequest() request.cipherSeedMnemonic = cipherSeedMnemonic request.walletPassword = passwordData do { LndmobileInitWallet( try request.serializedData(), LndEmptyResponseCallback { (error) in completion(error) if error == nil { EventBus.postToMainThread(.lndWalletUnlocked) } } ) } catch { return completion(error) } } func unlockWalet(password: String, completion: @escaping (Error?) -> Void) { guard let passwordData = password.data(using: .utf8) else { return completion(LightningError.invalidPassword) } var request = Lnrpc_UnlockWalletRequest() request.walletPassword = passwordData do { LndmobileUnlockWallet( try request.serializedData(), LndEmptyResponseCallback { (error) in completion(error) if error == nil { EventBus.postToMainThread(.lndWalletUnlocked) } } ) } catch { return completion(error) } } func walletBalance(_ completion: @escaping (Lnrpc_WalletBalanceResponse, Error?) -> Void) { do { LndmobileWalletBalance(try Lnrpc_WalletBalanceRequest().serializedData(), LndCallback(completion)) } catch { completion(Lnrpc_WalletBalanceResponse(), error) } } func lightningChannelBalance(_ completion: @escaping (Lnrpc_ChannelBalanceResponse, Error?) -> Void) { do { LndmobileChannelBalance(try Lnrpc_ChannelBalanceRequest().serializedData(), LndCallback(completion)) } catch { completion(Lnrpc_ChannelBalanceResponse(), error) } } func info(_ completion: @escaping (Lnrpc_GetInfoResponse, Error?) -> Void) { do { LndmobileGetInfo(try Lnrpc_GetInfoRequest().serializedData(), LndCallback(completion)) } catch { completion(Lnrpc_GetInfoResponse(), error) } } func newAddress(_ completion: @escaping (String, Error?) -> Void) { do { LndmobileNewAddress( try Lnrpc_NewAddressRequest().serializedData(), LndCallback { (response, error) in completion(response.address, error) } ) } catch { completion("", error) } } func listPeers(completion: @escaping (Lnrpc_ListPeersResponse, Error?) -> Void) { LndmobileListPeers(try Data(), LndCallback(completion)) } func connectToNode(nodePubkey: NodePublicKey, hostAddress: String, hostPort: UInt, _ completion: @escaping (Lnrpc_ConnectPeerResponse, Error?) -> Void) { var request = Lnrpc_ConnectPeerRequest() var addr = Lnrpc_LightningAddress() addr.pubkey = nodePubkey.hexString addr.host = "\(hostAddress):\(hostPort)" request.addr = addr request.perm = true do { LndmobileConnectPeer(try request.serializedData(), LndCallback(completion)) } catch { completion(Lnrpc_ConnectPeerResponse(), error) } } func openChannel(localFundingAmount: Int64, pushSat: Int64, closeAddress: String?, nodePubkey: NodePublicKey, _ completion: @escaping (Lnrpc_OpenStatusUpdate, Error?) -> Void) { var request = Lnrpc_OpenChannelRequest() request.localFundingAmount = localFundingAmount if let closeAddress = closeAddress{ request.closeAddress = closeAddress } request.nodePubkey = nodePubkey.data request.pushSat = pushSat //TODO have the below config driven request.minConfs = 2 request.targetConf = 2 request.spendUnconfirmed = false do { LndmobileOpenChannel(try request.serializedData(), LndCallback(completion)) } catch { completion(Lnrpc_OpenStatusUpdate(), nil) } } func closeChannel(channel: Lnrpc_Channel, _ completion: @escaping (Lnrpc_ClosedChannelsResponse, Error?) -> Void) { var request = Lnrpc_CloseChannelRequest() request.channelPoint = Lnrpc_ChannelPoint() let ch = channel.channelPoint.split(separator: ":") if ch.count == 2{ request.channelPoint.fundingTxidStr = String(ch[0]) request.channelPoint.outputIndex = UInt32(ch[1]) ?? 1 } debugPrint("channel \(channel), point \(channel.channelPoint), property list \(channel.channelPoint.propertyList())") do { LndmobileCloseChannel(try request.serializedData(), LndCallback(completion)) } catch { completion(Lnrpc_ClosedChannelsResponse(), nil) } } func listChannels(_ completion: @escaping (Lnrpc_ListChannelsResponse, Error?) -> Void) { do { LndmobileListChannels( try Lnrpc_ListChannelsRequest().serializedData(), LndCallback { (response, error) in completion(response, error) } ) } catch { completion(Lnrpc_ListChannelsResponse(), error) } } func listClosedChannels(_ completion: @escaping (Lnrpc_ClosedChannelsResponse, Error?) -> Void) { do { LndmobileClosedChannels( try Lnrpc_ClosedChannelsRequest().serializedData(), LndCallback { (response, error) in completion(response, error) } ) } catch { completion(Lnrpc_ClosedChannelsResponse(), error) } } func abandonChannel(pendingOpenChannel:Lnrpc_PendingChannelsResponse.PendingOpenChannel, _ completion: @escaping (Lnrpc_AbandonChannelResponse, Error?) -> Void) { var request = Lnrpc_AbandonChannelRequest() do { request.channelPoint = try Lnrpc_ChannelPoint(jsonString: pendingOpenChannel.channel.channelPoint) LndmobileListChannels( try request.serializedData(), LndCallback { (response, error) in debugPrint("Response Abandon channel", response, error) completion(response, error) } ) } catch { completion(Lnrpc_AbandonChannelResponse(), error) } } func listPendingChannels(_ completion: @escaping (Lnrpc_PendingChannelsResponse, Error?) -> Void) { do { LndmobilePendingChannels( try Lnrpc_PendingChannelsRequest().serializedData(), LndCallback { (response, error) in completion(response, error) } ) } catch { completion(Lnrpc_PendingChannelsResponse(), error) } } func getChanInfo(id: UInt64, _ completion: @escaping (Lnrpc_ChannelEdge, Error?) -> Void) { var request = Lnrpc_ChanInfoRequest() request.chanID = id do { LndmobileGetChanInfo(try request.serializedData(), LndCallback(completion)) } catch { completion(Lnrpc_ChannelEdge(), nil) } } func decodePaymentRequest(_ paymentRequest: String, _ completion: @escaping (Lnrpc_PayReq, Error?) -> Void) { var request = Lnrpc_PayReqString() request.payReq = paymentRequest do { LndmobileDecodePayReq(try request.serializedData(), LndCallback(completion)) } catch { completion(Lnrpc_PayReq(), nil) } } func listInvoices(completion: @escaping(Lnrpc_ListInvoiceResponse, Error?) -> Void){ do { LndmobileListInvoices( try Lnrpc_ListInvoiceRequest().serializedData(), LndCallback{ (response, error) in completion(response, error) } ) } catch { completion(Lnrpc_ListInvoiceResponse(), error) } } func listPayments(completion: @escaping(Lnrpc_ListPaymentsResponse, Error?) -> Void){ do { LndmobileListPayments( try Lnrpc_ListPaymentsRequest().serializedData(), LndCallback{ (response, error) in completion(response, error) } ) } catch { completion(Lnrpc_ListPaymentsResponse(), error) } } func payRequest(_ paymentRequest: String, _ completion: @escaping (Lnrpc_SendResponse, Error?) -> Void) { var request = Lnrpc_SendRequest() request.paymentRequest = paymentRequest do { //LND returns payment errors in the response and not with a real error. This just intercepts the callback and will return the custom error if applicable. LndmobileSendPaymentSync( try request.serializedData(), LndCallback { (response, error) in completion(response, error) }) } catch { completion(Lnrpc_SendResponse(), nil) } } func createPayRequest(amount: Int, memo: String, _ completion: @escaping (Lnrpc_AddInvoiceResponse, Error?) -> Void) { let request = Lnrpc_Invoice(amount: amount, memo: memo, expiry: .oneDay) do { LndmobileAddInvoice( try request.serializedData(), LndCallback{ (response, error) in guard response.paymentRequest.isEmpty else { completion(response, error) return } completion(response, error) }) } catch { completion(Lnrpc_AddInvoiceResponse(), nil) } } func queryRequestFee(key: String, _ completion: @escaping (Lnrpc_QueryRoutesResponse, Error?) -> Void) { var request = Lnrpc_QueryRoutesRequest() request.pubKey = key do { LndmobileQueryRoutes( try request.serializedData(), LndCallback{ (response, error) in completion(response, error) }) } catch { completion(Lnrpc_QueryRoutesResponse(), nil) } } func estimateFee(_ paymentRequest: String, amount:Int64, _ completion: @escaping (Lnrpc_EstimateFeeResponse, Error?) -> Void) { var request = Lnrpc_EstimateFeeRequest() request.addrToAmount = [paymentRequest:amount] do { LndmobileEstimateFee( try request.serializedData(), LndCallback{ (response, error) in completion(response, error) }) } catch { completion(Lnrpc_EstimateFeeResponse(), nil) } } func feeReport(_ completion: @escaping (Lnrpc_FeeReportResponse, Error?) -> Void) { let request = Lnrpc_FeeReportRequest() do { LndmobileFeeReport( try request.serializedData(), LndCallback{ (response, error) in completion(response, error) }) } catch { completion(Lnrpc_FeeReportResponse(), nil) } } } //Utils extension Lightning { func purge() { //TODO ensure testnet only print("WARNING: removing existing LND directory") try! FileManager.default.removeItem(at: storage) } } extension Lnrpc_Invoice { init(amount: Int?, memo: String?, expiry: ExpiryTime?) { self.init() if let amount = amount { value = Int64(amount) } if let memo = memo { self.memo = memo } if let expiry = expiry { self.expiry = Int64(expiry.rawValue) } `private` = true } } public enum ExpiryTime: Int, Codable, CaseIterable { case oneMinute = 60 case tenMinutes = 600 // 60 * 10 case thirtyMinutes = 1800 // 60 * 30 case oneHour = 3600 // 60 * 60 case sixHours = 21600 // 60 * 60 * 6 case oneDay = 86400 // 60 * 60 * 24 case oneWeek = 604800 // 60 * 60 * 24 * 7 case thirtyDays = 2592000 // 60 * 60 * 24 * 30 case oneYear = 31536000 // 60 * 60 * 24 * 365 }