From 4d1fb0256fdd5e2ea5a931621b4468a49040ec25 Mon Sep 17 00:00:00 2001 From: Satoshi Qazi Muhammed Date: Mon, 13 Jul 2026 06:52:34 -0700 Subject: [PATCH] fix: crash when generating an icon for a P2SH/P2PKH recipient Confirmed live via logcat: tapping "send" to a P2SH address force-closed the app - FATAL EXCEPTION: IllegalArgumentException: Bech32 string is mixed case, thrown from Chain.accountIdOf() -> bitcoinAddressToAccountId() (native-SegWit-only) via the Confirm Send screen's address icon generator, a completely separate call path from the transaction-building fix already shipped. Chain.accountIdOf() now falls back to decodeBitcoinDestination() for P2SH/P2PKH, safe here because this generic accountId is only ever used for opaque purposes (identicons, presence checks) - the real send path already bypasses it entirely and builds the scriptPubKey straight from the typed BitcoinDestination. --- .../nova/common/utils/BitcoinDestinationAddress.kt | 13 +++++++++++++ .../io/novafoundation/nova/runtime/ext/ChainExt.kt | 9 ++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/common/src/main/java/io/novafoundation/nova/common/utils/BitcoinDestinationAddress.kt b/common/src/main/java/io/novafoundation/nova/common/utils/BitcoinDestinationAddress.kt index 7263268b..3f43cc11 100644 --- a/common/src/main/java/io/novafoundation/nova/common/utils/BitcoinDestinationAddress.kt +++ b/common/src/main/java/io/novafoundation/nova/common/utils/BitcoinDestinationAddress.kt @@ -50,6 +50,19 @@ fun String.decodeBitcoinDestination(): BitcoinDestination { fun String.isValidBitcoinDestinationAddress(): Boolean = runCatching { decodeBitcoinDestination() }.isSuccess +/** + * The raw 20-byte hash underlying any destination type, with its type tag dropped - safe ONLY for consumers + * that treat it as an opaque identicon/display seed (e.g. `Chain.accountIdOf` and, downstream, address icon + * generation) and never feed it back into [toScriptPubKey] or re-encode it as an address. Real transaction + * construction must keep using [decodeBitcoinDestination]/[toScriptPubKey] directly, which keep the type. + */ +val BitcoinDestination.hash: ByteArray + get() = when (this) { + is BitcoinDestination.NativeSegwit -> witnessProgram + is BitcoinDestination.P2sh -> scriptHash + is BitcoinDestination.P2pkh -> pubKeyHash + } + /** * @return the scriptPubKey a transaction output must use to actually pay this destination - P2SH/P2PKH have * different script shapes from this wallet's own P2WPKH (see [toP2wpkhScriptPubKey]), despite all three being a diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/ext/ChainExt.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/ext/ChainExt.kt index 29b98c62..67fa6ae1 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/ext/ChainExt.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/ext/ChainExt.kt @@ -10,6 +10,8 @@ import io.novafoundation.nova.common.utils.TokenSymbol import io.novafoundation.nova.common.utils.Urls import io.novafoundation.nova.common.utils.asTokenSymbol import io.novafoundation.nova.common.utils.bitcoinAddressToAccountId +import io.novafoundation.nova.common.utils.decodeBitcoinDestination +import io.novafoundation.nova.common.utils.hash import io.novafoundation.nova.common.utils.bitcoinPublicKeyToAccountId import io.novafoundation.nova.common.utils.emptyBitcoinAccountId import io.novafoundation.nova.common.utils.emptyEthereumAccountId @@ -303,7 +305,12 @@ fun ByteArray.toEthereumAddress(): String { fun Chain.accountIdOf(address: String): ByteArray { return when { isTronBased -> address.tronAddressToAccountId() - isBitcoinBased -> address.bitcoinAddressToAccountId() + // Falls back to decodeBitcoinDestination() for a valid P2SH/P2PKH address (real exchange withdrawal + // addresses were confirmed to be P2SH-only) - this wallet's own address stays native-SegWit-only, but a + // SEND destination legitimately isn't. Safe here ONLY because this generic accountId is used for + // opaque purposes (identicon generation, presence checks) elsewhere, never fed back into building a + // scriptPubKey - see BitcoinDestinationAddress.kt's [hash] doc. + isBitcoinBased -> runCatching { address.bitcoinAddressToAccountId() }.getOrElse { address.decodeBitcoinDestination().hash } isEthereumBased -> address.asEthereumAddress().toAccountId().value else -> address.toAccountId() }