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:<address>:<pubkey> triples and bare address strings - neither of
which matches the bitcoin:<address>?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.
This commit is contained in:
2026-07-12 20:36:20 -07:00
parent 671534de57
commit ccf56c0599
2 changed files with 38 additions and 0 deletions
@@ -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:<address>?amount=...&label=...`) - the standard QR payload most wallets/exchanges generate
* for a Bitcoin receive address. Neither `SubstrateQrFormat` (expects a `substrate:<address>:<pubkey>` 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")
}
}
}
@@ -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
)