lnd-demo-app/wallet/Lightning/Lightning.swift
2023-06-08 09:36:06 +03:00

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
}