From ccf56c0599dd2d1d31ab789d0b3a63094cdcdf2e Mon Sep 17 00:00:00 2001 From: Satoshi Qazi Muhammed Date: Sun, 12 Jul 2026 20:36:20 -0700 Subject: [PATCH] fix: recognize BIP21 bitcoin: URIs when scanning a QR code Confirmed live: sending BTC back out via QR-scanning an external wallet's/ exchange's receive address failed with a generic "QR can't be decoded" error. Root cause: the QR decoder only ever recognized two shapes - substrate:
: triples and bare address strings - neither of which matches the bitcoin:
?amount=... BIP21 URI most wallets and exchanges generate for a BTC address. Adds BitcoinUriQrFormat, tried alongside the existing formats, that strips the scheme and query string before validating the address the normal way. --- .../multiNetwork/qr/BitcoinUriQrFormat.kt | 36 +++++++++++++++++++ .../qr/MultiChainQrSharingFactory.kt | 2 ++ 2 files changed, 38 insertions(+) create mode 100644 runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/qr/BitcoinUriQrFormat.kt diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/qr/BitcoinUriQrFormat.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/qr/BitcoinUriQrFormat.kt new file mode 100644 index 00000000..bf54d9eb --- /dev/null +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/qr/BitcoinUriQrFormat.kt @@ -0,0 +1,36 @@ +package io.novafoundation.nova.runtime.multiNetwork.qr + +import io.novasama.substrate_sdk_android.encrypt.qr.PublicQrFormat +import io.novasama.substrate_sdk_android.encrypt.qr.QrFormat + +private const val BITCOIN_URI_SCHEME = "bitcoin:" + +/** + * BIP21 URI (`bitcoin:
?amount=...&label=...`) - the standard QR payload most wallets/exchanges generate + * for a Bitcoin receive address. Neither `SubstrateQrFormat` (expects a `substrate:
:` triple) + * nor `AddressQrFormat` (treats the whole QR content as a bare address) recognize this, so scanning an external + * wallet's or exchange's BTC address QR failed outright with the generic "QR can't be decoded" error - confirmed + * live against a real BTC withdrawal QR. + */ +class BitcoinUriQrFormat( + private val addressValidator: (String) -> Boolean +) : PublicQrFormat { + + override fun encode(payload: PublicQrFormat.Payload): String { + return "$BITCOIN_URI_SCHEME${payload.address}" + } + + override fun decode(qrContent: String): PublicQrFormat.Payload { + if (!qrContent.startsWith(BITCOIN_URI_SCHEME, ignoreCase = true)) { + throw QrFormat.InvalidFormatException("Not a bitcoin: URI") + } + + val address = qrContent.substring(BITCOIN_URI_SCHEME.length).substringBefore('?') + + return if (addressValidator(address)) { + PublicQrFormat.Payload(address = address) + } else { + throw QrFormat.InvalidFormatException("Supplied bitcoin: URI has an invalid address") + } + } +} diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/qr/MultiChainQrSharingFactory.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/qr/MultiChainQrSharingFactory.kt index ec55265b..c4561f61 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/qr/MultiChainQrSharingFactory.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/qr/MultiChainQrSharingFactory.kt @@ -8,10 +8,12 @@ class MultiChainQrSharingFactory { fun create(addressValidator: (String) -> Boolean): QrSharing { val substrateFormat = SubstrateQrFormat() + val bitcoinUriFormat = BitcoinUriQrFormat(addressValidator) val onlyAddressFormat = AddressQrFormat(addressValidator) val formats = listOf( substrateFormat, + bitcoinUriFormat, onlyAddressFormat )