Add LNURL-verify notification plugin tasks (#911)

* Add LNURL-verify notification plugin tasks

* Only settled when MRH payment is complete

* Ignore unknown JSON keys

* Log fail replyURL

* Prevent escaping slashes
This commit is contained in:
Ross Savage
2025-05-15 06:26:32 +02:00
committed by GitHub
parent f8275cbd17
commit 42c7bfe285
14 changed files with 279 additions and 18 deletions

View File

@@ -26,6 +26,7 @@ object Constants {
const val MESSAGE_TYPE_INVOICE_REQUEST = "invoice_request" const val MESSAGE_TYPE_INVOICE_REQUEST = "invoice_request"
const val MESSAGE_TYPE_LNURL_PAY_INFO = "lnurlpay_info" const val MESSAGE_TYPE_LNURL_PAY_INFO = "lnurlpay_info"
const val MESSAGE_TYPE_LNURL_PAY_INVOICE = "lnurlpay_invoice" const val MESSAGE_TYPE_LNURL_PAY_INVOICE = "lnurlpay_invoice"
const val MESSAGE_TYPE_LNURL_PAY_VERIFY = "lnurlpay_verify"
const val MESSAGE_TYPE_SWAP_UPDATED = "swap_updated" const val MESSAGE_TYPE_SWAP_UPDATED = "swap_updated"
// Resource Identifiers // Resource Identifiers
@@ -51,6 +52,10 @@ object Constants {
"lnurl_pay_info_notification_title" "lnurl_pay_info_notification_title"
const val LNURL_PAY_INVOICE_NOTIFICATION_TITLE = const val LNURL_PAY_INVOICE_NOTIFICATION_TITLE =
"lnurl_pay_invoice_notification_title" "lnurl_pay_invoice_notification_title"
const val LNURL_PAY_VERIFY_NOTIFICATION_TITLE =
"lnurl_pay_verify_notification_title"
const val LNURL_PAY_VERIFY_NOTIFICATION_FAILURE_TITLE =
"lnurl_pay_verify_notification_failure_title"
const val LNURL_PAY_METADATA_PLAIN_TEXT = const val LNURL_PAY_METADATA_PLAIN_TEXT =
"lnurl_pay_metadata_plain_text" "lnurl_pay_metadata_plain_text"
const val LNURL_PAY_NOTIFICATION_FAILURE_TITLE = const val LNURL_PAY_NOTIFICATION_FAILURE_TITLE =
@@ -104,6 +109,10 @@ object Constants {
"Invoice Request Failed" "Invoice Request Failed"
const val DEFAULT_LNURL_PAY_INVOICE_NOTIFICATION_TITLE = const val DEFAULT_LNURL_PAY_INVOICE_NOTIFICATION_TITLE =
"Fetching Invoice" "Fetching Invoice"
const val DEFAULT_LNURL_PAY_VERIFY_NOTIFICATION_TITLE =
"Verifying Payment"
const val DEFAULT_LNURL_PAY_VERIFY_NOTIFICATION_FAILURE_TITLE =
"Payment Verification Failed"
const val DEFAULT_LNURL_PAY_METADATA_PLAIN_TEXT = const val DEFAULT_LNURL_PAY_METADATA_PLAIN_TEXT =
"Pay with LNURL" "Pay with LNURL"
const val DEFAULT_LNURL_PAY_NOTIFICATION_FAILURE_TITLE = const val DEFAULT_LNURL_PAY_NOTIFICATION_FAILURE_TITLE =

View File

@@ -14,6 +14,7 @@ import breez_sdk_liquid_notification.BreezSdkLiquidConnector.Companion.connectSD
import breez_sdk_liquid_notification.Constants.MESSAGE_TYPE_INVOICE_REQUEST import breez_sdk_liquid_notification.Constants.MESSAGE_TYPE_INVOICE_REQUEST
import breez_sdk_liquid_notification.Constants.MESSAGE_TYPE_LNURL_PAY_INFO import breez_sdk_liquid_notification.Constants.MESSAGE_TYPE_LNURL_PAY_INFO
import breez_sdk_liquid_notification.Constants.MESSAGE_TYPE_LNURL_PAY_INVOICE import breez_sdk_liquid_notification.Constants.MESSAGE_TYPE_LNURL_PAY_INVOICE
import breez_sdk_liquid_notification.Constants.MESSAGE_TYPE_LNURL_PAY_VERIFY
import breez_sdk_liquid_notification.Constants.MESSAGE_TYPE_SWAP_UPDATED import breez_sdk_liquid_notification.Constants.MESSAGE_TYPE_SWAP_UPDATED
import breez_sdk_liquid_notification.Constants.NOTIFICATION_ID_FOREGROUND_SERVICE import breez_sdk_liquid_notification.Constants.NOTIFICATION_ID_FOREGROUND_SERVICE
import breez_sdk_liquid_notification.Constants.NOTIFICATION_ID_REPLACEABLE import breez_sdk_liquid_notification.Constants.NOTIFICATION_ID_REPLACEABLE
@@ -25,6 +26,7 @@ import breez_sdk_liquid_notification.job.Job
import breez_sdk_liquid_notification.job.InvoiceRequestJob import breez_sdk_liquid_notification.job.InvoiceRequestJob
import breez_sdk_liquid_notification.job.LnurlPayInfoJob import breez_sdk_liquid_notification.job.LnurlPayInfoJob
import breez_sdk_liquid_notification.job.LnurlPayInvoiceJob import breez_sdk_liquid_notification.job.LnurlPayInvoiceJob
import breez_sdk_liquid_notification.job.LnurlPayVerifyJob
import breez_sdk_liquid_notification.job.SwapUpdatedJob import breez_sdk_liquid_notification.job.SwapUpdatedJob
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -185,6 +187,14 @@ abstract class ForegroundService :
logger, logger,
) )
MESSAGE_TYPE_LNURL_PAY_VERIFY ->
LnurlPayVerifyJob(
applicationContext,
this,
payload,
logger,
)
else -> null else -> null
} }
} }

View File

@@ -49,7 +49,8 @@ class InvoiceRequestJob(
override fun start(liquidSDK: BindingLiquidSdk) { override fun start(liquidSDK: BindingLiquidSdk) {
var request: InvoiceRequestRequest? = null var request: InvoiceRequestRequest? = null
try { try {
request = Json.decodeFromString(InvoiceRequestRequest.serializer(), payload) val decoder = Json { ignoreUnknownKeys = true }
request = decoder.decodeFromString(InvoiceRequestRequest.serializer(), payload)
val createBolt12InvoiceResponse = val createBolt12InvoiceResponse =
liquidSDK.createBolt12Invoice( liquidSDK.createBolt12Invoice(
CreateBolt12InvoiceRequest(request.offer, request.invoiceRequest), CreateBolt12InvoiceRequest(request.offer, request.invoiceRequest),

View File

@@ -1,6 +1,7 @@
package breez_sdk_liquid_notification.job package breez_sdk_liquid_notification.job
import breez_sdk_liquid.SdkEvent import breez_sdk_liquid.SdkEvent
import breez_sdk_liquid_notification.ServiceLogger
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
@@ -12,18 +13,25 @@ import java.net.URL
@Serializable @Serializable
data class LnurlErrorResponse( data class LnurlErrorResponse(
@SerialName("status") val status: String, @SerialName("status") val status: String,
@SerialName("reason") val reason: String?, @SerialName("reason") val reason: String? = null,
) )
interface LnurlPayJob : Job { interface LnurlPayJob : Job {
companion object {
private const val TAG = "LnurlPayJob"
}
override fun onEvent(e: SdkEvent) {} override fun onEvent(e: SdkEvent) {}
fun fail( fun fail(
withError: String?, withError: String?,
replyURL: String, replyURL: String,
) { logger: ServiceLogger,
): Boolean {
val url = URL(replyURL) val url = URL(replyURL)
val response = Json.encodeToString(LnurlErrorResponse("ERROR", withError)).toByteArray() val payload = Json.encodeToString(LnurlErrorResponse("ERROR", withError))
logger.log(TAG, "Send to: $replyURL, fail response: $payload", "WARN")
val response = payload.toByteArray()
with(url.openConnection() as HttpURLConnection) { with(url.openConnection() as HttpURLConnection) {
requestMethod = "POST" requestMethod = "POST"
@@ -32,6 +40,8 @@ interface LnurlPayJob : Job {
setRequestProperty("Content-Type", "application/json") setRequestProperty("Content-Type", "application/json")
setRequestProperty("Content-Length", response.size.toString()) setRequestProperty("Content-Length", response.size.toString())
DataOutputStream(outputStream).use { it.write(response, 0, response.size) } DataOutputStream(outputStream).use { it.write(response, 0, response.size) }
return responseCode == 200
} }
} }
} }

View File

@@ -48,7 +48,8 @@ class LnurlPayInfoJob(
override fun start(liquidSDK: BindingLiquidSdk) { override fun start(liquidSDK: BindingLiquidSdk) {
var request: LnurlInfoRequest? = null var request: LnurlInfoRequest? = null
try { try {
request = Json.decodeFromString(LnurlInfoRequest.serializer(), payload) val decoder = Json { ignoreUnknownKeys = true }
request = decoder.decodeFromString(LnurlInfoRequest.serializer(), payload)
// Get the lightning limits // Get the lightning limits
val limits = liquidSDK.fetchLightningLimits() val limits = liquidSDK.fetchLightningLimits()
// Max millisatoshi amount LN SERVICE is willing to receive // Max millisatoshi amount LN SERVICE is willing to receive
@@ -86,7 +87,7 @@ class LnurlPayInfoJob(
} catch (e: Exception) { } catch (e: Exception) {
logger.log(TAG, "Failed to process lnurl: ${e.message}", "WARN") logger.log(TAG, "Failed to process lnurl: ${e.message}", "WARN")
if (request != null) { if (request != null) {
fail(e.message, request.replyURL) fail(e.message, request.replyURL, logger)
} }
notifyChannel( notifyChannel(
context, context,

View File

@@ -2,6 +2,7 @@ package breez_sdk_liquid_notification.job
import android.content.Context import android.content.Context
import breez_sdk_liquid.BindingLiquidSdk import breez_sdk_liquid.BindingLiquidSdk
import breez_sdk_liquid.InputType
import breez_sdk_liquid.PaymentMethod import breez_sdk_liquid.PaymentMethod
import breez_sdk_liquid.PrepareReceiveRequest import breez_sdk_liquid.PrepareReceiveRequest
import breez_sdk_liquid.ReceiveAmount import breez_sdk_liquid.ReceiveAmount
@@ -26,14 +27,17 @@ import kotlinx.serialization.json.Json
data class LnurlInvoiceRequest( data class LnurlInvoiceRequest(
@SerialName("amount") val amount: ULong, @SerialName("amount") val amount: ULong,
@SerialName("reply_url") val replyURL: String, @SerialName("reply_url") val replyURL: String,
@SerialName("verify_url") val verifyURL: String? = null,
) )
// Serialize the response according to to LUD-06 payRequest base specification: // Serialize the response according to:
// https://github.com/lnurl/luds/blob/luds/06.md // - LUD-06: https://github.com/lnurl/luds/blob/luds/06.md
// - LUD-21: https://github.com/lnurl/luds/blob/luds/21.md
@Serializable @Serializable
data class LnurlPayInvoiceResponse( data class LnurlPayInvoiceResponse(
val pr: String, val pr: String,
val routes: List<String>, val routes: List<String>,
val verify: String?,
) )
class LnurlPayInvoiceJob( class LnurlPayInvoiceJob(
@@ -49,7 +53,8 @@ class LnurlPayInvoiceJob(
override fun start(liquidSDK: BindingLiquidSdk) { override fun start(liquidSDK: BindingLiquidSdk) {
var request: LnurlInvoiceRequest? = null var request: LnurlInvoiceRequest? = null
try { try {
request = Json.decodeFromString(LnurlInvoiceRequest.serializer(), payload) val decoder = Json { ignoreUnknownKeys = true }
request = decoder.decodeFromString(LnurlInvoiceRequest.serializer(), payload)
// Get the lightning limits // Get the lightning limits
val limits = liquidSDK.fetchLightningLimits() val limits = liquidSDK.fetchLightningLimits()
// Check amount is within limits // Check amount is within limits
@@ -75,10 +80,23 @@ class LnurlPayInvoiceJob(
useDescriptionHash = true, useDescriptionHash = true,
), ),
) )
// Add the verify URL
var verify: String? = null
if (request.verifyURL != null) {
try {
val inputType = liquidSDK.parse(receivePaymentResponse.destination)
if (inputType is InputType.Bolt11) {
verify = request.verifyURL?.replace("{payment_hash}", inputType.invoice.paymentHash)
}
} catch (e: Exception) {
logger.log(TAG, "Failed to parse destination: ${e.message}", "WARN")
}
}
val response = val response =
LnurlPayInvoiceResponse( LnurlPayInvoiceResponse(
receivePaymentResponse.destination, receivePaymentResponse.destination,
listOf(), listOf(),
verify,
) )
val success = replyServer(Json.encodeToString(response), request.replyURL) val success = replyServer(Json.encodeToString(response), request.replyURL)
notifyChannel( notifyChannel(
@@ -93,7 +111,7 @@ class LnurlPayInvoiceJob(
} catch (e: Exception) { } catch (e: Exception) {
logger.log(TAG, "Failed to process lnurl: ${e.message}", "WARN") logger.log(TAG, "Failed to process lnurl: ${e.message}", "WARN")
if (request != null) { if (request != null) {
fail(e.message, request.replyURL) fail(e.message, request.replyURL, logger)
} }
notifyChannel( notifyChannel(
context, context,

View File

@@ -0,0 +1,108 @@
package breez_sdk_liquid_notification.job
import android.content.Context
import breez_sdk_liquid.BindingLiquidSdk
import breez_sdk_liquid.GetPaymentRequest
import breez_sdk_liquid.PaymentDetails
import breez_sdk_liquid.PaymentState
import breez_sdk_liquid_notification.Constants.DEFAULT_LNURL_PAY_VERIFY_NOTIFICATION_TITLE
import breez_sdk_liquid_notification.Constants.DEFAULT_LNURL_PAY_VERIFY_NOTIFICATION_FAILURE_TITLE
import breez_sdk_liquid_notification.Constants.LNURL_PAY_VERIFY_NOTIFICATION_TITLE
import breez_sdk_liquid_notification.Constants.LNURL_PAY_VERIFY_NOTIFICATION_FAILURE_TITLE
import breez_sdk_liquid_notification.Constants.NOTIFICATION_CHANNEL_REPLACEABLE
import breez_sdk_liquid_notification.NotificationHelper.Companion.notifyChannel
import breez_sdk_liquid_notification.ResourceHelper.Companion.getString
import breez_sdk_liquid_notification.SdkForegroundService
import breez_sdk_liquid_notification.ServiceLogger
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@Serializable
data class LnurlVerifyRequest(
@SerialName("payment_hash") val paymentHash: String,
@SerialName("reply_url") val replyURL: String,
)
// Serialize the response according to to LUD-21 verify specification:
// https://github.com/lnurl/luds/blob/luds/21.md
@Serializable
data class LnurlPayVerifyResponse(
val settled: Boolean,
val preimage: String?,
val pr: String,
)
class LnurlPayVerifyJob(
private val context: Context,
private val fgService: SdkForegroundService,
private val payload: String,
private val logger: ServiceLogger,
) : LnurlPayJob {
companion object {
private const val TAG = "LnurlPayVerifyJob"
}
override fun start(liquidSDK: BindingLiquidSdk) {
var request: LnurlVerifyRequest? = null
try {
val decoder = Json { ignoreUnknownKeys = true }
request = decoder.decodeFromString(LnurlVerifyRequest.serializer(), payload)
// Get the payment by payment hash
val getPaymentReq = GetPaymentRequest.PaymentHash(request.paymentHash)
val payment = liquidSDK.getPayment(getPaymentReq) ?: run {
throw InvalidLnurlPayException("Not found")
}
val response = when (val details = payment.details) {
is PaymentDetails.Lightning -> {
// In the case of a Lightning payment, if it's paid via Lightning or MRH,
// we can release the preimage
val settled = when (payment.status) {
// If the payment is pending, we need to check if it's paid via Lightning or MRH
PaymentState.PENDING -> details.claimTxId != null
PaymentState.COMPLETE -> true
else -> false
}
LnurlPayVerifyResponse(
settled,
if (settled) details.preimage else null,
details.invoice!!,
)
}
else -> null
} ?: run {
throw InvalidLnurlPayException("Not found")
}
val success = replyServer(Json.encodeToString(response), request.replyURL)
notifyChannel(
context,
NOTIFICATION_CHANNEL_REPLACEABLE,
getString(
context,
if (success) LNURL_PAY_VERIFY_NOTIFICATION_TITLE else LNURL_PAY_VERIFY_NOTIFICATION_FAILURE_TITLE,
if (success) DEFAULT_LNURL_PAY_VERIFY_NOTIFICATION_TITLE else DEFAULT_LNURL_PAY_VERIFY_NOTIFICATION_FAILURE_TITLE,
),
)
} catch (e: Exception) {
logger.log(TAG, "Failed to process lnurl verify: ${e.message}", "WARN")
if (request != null) {
fail(e.message, request.replyURL, logger)
}
notifyChannel(
context,
NOTIFICATION_CHANNEL_REPLACEABLE,
getString(
context,
LNURL_PAY_VERIFY_NOTIFICATION_FAILURE_TITLE,
DEFAULT_LNURL_PAY_VERIFY_NOTIFICATION_FAILURE_TITLE,
),
)
}
fgService.onFinished(this)
}
override fun onShutdown() {}
}

View File

@@ -62,7 +62,8 @@ class SwapUpdatedJob(
override fun start(liquidSDK: BindingLiquidSdk) { override fun start(liquidSDK: BindingLiquidSdk) {
try { try {
val request = Json.decodeFromString(SwapUpdatedRequest.serializer(), payload) val decoder = Json { ignoreUnknownKeys = true }
val request = decoder.decodeFromString(SwapUpdatedRequest.serializer(), payload)
this.swapIdHash = request.id this.swapIdHash = request.id
startPolling(liquidSDK) startPolling(liquidSDK)
} catch (e: Exception) { } catch (e: Exception) {

View File

@@ -13,14 +13,17 @@ struct Constants {
static let MESSAGE_TYPE_SWAP_UPDATED = "swap_updated" static let MESSAGE_TYPE_SWAP_UPDATED = "swap_updated"
static let MESSAGE_TYPE_LNURL_PAY_INFO = "lnurlpay_info" static let MESSAGE_TYPE_LNURL_PAY_INFO = "lnurlpay_info"
static let MESSAGE_TYPE_LNURL_PAY_INVOICE = "lnurlpay_invoice" static let MESSAGE_TYPE_LNURL_PAY_INVOICE = "lnurlpay_invoice"
static let MESSAGE_TYPE_LNURL_PAY_VERIFY = "lnurlpay_verify"
// Resource Identifiers // Resource Identifiers
static let INVOICE_REQUEST_NOTIFICATION_TITLE = "invoice_request_notification_title" static let INVOICE_REQUEST_NOTIFICATION_TITLE = "invoice_request_notification_title"
static let INVOICE_REQUEST_NOTIFICATION_FAILURE_TITLE = "invoice_request_notification_failure_title" static let INVOICE_REQUEST_NOTIFICATION_FAILURE_TITLE = "invoice_request_notification_failure_title"
static let LNURL_PAY_INFO_NOTIFICATION_TITLE = "lnurl_pay_info_notification_title" 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_INVOICE_NOTIFICATION_TITLE = "lnurl_pay_invoice_notification_title"
static let LNURL_PAY_VERIFY_NOTIFICATION_TITLE = "lnurl_pay_verify_notification_title"
static let LNURL_PAY_METADATA_PLAIN_TEXT = "lnurl_pay_metadata_plain_text" 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 LNURL_PAY_NOTIFICATION_FAILURE_TITLE = "lnurl_pay_notification_failure_title"
static let LNURL_PAY_VERIFY_NOTIFICATION_FAILURE_TITLE = "lnurl_pay_notification_failure_title"
static let PAYMENT_RECEIVED_NOTIFICATION_TITLE = "payment_received_notification_title" static let PAYMENT_RECEIVED_NOTIFICATION_TITLE = "payment_received_notification_title"
static let PAYMENT_SENT_NOTIFICATION_TITLE = "payment_sent_notification_title" static let PAYMENT_SENT_NOTIFICATION_TITLE = "payment_sent_notification_title"
static let PAYMENT_WAITING_FEE_ACCEPTANCE_TITLE = "payment_waiting_fee_acceptance_notification_title" static let PAYMENT_WAITING_FEE_ACCEPTANCE_TITLE = "payment_waiting_fee_acceptance_notification_title"
@@ -33,8 +36,10 @@ struct Constants {
static let DEFAULT_INVOICE_REQUEST_NOTIFICATION_FAILURE_TITLE = "Invoice Request Failed" static let DEFAULT_INVOICE_REQUEST_NOTIFICATION_FAILURE_TITLE = "Invoice Request Failed"
static let DEFAULT_LNURL_PAY_INFO_NOTIFICATION_TITLE = "Retrieving Payment Information" 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_INVOICE_NOTIFICATION_TITLE = "Fetching Invoice"
static let DEFAULT_LNURL_PAY_VERIFY_NOTIFICATION_TITLE = "Verifying Payment"
static let DEFAULT_LNURL_PAY_METADATA_PLAIN_TEXT = "Pay with LNURL" 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_LNURL_PAY_NOTIFICATION_FAILURE_TITLE = "Receive Payment Failed"
static let DEFAULT_LNURL_PAY_VERIFY_NOTIFICATION_FAILURE_TITLE = "Payment Verification Failed"
static let DEFAULT_PAYMENT_RECEIVED_NOTIFICATION_TITLE = "Received %d sats" static let DEFAULT_PAYMENT_RECEIVED_NOTIFICATION_TITLE = "Received %d sats"
static let DEFAULT_PAYMENT_SENT_NOTIFICATION_TITLE = "Sent %d sats" static let DEFAULT_PAYMENT_SENT_NOTIFICATION_TITLE = "Sent %d sats"
static let DEFAULT_PAYMENT_WAITING_FEE_ACCEPTANCE_TITLE = "Payment requires fee acceptance" static let DEFAULT_PAYMENT_WAITING_FEE_ACCEPTANCE_TITLE = "Payment requires fee acceptance"

View File

@@ -80,6 +80,8 @@ open class SDKNotificationService: UNNotificationServiceExtension {
return LnurlPayInfoTask(payload: payload, logger: self.logger, contentHandler: contentHandler, bestAttemptContent: bestAttemptContent) return LnurlPayInfoTask(payload: payload, logger: self.logger, contentHandler: contentHandler, bestAttemptContent: bestAttemptContent)
case Constants.MESSAGE_TYPE_LNURL_PAY_INVOICE: case Constants.MESSAGE_TYPE_LNURL_PAY_INVOICE:
return LnurlPayInvoiceTask(payload: payload, logger: self.logger, contentHandler: contentHandler, bestAttemptContent: bestAttemptContent) return LnurlPayInvoiceTask(payload: payload, logger: self.logger, contentHandler: contentHandler, bestAttemptContent: bestAttemptContent)
case Constants.MESSAGE_TYPE_LNURL_PAY_VERIFY:
return LnurlPayVerifyTask(payload: payload, logger: self.logger, contentHandler: contentHandler, bestAttemptContent: bestAttemptContent)
default: default:
return nil return nil
} }

View File

@@ -28,17 +28,20 @@ class LnurlPayTask : ReplyableTask {
} }
enum InvalidLnurlPayError: Error { enum InvalidLnurlPayError: Error {
case minSendable
case amount(amount: UInt64) case amount(amount: UInt64)
case minSendable
case notFound
} }
extension InvalidLnurlPayError: LocalizedError { extension InvalidLnurlPayError: LocalizedError {
public var errorDescription: String? { public var errorDescription: String? {
switch self { switch self {
case .minSendable:
return NSLocalizedString("Minimum sendable amount is invalid", comment: "InvalidLnurlPayError")
case .amount(amount: let amount): case .amount(amount: let amount):
return NSLocalizedString("Invalid amount requested \(amount)", comment: "InvalidLnurlPayError") return NSLocalizedString("Invalid amount requested \(amount)", comment: "InvalidLnurlPayError")
case .minSendable:
return NSLocalizedString("Minimum sendable amount is invalid", comment: "InvalidLnurlPayError")
case .notFound:
return NSLocalizedString("Not found", comment: "InvalidLnurlPayError")
} }
} }
} }

View File

@@ -2,17 +2,23 @@ import UserNotifications
import Foundation import Foundation
struct LnurlInvoiceRequest: Codable { struct LnurlInvoiceRequest: Codable {
let reply_url: String
let amount: UInt64 let amount: UInt64
let reply_url: String
let verify_url: String?
} }
// Serialize the response according to:
// - LUD-06: https://github.com/lnurl/luds/blob/luds/06.md
// - LUD-21: https://github.com/lnurl/luds/blob/luds/21.md
struct LnurlInvoiceResponse: Decodable, Encodable { struct LnurlInvoiceResponse: Decodable, Encodable {
let pr: String let pr: String
let routes: [String] let routes: [String]
let verify: String?
init(pr: String, routes: [String]) { init(pr: String, routes: [String], verify: String?) {
self.pr = pr self.pr = pr
self.routes = routes self.routes = routes
self.verify = verify
} }
} }
@@ -48,7 +54,19 @@ class LnurlPayInvoiceTask : LnurlPayTask {
let amount = ReceiveAmount.bitcoin(payerAmountSat: amountSat) let amount = ReceiveAmount.bitcoin(payerAmountSat: amountSat)
let prepareReceivePaymentRes = try liquidSDK.prepareReceivePayment(req: PrepareReceiveRequest(paymentMethod: PaymentMethod.lightning, amount: amount)) let prepareReceivePaymentRes = try liquidSDK.prepareReceivePayment(req: PrepareReceiveRequest(paymentMethod: PaymentMethod.lightning, amount: amount))
let receivePaymentRes = try liquidSDK.receivePayment(req: ReceivePaymentRequest(prepareResponse: prepareReceivePaymentRes, description: metadata, useDescriptionHash: true)) 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) // Add the verify URL
var verify: String?
if let verifyUrl = request!.verify_url {
do {
let inputType = try liquidSDK.parse(input: receivePaymentRes.destination)
if case .bolt11(let invoice) = inputType {
verify = verifyUrl.replacingOccurrences(of: "{payment_hash}", with: invoice.paymentHash)
}
} catch let e {
self.logger.log(tag: TAG, line: "Failed to parse destination: \(e)", level: "ERROR")
}
}
self.replyServer(encodable: LnurlInvoiceResponse(pr: receivePaymentRes.destination, routes: [], verify: verify), replyURL: request!.reply_url)
} catch let e { } catch let e {
self.logger.log(tag: TAG, line: "failed to process lnurl: \(e)", level: "ERROR") self.logger.log(tag: TAG, line: "failed to process lnurl: \(e)", level: "ERROR")
self.fail(withError: e.localizedDescription, replyURL: request!.reply_url) self.fail(withError: e.localizedDescription, replyURL: request!.reply_url)

View File

@@ -0,0 +1,73 @@
import UserNotifications
import Foundation
struct LnurlVerifyRequest: Codable {
let payment_hash: String
let reply_url: String
}
struct LnurlVerifyResponse: Decodable, Encodable {
let settled: Bool
let preimage: String?
let pr: String
init(settled: Bool, preimage: String?, pr: String) {
self.settled = settled
self.preimage = preimage
self.pr = pr
}
}
class LnurlPayVerifyTask : LnurlPayTask {
fileprivate let TAG = "LnurlPayVerifyTask"
init(payload: String, logger: ServiceLogger, contentHandler: ((UNNotificationContent) -> Void)? = nil, bestAttemptContent: UNMutableNotificationContent? = nil) {
let successNotificationTitle = ResourceHelper.shared.getString(key: Constants.LNURL_PAY_VERIFY_NOTIFICATION_TITLE, fallback: Constants.DEFAULT_LNURL_PAY_VERIFY_NOTIFICATION_TITLE)
let failNotificationTitle = ResourceHelper.shared.getString(key: Constants.LNURL_PAY_VERIFY_NOTIFICATION_FAILURE_TITLE, fallback: Constants.DEFAULT_LNURL_PAY_VERIFY_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: LnurlVerifyRequest? = nil
do {
request = try JSONDecoder().decode(LnurlVerifyRequest.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_REPLACEABLE)
throw e
}
do {
// Get the payment by payment hash
let getPaymentReq = GetPaymentRequest.paymentHash(paymentHash: request!.payment_hash)
guard let payment = try liquidSDK.getPayment(req: getPaymentReq) else {
throw InvalidLnurlPayError.notFound
}
var response: LnurlVerifyResponse? = nil
switch payment.details {
case let .lightning(_, _, _, preimage, invoice, _, _, _, _, _, claimTxId, _, _):
// In the case of a Lightning payment, if it's paid via Lightning or MRH,
// we can release the preimage
let settled = switch payment.status {
case .pending:
// If the payment is pending, we need to check if it's paid via Lightning or MRH
claimTxId != nil
case .complete:
true
default:
false
}
response = LnurlVerifyResponse(settled: settled, preimage: settled ? preimage : nil, pr: invoice!)
default:
break
}
if response == nil {
throw InvalidLnurlPayError.notFound
}
replyServer(encodable: response, replyURL: request!.reply_url)
} catch let e {
self.logger.log(tag: TAG, line: "failed to process lnurl verify: \(e)", level: "ERROR")
fail(withError: e.localizedDescription, replyURL: request!.reply_url)
}
}
}

View File

@@ -92,7 +92,9 @@ class ReplyableTask : TaskProtocol {
} }
var request = URLRequest(url: serverReplyURL) var request = URLRequest(url: serverReplyURL)
request.httpMethod = "POST" request.httpMethod = "POST"
request.httpBody = try! JSONEncoder().encode(encodable) let encoder = JSONEncoder()
encoder.outputFormatting = .withoutEscapingSlashes
request.httpBody = try! encoder.encode(encodable)
let task = URLSession.shared.dataTask(with: request) { data, response, error in let task = URLSession.shared.dataTask(with: request) { data, response, error in
let statusCode = (response as! HTTPURLResponse).statusCode let statusCode = (response as! HTTPURLResponse).statusCode