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() }