feat: support sending BTC to P2SH/P2PKH destination addresses

Confirmed live via logcat: the real "QR can't be decoded" error was
neither a QR format issue (the earlier BIP21 fix, while correct, wasn't
the cause here) nor a scan failure - the raw QR content was a bare P2SH
address (3...), which this wallet's native-SegWit-only address validation
rejected outright. A real exchange (OKX) withdrawal address turned out to
be P2SH-only with no way to request native SegWit instead.

Adds Base58Check decoding and BitcoinDestinationAddress.kt (P2WPKH/P2SH/
P2PKH, each producing the correct scriptPubKey shape - these differ from
each other, not just cosmetically). This wallet's OWN address/derivation
stays native-SegWit-only and untouched (bitcoinAddressToAccountId is
unaffected) - only the SEND path now recognizes wider destination types.

BitcoinTransactionService/RealBitcoinTransactionService now take the raw
recipient address string instead of a pre-resolved AccountId - converting
to AccountId this early would have discarded which script shape the
destination actually needs, silently building a P2WPKH output for a P2SH
recipient (a real fund-misdirection risk, not just a validation gap).

Test vectors: Satoshi's genesis P2PKH address, a well-known P2SH vanity
address, and the real OKX withdrawal address from this session's live QR
scan, all cross-verified against an independent Python implementation
before writing the Kotlin equivalent.

Reverts the temporary QrDiag logging added to find this - no longer needed.
This commit is contained in:
2026-07-13 05:54:37 -07:00
parent ff19c7c4ae
commit d4faa64c19
8 changed files with 243 additions and 23 deletions
@@ -178,13 +178,9 @@ class AddressInputMixinProvider(
systemCallExecutor.executeSystemCall(ScanQrCodeCall()).mapCatching {
val spec = specProvider.spec.first()
android.util.Log.e("QrDiag", "raw QR content: $it")
qrSharingFactory.create(spec::isValidAddress).decode(it).address
}.onSuccess { address ->
inputFlow.value = address
}.onFailure {
android.util.Log.e("QrDiag", "decode failed", it)
}.onSystemCallFailure {
errorDisplayer(resourceManager.getString(R.string.invoice_scan_error_no_info))
}