kopia lustrzana https://github.com/vitorpamplona/amethyst
287 wiersze
12 KiB
Kotlin
287 wiersze
12 KiB
Kotlin
/**
|
|
* Copyright (c) 2024 Vitor Pamplona
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
* this software and associated documentation files (the "Software"), to deal in
|
|
* the Software without restriction, including without limitation the rights to use,
|
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
|
* subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in all
|
|
* copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
*/
|
|
package com.vitorpamplona.amethyst.service.lnurl
|
|
|
|
import android.content.Context
|
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
|
import com.vitorpamplona.amethyst.BuildConfig
|
|
import com.vitorpamplona.amethyst.R
|
|
import com.vitorpamplona.amethyst.service.HttpClientManager
|
|
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
|
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
|
|
import com.vitorpamplona.quartz.encoders.Lud06
|
|
import okhttp3.Request
|
|
import java.math.BigDecimal
|
|
import java.math.RoundingMode
|
|
import java.net.URLEncoder
|
|
import kotlin.coroutines.cancellation.CancellationException
|
|
|
|
class LightningAddressResolver() {
|
|
val client = HttpClientManager.getHttpClient()
|
|
|
|
fun assembleUrl(lnaddress: String): String? {
|
|
val parts = lnaddress.split("@")
|
|
|
|
if (parts.size == 2) {
|
|
return "https://${parts[1]}/.well-known/lnurlp/${parts[0]}"
|
|
}
|
|
|
|
if (lnaddress.lowercase().startsWith("lnurl")) {
|
|
return Lud06().toLnUrlp(lnaddress)
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
private fun fetchLightningAddressJson(
|
|
lnaddress: String,
|
|
onSuccess: (String) -> Unit,
|
|
onError: (String, String) -> Unit,
|
|
context: Context,
|
|
) {
|
|
checkNotInMainThread()
|
|
|
|
val url = assembleUrl(lnaddress)
|
|
|
|
if (url == null) {
|
|
onError(
|
|
context.getString(R.string.error_unable_to_fetch_invoice),
|
|
context.getString(
|
|
R.string.could_not_assemble_lnurl_from_lightning_address_check_the_user_s_setup,
|
|
lnaddress,
|
|
),
|
|
)
|
|
return
|
|
}
|
|
|
|
try {
|
|
val request: Request =
|
|
Request.Builder()
|
|
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
|
.url(url)
|
|
.build()
|
|
|
|
client.newCall(request).execute().use {
|
|
if (it.isSuccessful) {
|
|
onSuccess(it.body.string())
|
|
} else {
|
|
onError(
|
|
context.getString(R.string.error_unable_to_fetch_invoice),
|
|
context.getString(
|
|
R.string
|
|
.the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct,
|
|
url,
|
|
lnaddress,
|
|
it.code.toString(),
|
|
),
|
|
)
|
|
}
|
|
}
|
|
} catch (e: Exception) {
|
|
if (e is CancellationException) throw e
|
|
e.printStackTrace()
|
|
onError(
|
|
context.getString(R.string.error_unable_to_fetch_invoice),
|
|
context.getString(
|
|
R.string
|
|
.could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct_exception,
|
|
url,
|
|
lnaddress,
|
|
e.suppressedExceptions.getOrNull(0)?.message ?: e.cause?.message ?: e.message,
|
|
),
|
|
)
|
|
}
|
|
}
|
|
|
|
fun fetchLightningInvoice(
|
|
lnCallback: String,
|
|
milliSats: Long,
|
|
message: String,
|
|
nostrRequest: String? = null,
|
|
onSuccess: (String) -> Unit,
|
|
onError: (String, String) -> Unit,
|
|
context: Context,
|
|
) {
|
|
checkNotInMainThread()
|
|
|
|
val encodedMessage = URLEncoder.encode(message, "utf-8")
|
|
|
|
val urlBinder = if (lnCallback.contains("?")) "&" else "?"
|
|
var url = "$lnCallback${urlBinder}amount=$milliSats&comment=$encodedMessage"
|
|
|
|
if (nostrRequest != null) {
|
|
val encodedNostrRequest = URLEncoder.encode(nostrRequest, "utf-8")
|
|
url += "&nostr=$encodedNostrRequest"
|
|
}
|
|
|
|
val request: Request =
|
|
Request.Builder()
|
|
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
|
.url(url)
|
|
.build()
|
|
|
|
client.newCall(request).execute().use {
|
|
if (it.isSuccessful) {
|
|
onSuccess(it.body.string())
|
|
} else {
|
|
onError(
|
|
context.getString(R.string.error_unable_to_fetch_invoice),
|
|
context.getString(R.string.could_not_fetch_invoice_from, lnCallback),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
fun lnAddressInvoice(
|
|
lnaddress: String,
|
|
milliSats: Long,
|
|
message: String,
|
|
nostrRequest: String? = null,
|
|
onSuccess: (String) -> Unit,
|
|
onError: (String, String) -> Unit,
|
|
onProgress: (percent: Float) -> Unit,
|
|
context: Context,
|
|
) {
|
|
val mapper = jacksonObjectMapper()
|
|
|
|
fetchLightningAddressJson(
|
|
lnaddress,
|
|
onSuccess = { lnAddressJson ->
|
|
onProgress(0.4f)
|
|
|
|
val lnurlp =
|
|
try {
|
|
mapper.readTree(lnAddressJson)
|
|
} catch (t: Throwable) {
|
|
if (t is CancellationException) throw t
|
|
onError(
|
|
context.getString(R.string.error_unable_to_fetch_invoice),
|
|
context.getString(
|
|
R.string.error_parsing_json_from_lightning_address_check_the_user_s_lightning_setup_with_user,
|
|
lnaddress,
|
|
),
|
|
)
|
|
null
|
|
}
|
|
|
|
val callback = lnurlp?.get("callback")?.asText()
|
|
|
|
if (callback == null) {
|
|
onError(
|
|
context.getString(R.string.error_unable_to_fetch_invoice),
|
|
context.getString(
|
|
R.string.callback_url_not_found_in_the_user_s_lightning_address_server_configuration_with_user,
|
|
lnaddress,
|
|
),
|
|
)
|
|
}
|
|
|
|
val allowsNostr = lnurlp?.get("allowsNostr")?.asBoolean() ?: false
|
|
|
|
callback?.let { cb ->
|
|
fetchLightningInvoice(
|
|
cb,
|
|
milliSats,
|
|
message,
|
|
if (allowsNostr) nostrRequest else null,
|
|
onSuccess = {
|
|
onProgress(0.6f)
|
|
|
|
val lnInvoice =
|
|
try {
|
|
mapper.readTree(it)
|
|
} catch (t: Throwable) {
|
|
if (t is CancellationException) throw t
|
|
onError(
|
|
context.getString(R.string.error_unable_to_fetch_invoice),
|
|
context.getString(
|
|
R.string
|
|
.error_parsing_json_from_lightning_address_s_invoice_fetch_check_the_user_s_lightning_setup_with_user,
|
|
lnaddress,
|
|
),
|
|
)
|
|
null
|
|
}
|
|
|
|
lnInvoice
|
|
?.get("pr")
|
|
?.asText()
|
|
?.ifBlank { null }
|
|
?.let { pr ->
|
|
// Forces LN Invoice amount to be the requested amount.
|
|
val expectedAmountInSats =
|
|
BigDecimal(milliSats).divide(BigDecimal(1000), RoundingMode.HALF_UP).toLong()
|
|
val invoiceAmount = LnInvoiceUtil.getAmountInSats(pr)
|
|
if (invoiceAmount.toLong() == expectedAmountInSats) {
|
|
onProgress(0.7f)
|
|
onSuccess(pr)
|
|
} else {
|
|
onProgress(0.0f)
|
|
onError(
|
|
context.getString(R.string.error_unable_to_fetch_invoice),
|
|
context.getString(
|
|
R.string.incorrect_invoice_amount_sats_from_it_should_have_been,
|
|
invoiceAmount.toLong().toString(),
|
|
lnaddress,
|
|
expectedAmountInSats.toString(),
|
|
),
|
|
)
|
|
}
|
|
}
|
|
?: lnInvoice
|
|
?.get("reason")
|
|
?.asText()
|
|
?.ifBlank { null }
|
|
?.let { reason ->
|
|
onProgress(0.0f)
|
|
onError(
|
|
context.getString(R.string.error_unable_to_fetch_invoice),
|
|
context.getString(
|
|
R.string
|
|
.unable_to_create_a_lightning_invoice_before_sending_the_zap_the_receiver_s_lightning_wallet_sent_the_following_error_with_user,
|
|
lnaddress,
|
|
reason,
|
|
),
|
|
)
|
|
}
|
|
?: run {
|
|
onProgress(0.0f)
|
|
onError(
|
|
context.getString(R.string.error_unable_to_fetch_invoice),
|
|
context.getString(
|
|
R.string
|
|
.unable_to_create_a_lightning_invoice_before_sending_the_zap_element_pr_not_found_in_the_resulting_json_with_user,
|
|
lnaddress,
|
|
),
|
|
)
|
|
}
|
|
},
|
|
onError = onError,
|
|
context,
|
|
)
|
|
}
|
|
},
|
|
onError = onError,
|
|
context,
|
|
)
|
|
}
|
|
}
|