mirror of
https://github.com/aljazceru/breez-sdk-liquid.git
synced 2025-12-24 01:14:22 +01:00
Swift notification plugin (#436)
* Add Swift notification plugin * Hash the metadata * Validate min sendable amount * Remove initializer as base class, UNNotificationServiceExtension, has no default initializer * Set the PaymentMethod * Handle PaymentDetails in SwapUpdated * Improve payment text
This commit is contained in:
4
.github/workflows/publish-swift.yml
vendored
4
.github/workflows/publish-swift.yml
vendored
@@ -90,6 +90,10 @@ jobs:
|
||||
zip -9 -r breez_sdk_liquidFFI.xcframework.zip breez_sdk_liquidFFI.xcframework
|
||||
echo "XCF_CHECKSUM=`swift package compute-checksum breez_sdk_liquidFFI.xcframework.zip`" >> $GITHUB_ENV
|
||||
|
||||
- name: Remove dist Sources
|
||||
working-directory: dist
|
||||
run: git rm -r Sources
|
||||
|
||||
- name: Update Swift Package definition
|
||||
working-directory: build/lib/bindings/langs/swift
|
||||
run: |
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import Foundation
|
||||
import os.log
|
||||
|
||||
#if DEBUG && true
|
||||
fileprivate var logger = OSLog(
|
||||
subsystem: Bundle.main.bundleIdentifier!,
|
||||
category: "BreezSDKLiquidConnector"
|
||||
)
|
||||
#else
|
||||
fileprivate var logger = OSLog.disabled
|
||||
#endif
|
||||
|
||||
class BreezSDKLiquidConnector {
|
||||
private static var liquidSDK: BindingLiquidSdk? = nil
|
||||
fileprivate static var queue = DispatchQueue(label: "BreezSDKLiquidConnector")
|
||||
fileprivate static var sdkListener: EventListener? = nil
|
||||
|
||||
static func register(connectRequest: ConnectRequest, listener: EventListener) throws -> BindingLiquidSdk? {
|
||||
try BreezSDKLiquidConnector.queue.sync { [] in
|
||||
BreezSDKLiquidConnector.sdkListener = listener
|
||||
if BreezSDKLiquidConnector.liquidSDK == nil {
|
||||
BreezSDKLiquidConnector.liquidSDK = try BreezSDKLiquidConnector.connectSDK(connectRequest: connectRequest)
|
||||
}
|
||||
return BreezSDKLiquidConnector.liquidSDK
|
||||
}
|
||||
}
|
||||
|
||||
static func unregister() {
|
||||
BreezSDKLiquidConnector.queue.sync { [] in
|
||||
BreezSDKLiquidConnector.sdkListener = nil
|
||||
}
|
||||
}
|
||||
|
||||
static func connectSDK(connectRequest: ConnectRequest) throws -> BindingLiquidSdk? {
|
||||
// Connect to the Breez Liquid SDK make it ready for use
|
||||
os_log("Connecting to Breez Liquid SDK", log: logger, type: .debug)
|
||||
let liquidSDK = try connect(req: connectRequest)
|
||||
os_log("Connected to Breez Liquid SDK", log: logger, type: .debug)
|
||||
let _ = try liquidSDK.addEventListener(listener: BreezSDKEventListener())
|
||||
return liquidSDK
|
||||
}
|
||||
}
|
||||
|
||||
class BreezSDKEventListener: EventListener {
|
||||
func onEvent(e: SdkEvent) {
|
||||
BreezSDKLiquidConnector.queue.async { [] in
|
||||
BreezSDKLiquidConnector.sdkListener?.onEvent(e: e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import Foundation
|
||||
|
||||
struct Constants {
|
||||
// Notification Threads
|
||||
static let NOTIFICATION_THREAD_LNURL_PAY = "LNURL_PAY"
|
||||
static let NOTIFICATION_THREAD_SWAP_UPDATED = "SWAP_UPDATED"
|
||||
|
||||
// Message Data
|
||||
static let MESSAGE_DATA_TYPE = "notification_type"
|
||||
static let MESSAGE_DATA_PAYLOAD = "notification_payload"
|
||||
|
||||
static let MESSAGE_TYPE_SWAP_UPDATED = "swap_updated"
|
||||
static let MESSAGE_TYPE_LNURL_PAY_INFO = "lnurlpay_info"
|
||||
static let MESSAGE_TYPE_LNURL_PAY_INVOICE = "lnurlpay_invoice"
|
||||
|
||||
// Resource Identifiers
|
||||
static let LNURL_PAY_INFO_NOTIFICATION_TITLE = "lnurl_pay_info_notification_title"
|
||||
static let LNURL_PAY_INVOICE_NOTIFICATION_TITLE = "lnurl_pay_invoice_notification_title"
|
||||
static let LNURL_PAY_METADATA_PLAIN_TEXT = "lnurl_pay_metadata_plain_text"
|
||||
static let LNURL_PAY_NOTIFICATION_FAILURE_TITLE = "lnurl_pay_notification_failure_title"
|
||||
static let PAYMENT_RECEIVED_NOTIFICATION_TITLE = "payment_received_notification_title"
|
||||
static let PAYMENT_SENT_NOTIFICATION_TITLE = "payment_sent_notification_title"
|
||||
static let SWAP_CONFIRMED_NOTIFICATION_FAILURE_TEXT = "swap_confirmed_notification_failure_text"
|
||||
static let SWAP_CONFIRMED_NOTIFICATION_FAILURE_TITLE = "swap_confirmed_notification_failure_title"
|
||||
|
||||
// Resource Identifier Defaults
|
||||
static let DEFAULT_LNURL_PAY_INFO_NOTIFICATION_TITLE = "Retrieving Payment Information"
|
||||
static let DEFAULT_LNURL_PAY_INVOICE_NOTIFICATION_TITLE = "Fetching Invoice"
|
||||
static let DEFAULT_LNURL_PAY_METADATA_PLAIN_TEXT = "Pay with LNURL"
|
||||
static let DEFAULT_LNURL_PAY_NOTIFICATION_FAILURE_TITLE = "Receive Payment Failed"
|
||||
static let DEFAULT_PAYMENT_RECEIVED_NOTIFICATION_TITLE = "Received %d sats"
|
||||
static let DEFAULT_PAYMENT_SENT_NOTIFICATION_TITLE = "Sent %d sats"
|
||||
static let DEFAULT_SWAP_CONFIRMED_NOTIFICATION_FAILURE_TEXT = "Tap to complete payment"
|
||||
static let DEFAULT_SWAP_CONFIRMED_NOTIFICATION_FAILURE_TITLE = "Payment Pending"
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import Foundation
|
||||
|
||||
public class ResourceHelper {
|
||||
public static let shared = ResourceHelper()
|
||||
|
||||
private init() {/* must use shared instance */}
|
||||
|
||||
public func getString(key: String, fallback: String) -> String {
|
||||
return getString(key: key, validateContains: nil, fallback: fallback)
|
||||
}
|
||||
|
||||
public func getString(key: String, validateContains: String?, fallback: String) -> String {
|
||||
if let str = Bundle.main.object(forInfoDictionaryKey: key) as? String {
|
||||
if validateContains == nil || str.contains(validateContains!) {
|
||||
return str
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
}}
|
||||
@@ -0,0 +1,94 @@
|
||||
import UserNotifications
|
||||
import os.log
|
||||
|
||||
open class SDKNotificationService: UNNotificationServiceExtension {
|
||||
fileprivate let TAG = "SDKNotificationService"
|
||||
|
||||
var liquidSDK: BindingLiquidSdk?
|
||||
var contentHandler: ((UNNotificationContent) -> Void)?
|
||||
var bestAttemptContent: UNMutableNotificationContent?
|
||||
var currentTask: TaskProtocol?
|
||||
public var logger: ServiceLogger = ServiceLogger(logStream: nil)
|
||||
|
||||
override public init() { }
|
||||
|
||||
override open func didReceive(
|
||||
_ request: UNNotificationRequest,
|
||||
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
|
||||
) {
|
||||
self.logger.log(tag: TAG, line: "Notification received", level: "INFO")
|
||||
self.contentHandler = contentHandler
|
||||
self.bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
|
||||
|
||||
guard let connectRequest = self.getConnectRequest() else {
|
||||
if let content = bestAttemptContent {
|
||||
contentHandler(content)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if let currentTask = self.getTaskFromNotification() {
|
||||
self.currentTask = currentTask
|
||||
|
||||
DispatchQueue.main.async { [self] in
|
||||
do {
|
||||
logger.log(tag: TAG, line: "Breez Liquid SDK is not connected, connecting...", level: "INFO")
|
||||
liquidSDK = try BreezSDKLiquidConnector.register(connectRequest: connectRequest, listener: currentTask)
|
||||
logger.log(tag: TAG, line: "Breez Liquid SDK connected successfully", level: "INFO")
|
||||
try currentTask.start(liquidSDK: liquidSDK!)
|
||||
} catch {
|
||||
logger.log(tag: TAG, line: "Breez Liquid SDK connection failed \(error)", level: "ERROR")
|
||||
shutdown()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open func getConnectRequest() -> ConnectRequest? {
|
||||
return nil
|
||||
}
|
||||
|
||||
open func getTaskFromNotification() -> TaskProtocol? {
|
||||
guard let content = bestAttemptContent else { return nil }
|
||||
guard let notificationType = content.userInfo[Constants.MESSAGE_DATA_TYPE] as? String else { return nil }
|
||||
self.logger.log(tag: TAG, line: "Notification payload: \(content.userInfo)", level: "INFO")
|
||||
self.logger.log(tag: TAG, line: "Notification type: \(notificationType)", level: "INFO")
|
||||
|
||||
guard let payload = content.userInfo[Constants.MESSAGE_DATA_PAYLOAD] as? String else {
|
||||
contentHandler!(content)
|
||||
return nil
|
||||
}
|
||||
|
||||
self.logger.log(tag: TAG, line: "\(notificationType) data string: \(payload)", level: "INFO")
|
||||
switch(notificationType) {
|
||||
case Constants.MESSAGE_TYPE_SWAP_UPDATED:
|
||||
return SwapUpdatedTask(payload: payload, logger: self.logger, contentHandler: contentHandler, bestAttemptContent: bestAttemptContent)
|
||||
case Constants.MESSAGE_TYPE_LNURL_PAY_INFO:
|
||||
return LnurlPayInfoTask(payload: payload, logger: self.logger, contentHandler: contentHandler, bestAttemptContent: bestAttemptContent)
|
||||
case Constants.MESSAGE_TYPE_LNURL_PAY_INVOICE:
|
||||
return LnurlPayInvoiceTask(payload: payload, logger: self.logger, contentHandler: contentHandler, bestAttemptContent: bestAttemptContent)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override open func serviceExtensionTimeWillExpire() {
|
||||
self.logger.log(tag: TAG, line: "serviceExtensionTimeWillExpire()", level: "INFO")
|
||||
|
||||
// iOS calls this function just before the extension will be terminated by the system.
|
||||
// Use this as an opportunity to deliver your "best attempt" at modified content,
|
||||
// otherwise the original push payload will be used.
|
||||
self.shutdown()
|
||||
}
|
||||
|
||||
private func shutdown() -> Void {
|
||||
self.logger.log(tag: TAG, line: "shutting down...", level: "INFO")
|
||||
BreezSDKLiquidConnector.unregister()
|
||||
self.logger.log(tag: TAG, line: "task unregistered", level: "INFO")
|
||||
self.currentTask?.onShutdown()
|
||||
}
|
||||
|
||||
public func setLogger(logger: Logger) {
|
||||
self.logger = ServiceLogger(logStream: logger)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import Foundation
|
||||
import os.log
|
||||
|
||||
#if DEBUG && true
|
||||
fileprivate var logger = OSLog(
|
||||
subsystem: Bundle.main.bundleIdentifier!,
|
||||
category: "ServiceLogger"
|
||||
)
|
||||
#else
|
||||
fileprivate var logger = OSLog.disabled
|
||||
#endif
|
||||
|
||||
open class ServiceLogger {
|
||||
var logStream: Logger?
|
||||
|
||||
init(logStream: Logger?) {
|
||||
self.logStream = logStream
|
||||
}
|
||||
|
||||
public func log(tag: String, line: String, level: String) {
|
||||
if let logger = logStream {
|
||||
logger.log(l: LogEntry(line: line, level: level))
|
||||
} else {
|
||||
switch(level) {
|
||||
case "ERROR":
|
||||
os_log("[%{public}@] %{public}@", log: logger, type: .error, tag, line)
|
||||
break
|
||||
case "INFO", "WARN":
|
||||
os_log("[%{public}@] %{public}@", log: logger, type: .info, tag, line)
|
||||
break
|
||||
case "TRACE":
|
||||
os_log("[%{public}@] %{public}@", log: logger, type: .debug, tag, line)
|
||||
break
|
||||
default:
|
||||
os_log("[%{public}@] %{public}@", log: logger, type: .default, tag, line)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import Foundation
|
||||
import CommonCrypto
|
||||
|
||||
extension Data {
|
||||
public func sha256() -> String {
|
||||
return hexStringFromData(input: digest(input: self as NSData))
|
||||
}
|
||||
|
||||
private func digest(input : NSData) -> NSData {
|
||||
let digestLength = Int(CC_SHA256_DIGEST_LENGTH)
|
||||
var hash = [UInt8](repeating: 0, count: digestLength)
|
||||
CC_SHA256(input.bytes, UInt32(input.length), &hash)
|
||||
return NSData(bytes: hash, length: digestLength)
|
||||
}
|
||||
|
||||
private func hexStringFromData(input: NSData) -> String {
|
||||
var bytes = [UInt8](repeating: 0, count: input.length)
|
||||
input.getBytes(&bytes, length: input.length)
|
||||
|
||||
var hexString = ""
|
||||
for byte in bytes {
|
||||
hexString += String(format:"%02x", UInt8(byte))
|
||||
}
|
||||
|
||||
return hexString
|
||||
}
|
||||
}
|
||||
|
||||
public extension String {
|
||||
func sha256() -> String {
|
||||
if let stringData = self.data(using: String.Encoding.utf8) {
|
||||
return stringData.sha256()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import UserNotifications
|
||||
import Foundation
|
||||
|
||||
struct LnurlErrorResponse: Decodable, Encodable {
|
||||
let status: String
|
||||
let reason: String
|
||||
|
||||
init(status: String, reason: String) {
|
||||
self.status = status
|
||||
self.reason = reason
|
||||
}
|
||||
}
|
||||
|
||||
class LnurlPayTask : TaskProtocol {
|
||||
var payload: String
|
||||
var contentHandler: ((UNNotificationContent) -> Void)?
|
||||
var bestAttemptContent: UNMutableNotificationContent?
|
||||
var logger: ServiceLogger
|
||||
var successNotificationTitle: String
|
||||
var failNotificationTitle: String
|
||||
|
||||
init(payload: String, logger: ServiceLogger, contentHandler: ((UNNotificationContent) -> Void)? = nil, bestAttemptContent: UNMutableNotificationContent? = nil, successNotificationTitle: String, failNotificationTitle: String) {
|
||||
self.payload = payload
|
||||
self.contentHandler = contentHandler
|
||||
self.bestAttemptContent = bestAttemptContent
|
||||
self.logger = logger
|
||||
self.successNotificationTitle = successNotificationTitle;
|
||||
self.failNotificationTitle = failNotificationTitle;
|
||||
}
|
||||
|
||||
func start(liquidSDK: BindingLiquidSdk) throws {}
|
||||
|
||||
public func onEvent(e: SdkEvent) {}
|
||||
|
||||
func onShutdown() {
|
||||
displayPushNotification(title: self.failNotificationTitle, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_LNURL_PAY)
|
||||
}
|
||||
|
||||
func replyServer(encodable: Encodable, replyURL: String) {
|
||||
guard let serverReplyURL = URL(string: replyURL) else {
|
||||
self.displayPushNotification(title: self.failNotificationTitle, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_LNURL_PAY)
|
||||
return
|
||||
}
|
||||
var request = URLRequest(url: serverReplyURL)
|
||||
request.httpMethod = "POST"
|
||||
request.httpBody = try! JSONEncoder().encode(encodable)
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
let statusCode = (response as! HTTPURLResponse).statusCode
|
||||
|
||||
if statusCode == 200 {
|
||||
self.displayPushNotification(title: self.successNotificationTitle, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_LNURL_PAY)
|
||||
} else {
|
||||
self.displayPushNotification(title: self.failNotificationTitle, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_LNURL_PAY)
|
||||
return
|
||||
}
|
||||
}
|
||||
task.resume()
|
||||
}
|
||||
|
||||
func fail(withError: String, replyURL: String, failNotificationTitle: String? = nil) {
|
||||
if let serverReplyURL = URL(string: replyURL) {
|
||||
var request = URLRequest(url: serverReplyURL)
|
||||
request.httpMethod = "POST"
|
||||
request.httpBody = try! JSONEncoder().encode(LnurlErrorResponse(status: "ERROR", reason: withError))
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
let _ = (response as! HTTPURLResponse).statusCode
|
||||
}
|
||||
task.resume()
|
||||
}
|
||||
let title = failNotificationTitle != nil ? failNotificationTitle! : self.failNotificationTitle
|
||||
self.displayPushNotification(title: title, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_LNURL_PAY)
|
||||
}
|
||||
}
|
||||
|
||||
enum InvalidLnurlPayError: Error {
|
||||
case minSendable
|
||||
case amount(amount: UInt64)
|
||||
}
|
||||
|
||||
extension InvalidLnurlPayError: LocalizedError {
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .minSendable:
|
||||
return NSLocalizedString("Minimum sendable amount is invalid", comment: "InvalidLnurlPayError")
|
||||
case .amount(amount: let amount):
|
||||
return NSLocalizedString("Invalid amount requested \(amount)", comment: "InvalidLnurlPayError")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import UserNotifications
|
||||
import Foundation
|
||||
|
||||
struct LnurlInfoRequest: Codable {
|
||||
let callback_url: String
|
||||
let reply_url: String
|
||||
}
|
||||
|
||||
struct LnurlInfoResponse: Decodable, Encodable {
|
||||
let callback: String
|
||||
let maxSendable: UInt64
|
||||
let minSendable: UInt64
|
||||
let metadata: String
|
||||
let tag: String
|
||||
|
||||
init(callback: String, maxSendable: UInt64, minSendable: UInt64, metadata: String, tag: String) {
|
||||
self.callback = callback
|
||||
self.maxSendable = maxSendable
|
||||
self.minSendable = minSendable
|
||||
self.metadata = metadata
|
||||
self.tag = tag
|
||||
}
|
||||
}
|
||||
|
||||
class LnurlPayInfoTask : LnurlPayTask {
|
||||
fileprivate let TAG = "LnurlPayInfoTask"
|
||||
|
||||
init(payload: String, logger: ServiceLogger, contentHandler: ((UNNotificationContent) -> Void)? = nil, bestAttemptContent: UNMutableNotificationContent? = nil) {
|
||||
let successNotificationTitle = ResourceHelper.shared.getString(key: Constants.LNURL_PAY_INFO_NOTIFICATION_TITLE, fallback: Constants.DEFAULT_LNURL_PAY_INFO_NOTIFICATION_TITLE)
|
||||
let failNotificationTitle = ResourceHelper.shared.getString(key: Constants.LNURL_PAY_NOTIFICATION_FAILURE_TITLE, fallback: Constants.DEFAULT_LNURL_PAY_NOTIFICATION_FAILURE_TITLE)
|
||||
super.init(payload: payload, logger: logger, contentHandler: contentHandler, bestAttemptContent: bestAttemptContent, successNotificationTitle: successNotificationTitle, failNotificationTitle: failNotificationTitle)
|
||||
}
|
||||
|
||||
override func start(liquidSDK: BindingLiquidSdk) throws {
|
||||
var request: LnurlInfoRequest? = nil
|
||||
do {
|
||||
request = try JSONDecoder().decode(LnurlInfoRequest.self, from: self.payload.data(using: .utf8)!)
|
||||
} catch let e {
|
||||
self.logger.log(tag: TAG, line: "failed to decode payload: \(e)", level: "ERROR")
|
||||
self.displayPushNotification(title: self.failNotificationTitle, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_LNURL_PAY)
|
||||
throw e
|
||||
}
|
||||
|
||||
do {
|
||||
// Get the lightning limits
|
||||
let limits = try liquidSDK.fetchLightningLimits()
|
||||
// Max millisatoshi amount LN SERVICE is willing to receive
|
||||
let maxSendableMsat = limits.receive.maxSat * UInt64(1000)
|
||||
// Min millisatoshi amount LN SERVICE is willing to receive, can not be less than 1 or more than `maxSendableMsat`
|
||||
let minSendableMsat = limits.receive.minSat * UInt64(1000)
|
||||
if minSendableMsat < UInt64(1) || minSendableMsat > maxSendableMsat {
|
||||
throw InvalidLnurlPayError.minSendable
|
||||
}
|
||||
// Format the response
|
||||
let plainTextMetadata = ResourceHelper.shared.getString(key: Constants.LNURL_PAY_METADATA_PLAIN_TEXT, fallback: Constants.DEFAULT_LNURL_PAY_METADATA_PLAIN_TEXT)
|
||||
let metadata = "[[\"text/plain\",\"\(plainTextMetadata)\"]]"
|
||||
replyServer(encodable: LnurlInfoResponse(callback: request!.callback_url, maxSendable: maxSendableMsat, minSendable: minSendableMsat, metadata: metadata, tag: "payRequest"),
|
||||
replyURL: request!.reply_url)
|
||||
} catch let e {
|
||||
self.logger.log(tag: TAG, line: "failed to process lnurl: \(e)", level: "ERROR")
|
||||
fail(withError: e.localizedDescription, replyURL: request!.reply_url)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import UserNotifications
|
||||
import Foundation
|
||||
|
||||
struct LnurlInvoiceRequest: Codable {
|
||||
let reply_url: String
|
||||
let amount: UInt64
|
||||
}
|
||||
|
||||
struct LnurlInvoiceResponse: Decodable, Encodable {
|
||||
let pr: String
|
||||
let routes: [String]
|
||||
|
||||
init(pr: String, routes: [String]) {
|
||||
self.pr = pr
|
||||
self.routes = routes
|
||||
}
|
||||
}
|
||||
|
||||
class LnurlPayInvoiceTask : LnurlPayTask {
|
||||
fileprivate let TAG = "LnurlPayInvoiceTask"
|
||||
|
||||
init(payload: String, logger: ServiceLogger, contentHandler: ((UNNotificationContent) -> Void)? = nil, bestAttemptContent: UNMutableNotificationContent? = nil) {
|
||||
let successNotificationTitle = ResourceHelper.shared.getString(key: Constants.LNURL_PAY_INVOICE_NOTIFICATION_TITLE, fallback: Constants.DEFAULT_LNURL_PAY_INVOICE_NOTIFICATION_TITLE)
|
||||
let failNotificationTitle = ResourceHelper.shared.getString(key: Constants.LNURL_PAY_NOTIFICATION_FAILURE_TITLE, fallback: Constants.DEFAULT_LNURL_PAY_NOTIFICATION_FAILURE_TITLE)
|
||||
super.init(payload: payload, logger: logger, contentHandler: contentHandler, bestAttemptContent: bestAttemptContent, successNotificationTitle: successNotificationTitle, failNotificationTitle: failNotificationTitle)
|
||||
}
|
||||
|
||||
override func start(liquidSDK: BindingLiquidSdk) throws {
|
||||
var request: LnurlInvoiceRequest? = nil
|
||||
do {
|
||||
request = try JSONDecoder().decode(LnurlInvoiceRequest.self, from: self.payload.data(using: .utf8)!)
|
||||
} catch let e {
|
||||
self.logger.log(tag: TAG, line: "failed to decode payload: \(e)", level: "ERROR")
|
||||
self.displayPushNotification(title: self.failNotificationTitle, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_LNURL_PAY)
|
||||
throw e
|
||||
}
|
||||
|
||||
do {
|
||||
// Get the lightning limits
|
||||
let limits = try liquidSDK.fetchLightningLimits()
|
||||
// Check amount is within limits
|
||||
let amountSat = request!.amount / UInt64(1000)
|
||||
if amountSat < limits.receive.minSat || amountSat > limits.receive.maxSat {
|
||||
throw InvalidLnurlPayError.amount(amount: request!.amount)
|
||||
}
|
||||
let plainTextMetadata = ResourceHelper.shared.getString(key: Constants.LNURL_PAY_METADATA_PLAIN_TEXT, fallback: Constants.DEFAULT_LNURL_PAY_METADATA_PLAIN_TEXT)
|
||||
let metadata = "[[\"text/plain\",\"\(plainTextMetadata)\"]]"
|
||||
let prepareReceivePaymentRes = try liquidSDK.prepareReceivePayment(req: PrepareReceiveRequest(payerAmountSat: amountSat, paymentMethod: PaymentMethod.lightning))
|
||||
let receivePaymentRes = try liquidSDK.receivePayment(req: ReceivePaymentRequest(prepareResponse: prepareReceivePaymentRes, description: metadata, useDescriptionHash: true))
|
||||
self.replyServer(encodable: LnurlInvoiceResponse(pr: receivePaymentRes.destination, routes: []), replyURL: request!.reply_url)
|
||||
} catch let e {
|
||||
self.logger.log(tag: TAG, line: "failed to process lnurl: \(e)", level: "ERROR")
|
||||
self.fail(withError: e.localizedDescription, replyURL: request!.reply_url)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import UserNotifications
|
||||
import Foundation
|
||||
|
||||
struct SwapUpdatedRequest: Codable {
|
||||
let id: String
|
||||
let status: String
|
||||
}
|
||||
|
||||
class SwapUpdatedTask : TaskProtocol {
|
||||
fileprivate let TAG = "SwapUpdatedTask"
|
||||
|
||||
internal var payload: String
|
||||
internal var contentHandler: ((UNNotificationContent) -> Void)?
|
||||
internal var bestAttemptContent: UNMutableNotificationContent?
|
||||
internal var logger: ServiceLogger
|
||||
internal var request: SwapUpdatedRequest? = nil
|
||||
|
||||
init(payload: String, logger: ServiceLogger, contentHandler: ((UNNotificationContent) -> Void)? = nil, bestAttemptContent: UNMutableNotificationContent? = nil) {
|
||||
self.payload = payload
|
||||
self.contentHandler = contentHandler
|
||||
self.bestAttemptContent = bestAttemptContent
|
||||
self.logger = logger
|
||||
}
|
||||
|
||||
func start(liquidSDK: BindingLiquidSdk) throws {
|
||||
do {
|
||||
self.request = try JSONDecoder().decode(SwapUpdatedRequest.self, from: self.payload.data(using: .utf8)!)
|
||||
} catch let e {
|
||||
self.logger.log(tag: TAG, line: "Failed to decode payload: \(e)", level: "ERROR")
|
||||
self.onShutdown()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
public func onEvent(e: SdkEvent) {
|
||||
if let swapIdHash = self.request?.id {
|
||||
switch e {
|
||||
case .paymentSucceeded(details: let payment):
|
||||
let swapId = self.getSwapId(details: payment.details)
|
||||
if swapIdHash == swapId?.sha256() {
|
||||
self.logger.log(tag: TAG, line: "Received payment succeeded event: \(swapIdHash)", level: "INFO")
|
||||
self.notifySuccess(payment: payment)
|
||||
}
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getSwapId(details: PaymentDetails?) -> String? {
|
||||
if let details = details {
|
||||
switch details {
|
||||
case let .bitcoin(swapId, _, _, _):
|
||||
return swapId
|
||||
case let .lightning(swapId, _, _, _, _, _):
|
||||
return swapId
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func onShutdown() {
|
||||
let notificationTitle = ResourceHelper.shared.getString(key: Constants.SWAP_CONFIRMED_NOTIFICATION_FAILURE_TITLE, fallback: Constants.DEFAULT_SWAP_CONFIRMED_NOTIFICATION_FAILURE_TITLE)
|
||||
let notificationBody = ResourceHelper.shared.getString(key: Constants.SWAP_CONFIRMED_NOTIFICATION_FAILURE_TEXT, fallback: Constants.DEFAULT_SWAP_CONFIRMED_NOTIFICATION_FAILURE_TEXT)
|
||||
self.displayPushNotification(title: notificationTitle, body: notificationBody, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_SWAP_UPDATED)
|
||||
}
|
||||
|
||||
func notifySuccess(payment: Payment) {
|
||||
self.logger.log(tag: TAG, line: "Payment \(payment.txId ?? "") completed successfully", level: "INFO")
|
||||
let received = payment.paymentType == PaymentType.receive
|
||||
let notificationTitle = ResourceHelper.shared.getString(
|
||||
key: received ? Constants.PAYMENT_RECEIVED_NOTIFICATION_TITLE : Constants.PAYMENT_SENT_NOTIFICATION_TITLE,
|
||||
validateContains: "%d",
|
||||
fallback: received ? Constants.DEFAULT_PAYMENT_RECEIVED_NOTIFICATION_TITLE: Constants.DEFAULT_PAYMENT_SENT_NOTIFICATION_TITLE)
|
||||
self.displayPushNotification(title: String(format: notificationTitle, payment.amountSat), logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_SWAP_UPDATED)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import UserNotifications
|
||||
|
||||
public protocol TaskProtocol : EventListener {
|
||||
var payload: String { get set }
|
||||
var contentHandler: ((UNNotificationContent) -> Void)? { get set }
|
||||
var bestAttemptContent: UNMutableNotificationContent? { get set }
|
||||
|
||||
func start(liquidSDK: BindingLiquidSdk) throws
|
||||
func onShutdown()
|
||||
}
|
||||
|
||||
extension TaskProtocol {
|
||||
func displayPushNotification(title: String, body: String? = nil, logger: ServiceLogger, threadIdentifier: String? = nil) {
|
||||
logger.log(tag: "TaskProtocol", line:"displayPushNotification \(title)", level: "INFO")
|
||||
guard
|
||||
let contentHandler = contentHandler,
|
||||
let bestAttemptContent = bestAttemptContent
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
if let body = body {
|
||||
bestAttemptContent.body = body
|
||||
}
|
||||
|
||||
if let threadIdentifier = threadIdentifier {
|
||||
bestAttemptContent.threadIdentifier = threadIdentifier
|
||||
}
|
||||
|
||||
bestAttemptContent.title = title
|
||||
contentHandler(bestAttemptContent)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user