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
@@ -16,7 +16,7 @@ import io.novafoundation.nova.common.utils.emptyEthereumAccountId
import io.novafoundation.nova.common.utils.emptySubstrateAccountId
import io.novafoundation.nova.common.utils.findIsInstanceOrNull
import io.novafoundation.nova.common.utils.formatNamed
import io.novafoundation.nova.common.utils.isValidBitcoinAddress
import io.novafoundation.nova.common.utils.isValidBitcoinDestinationAddress
import io.novafoundation.nova.common.utils.removeHexPrefix
import io.novafoundation.nova.common.utils.emptyTronAccountId
import io.novafoundation.nova.common.utils.isValidTronAddress
@@ -372,7 +372,10 @@ fun Chain.multiAddressOf(accountId: ByteArray): MultiAddress {
fun Chain.isValidAddress(address: String): Boolean {
return runCatching {
when {
isBitcoinBased -> address.isValidBitcoinAddress()
// Wider than isValidBitcoinAddress() (native SegWit only, used for this wallet's OWN address/accountId):
// a valid SEND destination can legitimately be P2SH/P2PKH too - confirmed live via a real exchange
// withdrawal address - see BitcoinDestinationAddress.kt.
isBitcoinBased -> address.isValidBitcoinDestinationAddress()
// Tron addresses are Base58Check(0x41 ++ accountId), not SS58 or plain 0x-hex - neither of the two
// branches below would ever accept them, so this needs its own dedicated check.