From 7c5218baf3f7fa0c0defe09b6d8d35e5891d6428 Mon Sep 17 00:00:00 2001 From: Dominique Padiou <5765435+dpad85@users.noreply.github.com> Date: Fri, 19 Jul 2024 15:40:40 +0200 Subject: [PATCH] Fix uri parsing in Bip353 resolver We are now using our bip21 parser when reading the data field returned by the bip353 resolver, which lets us handle uris with parameters correctly. Also added new bip353 error types when relevant. --- .../phoenix/android/payments/ScanDataView.kt | 2 + .../android/settings/MutualCloseView.kt | 2 +- .../walletinfo/SwapInRefundViewModel.kt | 2 +- .../res/values-b+es+419/important_strings.xml | 2 + .../main/res/values-cs/important_strings.xml | 2 + .../main/res/values-de/important_strings.xml | 2 + .../main/res/values-es/important_strings.xml | 2 + .../main/res/values-fr/important_strings.xml | 2 + .../res/values-pt-rBR/important_strings.xml | 2 + .../main/res/values-sk/important_strings.xml | 2 + .../main/res/values-vi/important_strings.xml | 2 + .../src/main/res/values/important_strings.xml | 2 + .../CloseChannelsConfigurationController.kt | 2 +- .../controllers/payments/Scan.kt | 2 + .../controllers/payments/ScanController.kt | 38 +++---- .../kotlin/fr.acinq.phoenix/utils/Parser.kt | 10 +- .../acinq/phoenix/data/lnurl/LnurlPayTest.kt | 4 +- .../fr/acinq/phoenix/utils/ParserTest.kt | 102 ++++++++++++++---- 18 files changed, 128 insertions(+), 54 deletions(-) diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/ScanDataView.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/ScanDataView.kt index 239f552b0..704e62104 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/ScanDataView.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/ScanDataView.kt @@ -361,6 +361,8 @@ private fun ScanErrorView( is Scan.BadRequestReason.InvalidLnurl -> stringResource(R.string.scan_error_lnurl_invalid) is Scan.BadRequestReason.UnsupportedLnurl -> stringResource(R.string.scan_error_lnurl_unsupported) is Scan.BadRequestReason.UnknownFormat -> stringResource(R.string.scan_error_invalid_generic) + is Scan.BadRequestReason.Bip353NameNotFound -> stringResource(id = R.string.scan_error_bip353_name_not_found, reason.username, reason.domain) + is Scan.BadRequestReason.Bip353InvalidUri -> stringResource(id = R.string.scan_error_bip353_invalid_uri) is Scan.BadRequestReason.Bip353InvalidOffer -> stringResource(id = R.string.scan_error_bip353_invalid_offer) is Scan.BadRequestReason.Bip353NoDNSSEC -> stringResource(id = R.string.scan_error_bip353_dnssec) } diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/MutualCloseView.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/MutualCloseView.kt index 40e0b7b0f..348564b4b 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/MutualCloseView.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/MutualCloseView.kt @@ -152,7 +152,7 @@ fun MutualCloseView( modifier = Modifier.fillMaxWidth(), enabled = address.isNotBlank() && model.channels.isNotEmpty(), onClick = { - when (val validation = Parser.readBitcoinAddress(chain, address)) { + when (val validation = Parser.parseBip21Uri(chain, address)) { is Either.Left -> { val error = validation.value addressErrorMessage = when (error) { diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/walletinfo/SwapInRefundViewModel.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/walletinfo/SwapInRefundViewModel.kt index 0fc734d25..2f1295cdd 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/walletinfo/SwapInRefundViewModel.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/walletinfo/SwapInRefundViewModel.kt @@ -75,7 +75,7 @@ class SwapInRefundViewModel( log.error("error when estimating swap-in refund fees: ", e) state = SwapInRefundState.Done.Failed.Error(e) }) { - when (val parseAddress = Parser.readBitcoinAddress(NodeParamsManager.chain, address)) { + when (val parseAddress = Parser.parseBip21Uri(NodeParamsManager.chain, address)) { is Either.Left -> { log.debug("invalid refund address=$address (${parseAddress.value}") state = SwapInRefundState.Done.Failed.InvalidAddress(address, parseAddress.value) diff --git a/phoenix-android/src/main/res/values-b+es+419/important_strings.xml b/phoenix-android/src/main/res/values-b+es+419/important_strings.xml index e7b66ae86..5e546b468 100644 --- a/phoenix-android/src/main/res/values-b+es+419/important_strings.xml +++ b/phoenix-android/src/main/res/values-b+es+419/important_strings.xml @@ -166,6 +166,8 @@ Error al procesar este enlace LNURL. Comprueba que sea válido. Este tipo de LNURL aún no es compatible. Estos datos usan un formato desconocido por lo que no se pueden procesar. + El nombre \"%1$s\" no se encuentra en \"%2$s\". + Esta dirección utiliza un recurso Bip21 no válido. Esta dirección utiliza una oferta Bolt12 no válida. Esta dirección está alojada en un DNS no seguro. DNSSEC debe estar habilitado. diff --git a/phoenix-android/src/main/res/values-cs/important_strings.xml b/phoenix-android/src/main/res/values-cs/important_strings.xml index 1e8058161..9f4a465c7 100644 --- a/phoenix-android/src/main/res/values-cs/important_strings.xml +++ b/phoenix-android/src/main/res/values-cs/important_strings.xml @@ -162,6 +162,8 @@ Tento LNURL odkaz se nepodařilo zpracovat. Ujistěte se, že je platný. Tento typ LNURL zatím není podporován. Tato data používají neznámý formát a nelze je zpracovat. + Název \"%1$s\" nebyl nalezen na \"%2$s\". + Tato adresa používá neplatný zdroj Bip21. Tato adresa používá neplatnou nabídku Bolt12. Tato adresa je hostována na nezabezpečeném DNS. DNSSEC musí být povolen. diff --git a/phoenix-android/src/main/res/values-de/important_strings.xml b/phoenix-android/src/main/res/values-de/important_strings.xml index a1d1ec5c2..08c413c06 100644 --- a/phoenix-android/src/main/res/values-de/important_strings.xml +++ b/phoenix-android/src/main/res/values-de/important_strings.xml @@ -165,6 +165,8 @@ Dieser LNURL-Link konnte nicht verarbeitet werden. Stellen Sie sicher, dass er gültig ist. Dieser LNURL-Typ wird noch nicht unterstützt. Diese Daten haben ein unbekanntes Format und können nicht verarbeitet werden. + Name \"%1$s\" wird auf \"%2$s\" nicht gefunden. + Diese Adresse verwendet eine ungültige Bip21 Ressource. Diese Adresse verwendet ein ungültiges Bolt12-Angebot. Diese Adresse wird über einen unsicheren DNS gehostet. DNSSEC muss aktiviert sein. diff --git a/phoenix-android/src/main/res/values-es/important_strings.xml b/phoenix-android/src/main/res/values-es/important_strings.xml index 764a61234..b77e6643a 100644 --- a/phoenix-android/src/main/res/values-es/important_strings.xml +++ b/phoenix-android/src/main/res/values-es/important_strings.xml @@ -169,6 +169,8 @@ No se ha podido procesar este enlace LNURL. Asegúrese de que es válido. Este tipo de LNURL aún no se admite. Estos datos utilizan un formato desconocido y no pueden ser procesados. + El nombre \"%1$s\" no se encuentra en \"%2$s\". + Esta dirección utiliza un recurso Bip21 no válido. Esta dirección utiliza una oferta Bolt12 no válida. Esta dirección está alojada en un DNS no seguro. DNSSEC debe estar habilitado. diff --git a/phoenix-android/src/main/res/values-fr/important_strings.xml b/phoenix-android/src/main/res/values-fr/important_strings.xml index 317b6638e..52c827bbe 100644 --- a/phoenix-android/src/main/res/values-fr/important_strings.xml +++ b/phoenix-android/src/main/res/values-fr/important_strings.xml @@ -169,6 +169,8 @@ Ce lien LNURL n\'a pas pu être traité. Assurez-vous qu\'il soit valide. Ce type de lien LNURL n\'est pas supporté. Ce contenu est mal formatté ou bien n\'est pas géré par Phoenix. + Le nom \"%1$s\" est introuvable sur \"%2$s\" + Cette adresse utilise une ressource Bip21 non valide. Cette adresse utilise une offre Bolt12 invalide. Cette adresse est hébergée sur un DNS non sécurisé. DNSSEC doit être activé. diff --git a/phoenix-android/src/main/res/values-pt-rBR/important_strings.xml b/phoenix-android/src/main/res/values-pt-rBR/important_strings.xml index 27d04e913..75c5c5ed4 100644 --- a/phoenix-android/src/main/res/values-pt-rBR/important_strings.xml +++ b/phoenix-android/src/main/res/values-pt-rBR/important_strings.xml @@ -167,6 +167,8 @@ Falha ao processar esse link LNURL. Verifique se ele é válido. Este tipo de LNURL ainda não é suportado. Esses dados usam um formato desconhecido e não podem ser processados. + O nome \"%1$s\" não foi encontrado em \"%2$s\". + Este endereço usa um recurso Bip21 inválido. Este endereço usa uma oferta Bolt12 inválida. Este endereço está hospedado em um DNS não seguro. O DNSSEC deve estar ativado. diff --git a/phoenix-android/src/main/res/values-sk/important_strings.xml b/phoenix-android/src/main/res/values-sk/important_strings.xml index 7c875ec76..0b61efaa3 100644 --- a/phoenix-android/src/main/res/values-sk/important_strings.xml +++ b/phoenix-android/src/main/res/values-sk/important_strings.xml @@ -168,6 +168,8 @@ Tento LNURL odkaz sa nepodarilo spracovať. Uistite sa, že je platný. Tento typ LNURL zatiaľ nie je podporovaný. Tieto údaje používajú neznámy formát a nemožno ich spracovať. + Názov \"%1$s\" sa nenašiel na \"%2$s\". + Táto adresa používa neplatný zdroj Bip21. Táto adresa používa neplatnú ponuku Bolt12. Táto adresa je umiestnená na nezabezpečenom DNS. DNSSEC musí byť povolený. diff --git a/phoenix-android/src/main/res/values-vi/important_strings.xml b/phoenix-android/src/main/res/values-vi/important_strings.xml index 0e4f6a226..a8a7d5224 100644 --- a/phoenix-android/src/main/res/values-vi/important_strings.xml +++ b/phoenix-android/src/main/res/values-vi/important_strings.xml @@ -175,6 +175,8 @@ Không thể xử lý liên kết LNURL này. Hãy đảm bảo liên kết này có hiệu lực. Loại LNURL này chưa được hỗ trợ. Dữ liệu này sử dụng định dạng không xác định và không thể xử lý được. + Không tìm thấy tên \"%1$s\" trên \"%2$s\". + Địa chỉ này sử dụng tài nguyên Bip21 không hợp lệ. Địa chỉ này sử dụng ưu đãi Bolt12 không hợp lệ. Địa chỉ này được lưu trữ trên một DNS không an toàn. DNSSEC phải được bật. diff --git a/phoenix-android/src/main/res/values/important_strings.xml b/phoenix-android/src/main/res/values/important_strings.xml index d9a443e67..a67f3efc9 100644 --- a/phoenix-android/src/main/res/values/important_strings.xml +++ b/phoenix-android/src/main/res/values/important_strings.xml @@ -171,6 +171,8 @@ Failed to process this LNURL link. Make sure it is valid. This type of LNURL is not supported yet. This data uses an unknown format and cannot be processed. + Name \"%1$s\" is not found on \"%2$s\". + This address uses an invalid Bip21 resource. This address uses an invalid Bolt12 offer. This address is hosted on an unsecure DNS. DNSSEC must be enabled. diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/controllers/config/CloseChannelsConfigurationController.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/controllers/config/CloseChannelsConfigurationController.kt index 48833bb1d..5cb5c9785 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/controllers/config/CloseChannelsConfigurationController.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/controllers/config/CloseChannelsConfigurationController.kt @@ -107,7 +107,7 @@ class AppCloseChannelsConfigurationController( override fun process(intent: CloseChannelsConfiguration.Intent) { val scriptPubKey = if (intent is CloseChannelsConfiguration.Intent.MutualCloseAllChannels) { try { - Parser.readBitcoinAddress(chain, intent.address).right!!.script + Parser.parseBip21Uri(chain, intent.address).right!!.script } catch (e: Exception) { throw IllegalArgumentException("Address is invalid. Caller MUST validate user input via readBitcoinAddress") } diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/controllers/payments/Scan.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/controllers/payments/Scan.kt index 3e9b5f58a..6402ce47f 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/controllers/payments/Scan.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/controllers/payments/Scan.kt @@ -42,6 +42,8 @@ object Scan { data class ChainMismatch(val expected: Chain) : BadRequestReason() data class ServiceError(val url: Url, val error: LnurlError.RemoteFailure) : BadRequestReason() data class InvalidLnurl(val url: Url) : BadRequestReason() + data class Bip353NameNotFound(val username: String, val domain: String) : BadRequestReason() + data class Bip353InvalidUri(val path: String) : BadRequestReason() data class Bip353InvalidOffer(val path: String) : BadRequestReason() data class Bip353NoDNSSEC(val path: String) : BadRequestReason() data class UnsupportedLnurl(val url: Url) : BadRequestReason() diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/controllers/payments/ScanController.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/controllers/payments/ScanController.kt index 6af01f33c..02bb8ba18 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/controllers/payments/ScanController.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/controllers/payments/ScanController.kt @@ -19,7 +19,6 @@ package fr.acinq.phoenix.controllers.payments import fr.acinq.bitcoin.BitcoinError import fr.acinq.bitcoin.Chain import fr.acinq.bitcoin.utils.Either -import fr.acinq.bitcoin.utils.Try import fr.acinq.lightning.Lightning import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.TrampolineFees @@ -576,35 +575,32 @@ class AppScanController( logger.debug { "dns resolved to ${json.toString().take(100)}" } val status = json["Status"]?.jsonPrimitive?.intOrNull + // could be a [BadRequestReason.Bip353NameNotFound] it status == 3 if (status == null || status > 0) return null + // check dnssec + val ad = json["AD"]?.jsonPrimitive?.booleanOrNull + if (ad != true) { + logger.debug { "AD false, abort dns lookup" } + throw Scan.BadRequestReason.Bip353NoDNSSEC(dnsPath) + } + + // check name matches records val records = json["Answer"]?.jsonArray if (records.isNullOrEmpty()) { - logger.debug { "no records for $dnsPath" } - return null + logger.debug { "no answer for $dnsPath" } + throw Scan.BadRequestReason.Bip353NameNotFound(username, domain) } val matchingRecord = records.filterIsInstance().firstOrNull { logger.debug { "inspecting record=$it" } it["name"]?.jsonPrimitive?.content == dnsPath - } ?: return null - - val ad = json["AD"]?.jsonPrimitive?.booleanOrNull - if (ad != true) { - logger.debug { "AD false, abort dns lookup" } - throw Scan.BadRequestReason.Bip353NoDNSSEC(dnsPath) - } - - val data = matchingRecord["data"]?.jsonPrimitive?.content ?: return null - if (!data.startsWith("bitcoin:")) throw Scan.BadRequestReason.Bip353InvalidOffer(dnsPath) - val offerString = data.substringAfter("lno=").substringBefore("?") - if (offerString.isBlank()) throw Scan.BadRequestReason.Bip353InvalidOffer(dnsPath) + } ?: throw Scan.BadRequestReason.Bip353NameNotFound(username, domain) - return when (val offer = OfferTypes.Offer.decode(offerString)) { - is Try.Success -> { offer.result } - is Try.Failure -> { - throw Scan.BadRequestReason.Bip353InvalidOffer(dnsPath) - } + val data = matchingRecord["data"]?.jsonPrimitive?.content ?: throw Scan.BadRequestReason.Bip353InvalidUri(dnsPath) + return when (val res = Parser.parseBip21Uri(chain, data)) { + is Either.Left -> throw Scan.BadRequestReason.Bip353InvalidUri(dnsPath) + is Either.Right -> res.value.offer ?: throw Scan.BadRequestReason.Bip353InvalidOffer(dnsPath) } } @@ -617,7 +613,7 @@ class AppScanController( /** Invokes `Parser.readBitcoinAddress`, but maps [BitcoinUriError.InvalidUri] to a null result instead of a fatal error. */ private fun readBitcoinAddress(input: String): Either? { - return when (val result = Parser.readBitcoinAddress(chain, input)) { + return when (val result = Parser.parseBip21Uri(chain, input)) { is Either.Left -> when (result.left) { is BitcoinUriError.InvalidUri -> null else -> result diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/Parser.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/Parser.kt index 2c809848c..59ae3c288 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/Parser.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/Parser.kt @@ -87,12 +87,12 @@ object Parser { } /** - * Parses an input and returns a [BitcoinUri] if it is valid, or a typed error otherwise. + * Parses an input and returns a bip-21 [BitcoinUri] if it is valid, or a typed error otherwise. * * @param chain the chain this parser expects the address to be valid on. * @param input can range from a basic bitcoin address to a sophisticated Bitcoin URI with a prefix and parameters. */ - fun readBitcoinAddress( + fun parseBip21Uri( chain: Chain, input: String ): Either { @@ -128,13 +128,13 @@ object Parser { val message = url.parameters["message"] val lightning = url.parameters["lightning"]?.let { when (val res = Bolt11Invoice.read(it)) { - is Try.Success -> res.get() + is Try.Success -> res.result is Try.Failure -> null } } val offer = url.parameters["lno"]?.let { when (val res = OfferTypes.Offer.decode(it)) { - is Try.Success -> res.get() + is Try.Success -> res.result is Try.Failure -> null } } @@ -160,6 +160,6 @@ object Parser { /** Transforms a bitcoin address into a public key script if valid, otherwise returns null. */ fun addressToPublicKeyScriptOrNull(chain: Chain, address: String): ByteVector? { - return readBitcoinAddress(chain, address).right?.script + return parseBip21Uri(chain, address).right?.script } } \ No newline at end of file diff --git a/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/data/lnurl/LnurlPayTest.kt b/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/data/lnurl/LnurlPayTest.kt index 4e5fa6df8..c0ca30761 100644 --- a/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/data/lnurl/LnurlPayTest.kt +++ b/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/data/lnurl/LnurlPayTest.kt @@ -117,8 +117,8 @@ class LnurlPayTest { ) } val client = fakeClient(engine) - val lnurl = Lnurl.extractLnurl("acinq@zbd.gg", logger) - assertIs(lnurl) + // TODO move to an email-like reader test with bip353 + val lnurl = Lnurl.Request(Url("https://zbd.gg/.well-known/lnurlp/acinq"), tag = Lnurl.Tag.Pay) val response: HttpResponse = client.get(lnurl.initialUrl) val json = Lnurl.processLnurlResponse(response, logger) assertEquals("payRequest", json.get("tag")!!.jsonPrimitive.content) diff --git a/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/utils/ParserTest.kt b/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/utils/ParserTest.kt index 242e6130f..56f240d33 100644 --- a/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/utils/ParserTest.kt +++ b/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/utils/ParserTest.kt @@ -17,14 +17,13 @@ package fr.acinq.phoenix.utils -import fr.acinq.bitcoin.Bitcoin import fr.acinq.bitcoin.Chain import fr.acinq.bitcoin.BitcoinError import fr.acinq.bitcoin.ByteVector import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.payment.Bolt11Invoice -import fr.acinq.lightning.payment.PaymentRequest import fr.acinq.lightning.utils.sat +import fr.acinq.lightning.wire.OfferTypes import fr.acinq.phoenix.data.BitcoinUriError import fr.acinq.phoenix.data.BitcoinUri import io.ktor.http.* @@ -36,47 +35,47 @@ class ParserTest { @Test fun parse_bitcoin_uri_with_valid_addresses() { - assertIs>(Parser.readBitcoinAddress(Chain.Mainnet, "17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhem")) - assertIs>(Parser.readBitcoinAddress(Chain.Mainnet, "3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX")) - assertIs>(Parser.readBitcoinAddress(Chain.Mainnet, "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4")) - assertIs>(Parser.readBitcoinAddress(Chain.Mainnet, "bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3")) + assertIs>(Parser.parseBip21Uri(Chain.Mainnet, "17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhem")) + assertIs>(Parser.parseBip21Uri(Chain.Mainnet, "3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX")) + assertIs>(Parser.parseBip21Uri(Chain.Mainnet, "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4")) + assertIs>(Parser.parseBip21Uri(Chain.Mainnet, "bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3")) - assertIs>(Parser.readBitcoinAddress(Chain.Testnet, "mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJRfn")) - assertIs>(Parser.readBitcoinAddress(Chain.Testnet, "2MzQwSSnBHWHqSAqtTVQ6v47XtaisrJa1Vc")) - assertIs>(Parser.readBitcoinAddress(Chain.Testnet, "tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx")) - assertIs>(Parser.readBitcoinAddress(Chain.Testnet, "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7")) - assertIs>(Parser.readBitcoinAddress(Chain.Testnet, "tb1p607g5ea77m370pey3y5rg58fz7542hnpg40rs2cqw6w69yt5lf2qlktj2a")) + assertIs>(Parser.parseBip21Uri(Chain.Testnet, "mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJRfn")) + assertIs>(Parser.parseBip21Uri(Chain.Testnet, "2MzQwSSnBHWHqSAqtTVQ6v47XtaisrJa1Vc")) + assertIs>(Parser.parseBip21Uri(Chain.Testnet, "tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx")) + assertIs>(Parser.parseBip21Uri(Chain.Testnet, "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7")) + assertIs>(Parser.parseBip21Uri(Chain.Testnet, "tb1p607g5ea77m370pey3y5rg58fz7542hnpg40rs2cqw6w69yt5lf2qlktj2a")) } @Test fun parse_bitcoin_uri_chain_mismatch() { assertEquals( expected = Either.Left(BitcoinUriError.InvalidScript(error = BitcoinError.ChainHashMismatch)), - actual = Parser.readBitcoinAddress(Chain.Testnet, "bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3") + actual = Parser.parseBip21Uri(Chain.Testnet, "bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3") ) assertEquals( expected = Either.Left(BitcoinUriError.InvalidScript(error = BitcoinError.ChainHashMismatch)), - actual = Parser.readBitcoinAddress(Chain.Mainnet, "tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx") + actual = Parser.parseBip21Uri(Chain.Mainnet, "tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx") ) } @Test fun parse_bitcoin_uri_with_invalid_addresses() { assertIs>( - Parser.readBitcoinAddress(Chain.Mainnet, "17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhe") + Parser.parseBip21Uri(Chain.Mainnet, "17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhe") ) } @Test fun parse_bitcoin_uri_with_bitcoin_prefixes() { assertIs>( - Parser.readBitcoinAddress(Chain.Mainnet, "bitcoin:bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4") + Parser.parseBip21Uri(Chain.Mainnet, "bitcoin:bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4") ) assertIs>( - Parser.readBitcoinAddress(Chain.Mainnet, "bitcoin://bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4") + Parser.parseBip21Uri(Chain.Mainnet, "bitcoin://bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4") ) assertIs>( - Parser.readBitcoinAddress(Chain.Testnet, "bitcoin:?lno=lno1qgsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqsespexwyy4tcadvgg89l9aljus6709kx235hhqrk6n8dey98uyuftzdqrt2gkjvf2rj2vnt7m7chnmazen8wpur2h65ttgftkqaugy6ql9dcsyq39xc2g084xfn0s50zlh2ex22vvaqxqz3vmudklz453nns4d0624sqr8ux4p5usm22qevld4ydfck7hwgcg9wc3f78y7jqhc6hwdq7e9dwkhty3svq5ju4dptxtldjumlxh5lw48jsz6pnagtwrmeus7uq9rc5g6uddwcwldpklxexvlezld8egntua4gsqqy8auz966nksacdac8yv3maq6elp") + Parser.parseBip21Uri(Chain.Testnet, "bitcoin:?lno=lno1qgsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqsespexwyy4tcadvgg89l9aljus6709kx235hhqrk6n8dey98uyuftzdqrt2gkjvf2rj2vnt7m7chnmazen8wpur2h65ttgftkqaugy6ql9dcsyq39xc2g084xfn0s50zlh2ex22vvaqxqz3vmudklz453nns4d0624sqr8ux4p5usm22qevld4ydfck7hwgcg9wc3f78y7jqhc6hwdq7e9dwkhty3svq5ju4dptxtldjumlxh5lw48jsz6pnagtwrmeus7uq9rc5g6uddwcwldpklxexvlezld8egntua4gsqqy8auz966nksacdac8yv3maq6elp") ) } @@ -84,13 +83,13 @@ class ParserTest { fun parse_bitcoin_uri_with_non_bitcoin_prefixes() { // non-bitcoin prefixes are not trimmed, so error is invalid script assertIs>( - Parser.readBitcoinAddress(Chain.Mainnet, "btc:bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4") + Parser.parseBip21Uri(Chain.Mainnet, "btc:bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4") ) assertIs>( - Parser.readBitcoinAddress(Chain.Mainnet, "lightning:bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4") + Parser.parseBip21Uri(Chain.Mainnet, "lightning:bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4") ) assertIs>( - Parser.readBitcoinAddress(Chain.Mainnet, "lnurl://bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4") + Parser.parseBip21Uri(Chain.Mainnet, "lnurl://bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4") ) } @@ -120,7 +119,7 @@ class ParserTest { BitcoinUriError.UnhandledRequiredParams(parameters = listOf("req-somethingyoudontunderstand" to "50", "req-somethingelseyoudontget" to "999")) ), ).forEach { - assertEquals(it.second, Parser.readBitcoinAddress(Chain.Mainnet, it.first)) + assertEquals(it.second, Parser.parseBip21Uri(Chain.Mainnet, it.first)) } } @@ -146,7 +145,7 @@ class ParserTest { BitcoinUri(chain = Chain.Mainnet, address = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", script = ByteVector("0014751e76e8199196d454941c45d1b3a323f1433bd6")) ), ).forEach { (address, expected) -> - val uri = Parser.readBitcoinAddress(Chain.Mainnet, address) + val uri = Parser.parseBip21Uri(Chain.Mainnet, address) assertEquals(expected, uri) } } @@ -224,7 +223,7 @@ class ParserTest { ) ) ).forEach { - assertEquals(it.second, Parser.readBitcoinAddress(Chain.Mainnet, it.first)) + assertEquals(it.second, Parser.parseBip21Uri(Chain.Mainnet, it.first)) } } @@ -264,4 +263,61 @@ class ParserTest { } } + @Test + fun parse_bitcoin_uri_with_offer_parameter() { + val offer = OfferTypes.Offer + .decode("lno1qgsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqsespexwyy4tcadvgg89l9aljus6709kx235hhqrk6n8dey98uyuftzdqzrtkahuum7m56dxlnx8r6tffy54004l7kvs7pylmxx7xs4n54986qyqeeuhhunayntt50snmdkq4t7fzsgghpl69v9csgparek8kv7dlp5uqr8ymp5s4z9upmwr2s8xu020d45t5phqc8nljrq8gzsjmurzevawjz6j6rc95xwfvnhgfx6v4c3jha7jwynecrz3y092nn25ek4yl7xp9yu9ry9zqagt0ktn4wwvqg52v9ss9ls22sqyqqestzp2l6decpn87pq96udsvx") + .get() + + val validUri = BitcoinUri( + chain = Chain.Mainnet, + address = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", + script = ByteVector("0014751e76e8199196d454941c45d1b3a323f1433bd6"), + offer = offer, + ) + val validFoobarUri = validUri + + listOf>>( + // valid offer uris + "bitcoin:bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4?lno=$offer" to Either.Right(validFoobarUri), + "bitcoin:bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4?lno=$offer&foo=bar" to Either.Right( + validFoobarUri.copy(ignoredParams = ParametersBuilder().apply { set("foo", "bar") }.build()) + ), + "bitcoin:bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4?foo=bar&lno=$offer" to Either.Right( + validFoobarUri.copy(ignoredParams = ParametersBuilder().apply { set("foo", "bar") }.build()) + ), + "bitcoin:bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4?foo=bar&lno=$offer&bar=baz" to Either.Right( + validFoobarUri.copy(ignoredParams = ParametersBuilder().apply { set("foo", "bar") ; set("bar", "baz") }.build()) + ), + // valid offer in a typical bip353 uri + "bitcoin:?sp=silentpayment&lno=$offer" to Either.Right( + BitcoinUri( + chain = Chain.Mainnet, + address = "", + script = null, + offer = offer, + ignoredParams = ParametersBuilder().apply { set("sp", "silentpayment") }.build() + ) + ), + // invalid offer parameter + "bitcoin:bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4?lightning=lno1qgsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqsespexwyy4tcadvgg89l9a" to Either.Right( + BitcoinUri( + chain = Chain.Mainnet, + address = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", + script = ByteVector("0014751e76e8199196d454941c45d1b3a323f1433bd6") + ) + ), + // empty offer invoice + "bitcoin:bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4?lno=" to Either.Right( + BitcoinUri( + chain = Chain.Mainnet, + address = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", + script = ByteVector("0014751e76e8199196d454941c45d1b3a323f1433bd6") + ) + ), + ).forEach { (address, expected) -> + val uri = Parser.parseBip21Uri(Chain.Mainnet, address) + assertEquals(expected, uri) + } + } } \ No newline at end of file