mirror of
https://github.com/aljazceru/breez-sdk-liquid.git
synced 2026-01-06 23:54:26 +01:00
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:
@@ -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 =
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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() {}
|
||||||
|
}
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user