452 lines
16 KiB
Swift
452 lines
16 KiB
Swift
//
|
|
// 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<Lnrpc_StopResponse>({ (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<Lnrpc_GenSeedResponse> { (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<Lnrpc_WalletBalanceResponse>(completion))
|
|
} catch {
|
|
completion(Lnrpc_WalletBalanceResponse(), error)
|
|
}
|
|
}
|
|
func lightningChannelBalance(_ completion: @escaping (Lnrpc_ChannelBalanceResponse, Error?) -> Void) {
|
|
|
|
|
|
do {
|
|
LndmobileChannelBalance(try Lnrpc_ChannelBalanceRequest().serializedData(),
|
|
LndCallback<Lnrpc_ChannelBalanceResponse>(completion))
|
|
} catch {
|
|
completion(Lnrpc_ChannelBalanceResponse(), error)
|
|
}
|
|
}
|
|
func info(_ completion: @escaping (Lnrpc_GetInfoResponse, Error?) -> Void) {
|
|
do {
|
|
LndmobileGetInfo(try Lnrpc_GetInfoRequest().serializedData(), LndCallback<Lnrpc_GetInfoResponse>(completion))
|
|
} catch {
|
|
completion(Lnrpc_GetInfoResponse(), error)
|
|
}
|
|
}
|
|
|
|
func newAddress(_ completion: @escaping (String, Error?) -> Void) {
|
|
do {
|
|
LndmobileNewAddress(
|
|
try Lnrpc_NewAddressRequest().serializedData(),
|
|
LndCallback<Lnrpc_NewAddressResponse> { (response, error) in
|
|
completion(response.address, error)
|
|
}
|
|
)
|
|
} catch {
|
|
completion("", error)
|
|
}
|
|
}
|
|
func listPeers(completion: @escaping (Lnrpc_ListPeersResponse, Error?) -> Void) {
|
|
LndmobileListPeers(try Data(), LndCallback<Lnrpc_ListPeersResponse>(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<Lnrpc_ConnectPeerResponse>(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<Lnrpc_OpenStatusUpdate>(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<Lnrpc_ClosedChannelsResponse>(completion))
|
|
} catch {
|
|
completion(Lnrpc_ClosedChannelsResponse(), nil)
|
|
}
|
|
}
|
|
func listChannels(_ completion: @escaping (Lnrpc_ListChannelsResponse, Error?) -> Void) {
|
|
do {
|
|
LndmobileListChannels(
|
|
try Lnrpc_ListChannelsRequest().serializedData(),
|
|
LndCallback<Lnrpc_ListChannelsResponse> { (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<Lnrpc_ClosedChannelsResponse> { (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<Lnrpc_AbandonChannelResponse> { (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<Lnrpc_PendingChannelsResponse> { (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<Lnrpc_ChannelEdge>(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<Lnrpc_PayReq>(completion))
|
|
} catch {
|
|
completion(Lnrpc_PayReq(), nil)
|
|
}
|
|
}
|
|
|
|
func listInvoices(completion: @escaping(Lnrpc_ListInvoiceResponse, Error?) -> Void){
|
|
do {
|
|
LndmobileListInvoices(
|
|
try Lnrpc_ListInvoiceRequest().serializedData(),
|
|
LndCallback<Lnrpc_ListInvoiceResponse>{ (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<Lnrpc_ListPaymentsResponse>{ (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<Lnrpc_SendResponse> { (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<Lnrpc_AddInvoiceResponse>{ (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<Lnrpc_QueryRoutesResponse>{ (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<Lnrpc_EstimateFeeResponse>{ (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<Lnrpc_FeeReportResponse>{ (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
|
|
}
|