From 26fb9de65f26d98ef18d3963f0c918f09dfc6671 Mon Sep 17 00:00:00 2001 From: Satoshi Otomakan Date: Tue, 24 Sep 2024 11:29:42 +0200 Subject: [PATCH 01/12] feat(bch): Add `BitcoinCash` blockchain type --- include/TrustWalletCore/TWBlockchain.h | 1 + registry.json | 2 +- rust/Cargo.lock | 13 +++ rust/Cargo.toml | 1 + rust/chains/tw_bitcoincash/Cargo.toml | 12 +++ rust/chains/tw_bitcoincash/src/address.rs | 34 +++++++ rust/chains/tw_bitcoincash/src/entry.rs | 95 +++++++++++++++++++ rust/chains/tw_bitcoincash/src/lib.rs | 6 ++ rust/tw_coin_registry/Cargo.toml | 1 + rust/tw_coin_registry/src/blockchain_type.rs | 1 + rust/tw_coin_registry/src/dispatcher.rs | 3 + .../chains/bitcoincash/bitcoincash_address.rs | 34 +++++++ .../chains/bitcoincash/bitcoincash_compile.rs | 8 ++ .../chains/bitcoincash/bitcoincash_sign.rs | 8 ++ rust/tw_tests/tests/chains/bitcoincash/mod.rs | 7 ++ rust/tw_tests/tests/chains/mod.rs | 1 + .../tests/coin_address_derivation_test.rs | 3 +- src/Bitcoin/Entry.h | 18 ++-- src/BitcoinCash/Entry.h | 21 ++++ src/Coin.cpp | 3 + 20 files changed, 260 insertions(+), 12 deletions(-) create mode 100644 rust/chains/tw_bitcoincash/Cargo.toml create mode 100644 rust/chains/tw_bitcoincash/src/address.rs create mode 100644 rust/chains/tw_bitcoincash/src/entry.rs create mode 100644 rust/chains/tw_bitcoincash/src/lib.rs create mode 100644 rust/tw_tests/tests/chains/bitcoincash/bitcoincash_address.rs create mode 100644 rust/tw_tests/tests/chains/bitcoincash/bitcoincash_compile.rs create mode 100644 rust/tw_tests/tests/chains/bitcoincash/bitcoincash_sign.rs create mode 100644 rust/tw_tests/tests/chains/bitcoincash/mod.rs create mode 100644 src/BitcoinCash/Entry.h diff --git a/include/TrustWalletCore/TWBlockchain.h b/include/TrustWalletCore/TWBlockchain.h index faf4c575c1b..84cac17e3b4 100644 --- a/include/TrustWalletCore/TWBlockchain.h +++ b/include/TrustWalletCore/TWBlockchain.h @@ -65,6 +65,7 @@ enum TWBlockchain { TWBlockchainInternetComputer = 52, TWBlockchainNativeEvmos = 53, // Cosmos TWBlockchainNativeInjective = 54, // Cosmos + TWBlockchainBitcoinCash = 55, }; TW_EXTERN_C_END diff --git a/registry.json b/registry.json index 1bb8123783a..9a06495d51e 100644 --- a/registry.json +++ b/registry.json @@ -1453,7 +1453,7 @@ "coinId": 145, "symbol": "BCH", "decimals": 8, - "blockchain": "Bitcoin", + "blockchain": "BitcoinCash", "derivation": [ { "path": "m/44'/145'/0'/0/0", diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 108bb84e1e0..e45a28fec4e 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1800,6 +1800,18 @@ dependencies = [ "tw_utxo", ] +[[package]] +name = "tw_bitcoincash" +version = "0.1.0" +dependencies = [ + "tw_bitcoin", + "tw_coin_entry", + "tw_keypair", + "tw_memory", + "tw_proto", + "tw_utxo", +] + [[package]] name = "tw_coin_entry" version = "0.1.0" @@ -1829,6 +1841,7 @@ dependencies = [ "tw_aptos", "tw_binance", "tw_bitcoin", + "tw_bitcoincash", "tw_coin_entry", "tw_cosmos", "tw_ethereum", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index ca41fb6099d..f6bd5522ea7 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -3,6 +3,7 @@ members = [ "chains/tw_aptos", "chains/tw_binance", "chains/tw_bitcoin", + "chains/tw_bitcoincash", "chains/tw_cosmos", "chains/tw_ethereum", "chains/tw_greenfield", diff --git a/rust/chains/tw_bitcoincash/Cargo.toml b/rust/chains/tw_bitcoincash/Cargo.toml new file mode 100644 index 00000000000..5a2148d5d8d --- /dev/null +++ b/rust/chains/tw_bitcoincash/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "tw_bitcoincash" +version = "0.1.0" +edition = "2021" + +[dependencies] +tw_bitcoin = { path = "../tw_bitcoin" } +tw_coin_entry = { path = "../../tw_coin_entry" } +tw_keypair = { path = "../../tw_keypair" } +tw_memory = { path = "../../tw_memory" } +tw_proto = { path = "../../tw_proto" } +tw_utxo = { path = "../../frameworks/tw_utxo" } diff --git a/rust/chains/tw_bitcoincash/src/address.rs b/rust/chains/tw_bitcoincash/src/address.rs new file mode 100644 index 00000000000..14943a0630c --- /dev/null +++ b/rust/chains/tw_bitcoincash/src/address.rs @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use std::fmt; +use std::str::FromStr; +use tw_coin_entry::coin_entry::CoinAddress; +use tw_coin_entry::error::prelude::*; +use tw_memory::Data; + +pub struct BitcoinCashAddress { + // TODO add necessary fields. +} + +impl CoinAddress for BitcoinCashAddress { + #[inline] + fn data(&self) -> Data { + todo!() + } +} + +impl FromStr for BitcoinCashAddress { + type Err = AddressError; + + fn from_str(_s: &str) -> Result { + todo!() + } +} + +impl fmt::Display for BitcoinCashAddress { + fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result { + todo!() + } +} diff --git a/rust/chains/tw_bitcoincash/src/entry.rs b/rust/chains/tw_bitcoincash/src/entry.rs new file mode 100644 index 00000000000..846ebd17dd6 --- /dev/null +++ b/rust/chains/tw_bitcoincash/src/entry.rs @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::address::BitcoinCashAddress; +use std::str::FromStr; +use tw_bitcoin::modules::compiler::BitcoinCompiler; +use tw_bitcoin::modules::planner::BitcoinPlanner; +use tw_bitcoin::modules::signer::BitcoinSigner; +use tw_bitcoin::modules::transaction_util::BitcoinTransactionUtil; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::{CoinEntry, PublicKeyBytes, SignatureBytes}; +use tw_coin_entry::derivation::Derivation; +use tw_coin_entry::error::prelude::*; +use tw_coin_entry::modules::json_signer::NoJsonSigner; +use tw_coin_entry::modules::message_signer::NoMessageSigner; +use tw_coin_entry::modules::transaction_decoder::NoTransactionDecoder; +use tw_coin_entry::modules::wallet_connector::NoWalletConnector; +use tw_coin_entry::prefix::NoPrefix; +use tw_keypair::tw::PublicKey; +use tw_proto::BitcoinV2::Proto; + +pub struct BitcoinCashEntry; + +impl CoinEntry for BitcoinCashEntry { + // TODO BitcoinCashPrefix + type AddressPrefix = NoPrefix; + type Address = BitcoinCashAddress; + type SigningInput<'a> = Proto::SigningInput<'a>; + type SigningOutput = Proto::SigningOutput<'static>; + type PreSigningOutput = Proto::PreSigningOutput<'static>; + + // Optional modules: + type JsonSigner = NoJsonSigner; + type PlanBuilder = BitcoinPlanner; + type MessageSigner = NoMessageSigner; + type WalletConnector = NoWalletConnector; + type TransactionDecoder = NoTransactionDecoder; + type TransactionUtil = BitcoinTransactionUtil; + + #[inline] + fn parse_address( + &self, + _coin: &dyn CoinContext, + _address: &str, + _prefix: Option, + ) -> AddressResult { + todo!() + } + + #[inline] + fn parse_address_unchecked( + &self, + _coin: &dyn CoinContext, + address: &str, + ) -> AddressResult { + BitcoinCashAddress::from_str(address) + } + + #[inline] + fn derive_address( + &self, + _coin: &dyn CoinContext, + _public_key: PublicKey, + _derivation: Derivation, + _prefix: Option, + ) -> AddressResult { + todo!() + } + + #[inline] + fn sign(&self, coin: &dyn CoinContext, input: Self::SigningInput<'_>) -> Self::SigningOutput { + BitcoinSigner::sign(coin, &input) + } + + #[inline] + fn preimage_hashes( + &self, + coin: &dyn CoinContext, + input: Self::SigningInput<'_>, + ) -> Self::PreSigningOutput { + BitcoinCompiler::preimage_hashes(coin, input) + } + + #[inline] + fn compile( + &self, + coin: &dyn CoinContext, + input: Self::SigningInput<'_>, + signatures: Vec, + public_keys: Vec, + ) -> Self::SigningOutput { + BitcoinCompiler::compile(coin, input, signatures, public_keys) + } +} diff --git a/rust/chains/tw_bitcoincash/src/lib.rs b/rust/chains/tw_bitcoincash/src/lib.rs new file mode 100644 index 00000000000..249f8bc72ca --- /dev/null +++ b/rust/chains/tw_bitcoincash/src/lib.rs @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod address; +pub mod entry; diff --git a/rust/tw_coin_registry/Cargo.toml b/rust/tw_coin_registry/Cargo.toml index b23f576ab7b..40e2a51b0ef 100644 --- a/rust/tw_coin_registry/Cargo.toml +++ b/rust/tw_coin_registry/Cargo.toml @@ -12,6 +12,7 @@ strum_macros = "0.25" tw_aptos = { path = "../chains/tw_aptos" } tw_binance = { path = "../chains/tw_binance" } tw_bitcoin = { path = "../chains/tw_bitcoin" } +tw_bitcoincash = { path = "../chains/tw_bitcoincash" } tw_coin_entry = { path = "../tw_coin_entry" } tw_cosmos = { path = "../chains/tw_cosmos" } tw_ethereum = { path = "../chains/tw_ethereum" } diff --git a/rust/tw_coin_registry/src/blockchain_type.rs b/rust/tw_coin_registry/src/blockchain_type.rs index a91e4172813..93630d0d66d 100644 --- a/rust/tw_coin_registry/src/blockchain_type.rs +++ b/rust/tw_coin_registry/src/blockchain_type.rs @@ -12,6 +12,7 @@ pub enum BlockchainType { Aptos, Binance, Bitcoin, + BitcoinCash, Cosmos, Ethereum, Greenfield, diff --git a/rust/tw_coin_registry/src/dispatcher.rs b/rust/tw_coin_registry/src/dispatcher.rs index ec28beb651a..b7d7ceba35e 100644 --- a/rust/tw_coin_registry/src/dispatcher.rs +++ b/rust/tw_coin_registry/src/dispatcher.rs @@ -24,6 +24,7 @@ use tw_sui::entry::SuiEntry; use tw_thorchain::entry::ThorchainEntry; use tw_ton::entry::TheOpenNetworkEntry; use tw_utxo::utxo_entry::UtxoEntryExt; +use tw_bitcoincash::entry::BitcoinCashEntry; pub type CoinEntryExtStaticRef = &'static dyn CoinEntryExt; pub type EvmEntryExtStaticRef = &'static dyn EvmEntryExt; @@ -33,6 +34,7 @@ pub type UtxoEntryExtStaticRef = &'static dyn UtxoEntryExt; const APTOS: AptosEntry = AptosEntry; const BINANCE: BinanceEntry = BinanceEntry; const BITCOIN: BitcoinEntry = BitcoinEntry; +const BITCOIN_CASH: BitcoinCashEntry = BitcoinCashEntry; const COSMOS: CosmosEntry = CosmosEntry; const ETHEREUM: EthereumEntry = EthereumEntry; const GREENFIELD: GreenfieldEntry = GreenfieldEntry; @@ -52,6 +54,7 @@ pub fn blockchain_dispatcher(blockchain: BlockchainType) -> RegistryResult Ok(&APTOS), BlockchainType::Binance => Ok(&BINANCE), BlockchainType::Bitcoin => Ok(&BITCOIN), + BlockchainType::BitcoinCash => Ok(&BITCOIN_CASH), BlockchainType::Cosmos => Ok(&COSMOS), BlockchainType::Ethereum => Ok(ÐEREUM), BlockchainType::Greenfield => Ok(&GREENFIELD), diff --git a/rust/tw_tests/tests/chains/bitcoincash/bitcoincash_address.rs b/rust/tw_tests/tests/chains/bitcoincash/bitcoincash_address.rs new file mode 100644 index 00000000000..d94de0d342c --- /dev/null +++ b/rust/tw_tests/tests/chains/bitcoincash/bitcoincash_address.rs @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_any_coin::test_utils::address_utils::{ + test_address_derive, test_address_get_data, test_address_invalid, test_address_normalization, + test_address_valid, +}; +use tw_coin_registry::coin_type::CoinType; + +#[test] +fn test_bitcoincash_address_derive() { + test_address_derive(CoinType::BitcoinCash, "PRIVATE_KEY", "EXPECTED ADDRESS"); +} + +#[test] +fn test_bitcoincash_address_normalization() { + test_address_normalization(CoinType::BitcoinCash, "DENORMALIZED", "EXPECTED"); +} + +#[test] +fn test_bitcoincash_address_is_valid() { + test_address_valid(CoinType::BitcoinCash, "VALID ADDRESS"); +} + +#[test] +fn test_bitcoincash_address_invalid() { + test_address_invalid(CoinType::BitcoinCash, "INVALID ADDRESS"); +} + +#[test] +fn test_bitcoincash_address_get_data() { + test_address_get_data(CoinType::BitcoinCash, "ADDRESS", "HEX(DATA)"); +} diff --git a/rust/tw_tests/tests/chains/bitcoincash/bitcoincash_compile.rs b/rust/tw_tests/tests/chains/bitcoincash/bitcoincash_compile.rs new file mode 100644 index 00000000000..df86f95313c --- /dev/null +++ b/rust/tw_tests/tests/chains/bitcoincash/bitcoincash_compile.rs @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#[test] +fn test_bitcoincash_compile() { + todo!() +} diff --git a/rust/tw_tests/tests/chains/bitcoincash/bitcoincash_sign.rs b/rust/tw_tests/tests/chains/bitcoincash/bitcoincash_sign.rs new file mode 100644 index 00000000000..c9b67069cda --- /dev/null +++ b/rust/tw_tests/tests/chains/bitcoincash/bitcoincash_sign.rs @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#[test] +fn test_bitcoincash_sign() { + todo!() +} diff --git a/rust/tw_tests/tests/chains/bitcoincash/mod.rs b/rust/tw_tests/tests/chains/bitcoincash/mod.rs new file mode 100644 index 00000000000..ae50602c4d9 --- /dev/null +++ b/rust/tw_tests/tests/chains/bitcoincash/mod.rs @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +mod bitcoincash_address; +mod bitcoincash_compile; +mod bitcoincash_sign; diff --git a/rust/tw_tests/tests/chains/mod.rs b/rust/tw_tests/tests/chains/mod.rs index 0f587a6d26a..66129007c4e 100644 --- a/rust/tw_tests/tests/chains/mod.rs +++ b/rust/tw_tests/tests/chains/mod.rs @@ -5,6 +5,7 @@ mod aptos; mod binance; mod bitcoin; +mod bitcoincash; mod common; mod cosmos; mod dydx; diff --git a/rust/tw_tests/tests/coin_address_derivation_test.rs b/rust/tw_tests/tests/coin_address_derivation_test.rs index c0b66f78a96..451b2fa9869 100644 --- a/rust/tw_tests/tests/coin_address_derivation_test.rs +++ b/rust/tw_tests/tests/coin_address_derivation_test.rs @@ -103,8 +103,7 @@ fn test_coin_address_derivation() { CoinType::Syscoin => "sys1qten42eesehw0ktddcp0fws7d3ycsqez3pwuwme", CoinType::Pivx => "DDkFr311AYe6ABMsdSnjv8yoSr1Tppokp8", CoinType::Firo => "a9Kd3gVz5vjegicNuG7K8f8iB5QWkUuTxW", - // TODO should be "bitcoincash:qp0xw4t8xrxae7ed4hq9a96rekynzqry2ydzeh0jgs" - CoinType::BitcoinCash => "19cAJn4Ms8jodBBGtroBNNpCZiHAWGAq7X", + CoinType::BitcoinCash => "bitcoincash:qp0xw4t8xrxae7ed4hq9a96rekynzqry2ydzeh0jgs", CoinType::BitcoinGold => "btg1qten42eesehw0ktddcp0fws7d3ycsqez3lht33r", CoinType::Ravencoin => "RHtMPHweTxYNhBYUN2nJTu9QKyjm7MRKsF", // TODO should be "ecash:qp0xw4t8xrxae7ed4hq9a96rekynzqry2y50du5gw8" diff --git a/src/Bitcoin/Entry.h b/src/Bitcoin/Entry.h index 9d2c8503097..d4ea8e4f619 100644 --- a/src/Bitcoin/Entry.h +++ b/src/Bitcoin/Entry.h @@ -11,18 +11,18 @@ namespace TW::Bitcoin { /// Bitcoin entry dispatcher. /// Note: do not put the implementation here (no matter how simple), to avoid having coin-specific /// includes in this file -class Entry final : public CoinEntry { +class Entry : public CoinEntry { public: - bool validateAddress(TWCoinType coin, const std::string& address, const PrefixVariant& addressPrefix) const; - std::string normalizeAddress(TWCoinType coin, const std::string& address) const; - std::string deriveAddress(TWCoinType coin, const PublicKey& publicKey, TWDerivation derivation, const PrefixVariant& addressPrefix) const; - Data addressToData(TWCoinType coin, const std::string& address) const; - void sign(TWCoinType coin, const Data& dataIn, Data& dataOut) const; - void plan(TWCoinType coin, const Data& dataIn, Data& dataOut) const; + bool validateAddress(TWCoinType coin, const std::string& address, const PrefixVariant& addressPrefix) const final; + std::string normalizeAddress(TWCoinType coin, const std::string& address) const final; + std::string deriveAddress(TWCoinType coin, const PublicKey& publicKey, TWDerivation derivation, const PrefixVariant& addressPrefix) const final; + Data addressToData(TWCoinType coin, const std::string& address) const final; + void sign(TWCoinType coin, const Data& dataIn, Data& dataOut) const final; + void plan(TWCoinType coin, const Data& dataIn, Data& dataOut) const final; - Data preImageHashes(TWCoinType coin, const Data& txInputData) const; + Data preImageHashes(TWCoinType coin, const Data& txInputData) const final; void compile(TWCoinType coin, const Data& txInputData, const std::vector& signatures, - const std::vector& publicKeys, Data& dataOut) const; + const std::vector& publicKeys, Data& dataOut) const final; // Note: buildTransactionInput is not implemented for Binance chain with UTXOs }; diff --git a/src/BitcoinCash/Entry.h b/src/BitcoinCash/Entry.h new file mode 100644 index 00000000000..cc5e78fe61b --- /dev/null +++ b/src/BitcoinCash/Entry.h @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#pragma once + +#include "Bitcoin/Entry.h" + +namespace TW::BitcoinCash { + +/// Entry point for BitcoinCash coin. +/// Note: do not put the implementation here (no matter how simple), to avoid having coin-specific includes in this file +/// +/// Currently, we must support the legacy `Bitcoin.proto` API, +/// but `BitcoinV2.proto` still can be used through `Bitcoin.SigningInput.signing_v2`. +/// TODO inherit Rust::RustCoinEntry directly when `Bitcoin.proto` is deprecated. +class Entry : public Bitcoin::Entry { +}; + +} // namespace TW::BitcoinCash + diff --git a/src/Coin.cpp b/src/Coin.cpp index 6033666b9ef..bb3d3717d54 100644 --- a/src/Coin.cpp +++ b/src/Coin.cpp @@ -66,6 +66,7 @@ #include "InternetComputer/Entry.h" #include "NativeEvmos/Entry.h" #include "NativeInjective/Entry.h" +#include "BitcoinCash/Entry.h" // end_of_coin_includes_marker_do_not_modify using namespace TW; @@ -125,6 +126,7 @@ Greenfield::Entry GreenfieldDP; InternetComputer::Entry InternetComputerDP; NativeEvmos::Entry NativeEvmosDP; NativeInjective::Entry NativeInjectiveDP; +BitcoinCash::Entry BitcoinCashDP; // end_of_coin_dipatcher_declarations_marker_do_not_modify CoinEntry* coinDispatcher(TWCoinType coinType) { @@ -186,6 +188,7 @@ CoinEntry* coinDispatcher(TWCoinType coinType) { case TWBlockchainInternetComputer: entry = &InternetComputerDP; break; case TWBlockchainNativeEvmos: entry = &NativeEvmosDP; break; case TWBlockchainNativeInjective: entry = &NativeInjectiveDP; break; + case TWBlockchainBitcoinCash: entry = &BitcoinCashDP; break; // end_of_coin_dipatcher_switch_marker_do_not_modify default: entry = nullptr; break; From 2ed56e556f1b4f340ad172c5a5d0a6869310034d Mon Sep 17 00:00:00 2001 From: Satoshi Otomakan Date: Tue, 1 Oct 2024 19:38:58 +0200 Subject: [PATCH 02/12] feat(bch): Add support for `CashAddress` * TODO implement `Entry::parse_address_unchecked` --- rust/Cargo.lock | 3 + rust/chains/tw_bitcoincash/Cargo.toml | 3 + rust/chains/tw_bitcoincash/src/address.rs | 41 ++- .../src/cash_address/cash_base32.rs | 78 +++++ .../src/cash_address/checksum.rs | 69 ++++ .../tw_bitcoincash/src/cash_address/mod.rs | 298 ++++++++++++++++++ rust/chains/tw_bitcoincash/src/entry.rs | 17 +- rust/chains/tw_bitcoincash/src/lib.rs | 1 + rust/tw_encoding/src/bech32.rs | 2 +- 9 files changed, 492 insertions(+), 20 deletions(-) create mode 100644 rust/chains/tw_bitcoincash/src/cash_address/cash_base32.rs create mode 100644 rust/chains/tw_bitcoincash/src/cash_address/checksum.rs create mode 100644 rust/chains/tw_bitcoincash/src/cash_address/mod.rs diff --git a/rust/Cargo.lock b/rust/Cargo.lock index e45a28fec4e..dcb26f709ab 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1806,8 +1806,11 @@ version = "0.1.0" dependencies = [ "tw_bitcoin", "tw_coin_entry", + "tw_encoding", + "tw_hash", "tw_keypair", "tw_memory", + "tw_misc", "tw_proto", "tw_utxo", ] diff --git a/rust/chains/tw_bitcoincash/Cargo.toml b/rust/chains/tw_bitcoincash/Cargo.toml index 5a2148d5d8d..2ef2ff965c8 100644 --- a/rust/chains/tw_bitcoincash/Cargo.toml +++ b/rust/chains/tw_bitcoincash/Cargo.toml @@ -6,7 +6,10 @@ edition = "2021" [dependencies] tw_bitcoin = { path = "../tw_bitcoin" } tw_coin_entry = { path = "../../tw_coin_entry" } +tw_encoding = { path = "../../tw_encoding" } +tw_hash = { path = "../../tw_hash" } tw_keypair = { path = "../../tw_keypair" } tw_memory = { path = "../../tw_memory" } +tw_misc = { path = "../../tw_misc" } tw_proto = { path = "../../tw_proto" } tw_utxo = { path = "../../frameworks/tw_utxo" } diff --git a/rust/chains/tw_bitcoincash/src/address.rs b/rust/chains/tw_bitcoincash/src/address.rs index 14943a0630c..5de23b3edd1 100644 --- a/rust/chains/tw_bitcoincash/src/address.rs +++ b/rust/chains/tw_bitcoincash/src/address.rs @@ -2,24 +2,42 @@ // // Copyright © 2017 Trust Wallet. +use crate::cash_address::CashAddress; use std::fmt; use std::str::FromStr; +use tw_coin_entry::coin_context::CoinContext; use tw_coin_entry::coin_entry::CoinAddress; -use tw_coin_entry::error::prelude::*; +use tw_coin_entry::error::prelude::{AddressError, AddressResult}; use tw_memory::Data; +use tw_utxo::address::legacy::LegacyAddress; -pub struct BitcoinCashAddress { - // TODO add necessary fields. +pub enum Address { + Cash(CashAddress), + Legacy(LegacyAddress), } -impl CoinAddress for BitcoinCashAddress { - #[inline] +impl Address { + pub fn from_str_with_coin(coin: &dyn CoinContext, address_str: &str) -> AddressResult { + if let Ok(cash) = CashAddress::from_str_with_coin_and_hrp(coin, address_str, None) { + return Ok(Address::Cash(cash)); + } + if let Ok(legacy) = LegacyAddress::from_str_with_coin_and_prefix(coin, address_str, None) { + return Ok(Address::Legacy(legacy)); + } + Err(AddressError::InvalidInput) + } +} + +impl CoinAddress for Address { fn data(&self) -> Data { - todo!() + match self { + Address::Cash(cash) => cash.data(), + Address::Legacy(legacy) => legacy.bytes().to_vec(), + } } } -impl FromStr for BitcoinCashAddress { +impl FromStr for Address { type Err = AddressError; fn from_str(_s: &str) -> Result { @@ -27,8 +45,11 @@ impl FromStr for BitcoinCashAddress { } } -impl fmt::Display for BitcoinCashAddress { - fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result { - todo!() +impl fmt::Display for Address { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Address::Cash(cash) => write!(f, "{cash}"), + Address::Legacy(legacy) => write!(f, "{legacy}"), + } } } diff --git a/rust/chains/tw_bitcoincash/src/cash_address/cash_base32.rs b/rust/chains/tw_bitcoincash/src/cash_address/cash_base32.rs new file mode 100644 index 00000000000..4d7ebcab0b8 --- /dev/null +++ b/rust/chains/tw_bitcoincash/src/cash_address/cash_base32.rs @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_encoding::{EncodingError, EncodingResult}; + +/// Charset for converting from base32. +const CHARSET_REV: [i8; 128] = [ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + 15, -1, 10, 17, 21, 20, 26, 30, 7, 5, -1, -1, -1, -1, -1, -1, -1, 29, -1, 24, 13, 25, 9, 8, 23, + -1, 18, 22, 31, 27, 19, -1, 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1, -1, 29, + -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, + -1, -1, -1, -1, +]; + +/// Charset for converting to base32. +const CHARSET: [char; 32] = [ + 'q', 'p', 'z', 'r', 'y', '9', 'x', '8', 'g', 'f', '2', 't', 'v', 'd', 'w', '0', 's', '3', 'j', + 'n', '5', '4', 'k', 'h', 'c', 'e', '6', 'm', 'u', 'a', '7', 'l', +]; + +/// Encodes a 5-bit packed array as BCH base32. +pub fn encode(input: &[u8]) -> EncodingResult { + input + .iter() + .map(|i| CHARSET.get(*i as usize)) + .collect::>() + .ok_or(EncodingError::InvalidInput) +} + +/// Decods a BCH base32 string as a 5-bit packed array. +pub fn decode(input: &str) -> EncodingResult> { + input + .chars() + .map(|c| { + let val = *CHARSET_REV + .get(c as usize) + .ok_or(EncodingError::InvalidInput)?; + if val == -1 { + return Err(EncodingError::InvalidInput); + } + Ok(val as u8) + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + use tw_encoding::hex::DecodeHex; + + /// `raw` must be 5-bit packed - the condition is required by base32 encode and decode functions. + #[track_caller] + fn test_base32_impl(raw_hex: &str, encoded: &str) { + let raw = raw_hex.decode_hex().unwrap(); + let actual_encoded = encode(&raw).unwrap(); + assert_eq!(actual_encoded, encoded); + let actual_raw = decode(&actual_encoded).unwrap(); + assert_eq!(actual_raw, raw); + } + + #[test] + fn test_base32() { + test_base32_impl( + "180e0919131e16011c001e1c1607010b120701071317151e1819141b031b1d0a", + "cwfen7kpuq7uk8ptj8p8nh47ce5mrma2", + ); + test_base32_impl( + "08080f081c1d1002011119051104020811150f140b181d1006140b0216121605", + "gg0guaszp3e93yzg3405tcasx5tzkjk9", + ); + test_base32_impl( + "0c0215180000050e07061619161f1409120c0a060b1c070e13090f1d0f160b1b", + "vz4cqq9w8xkekl5fjv2xtu8wnf0a0ktm", + ); + } +} diff --git a/rust/chains/tw_bitcoincash/src/cash_address/checksum.rs b/rust/chains/tw_bitcoincash/src/cash_address/checksum.rs new file mode 100644 index 00000000000..3f3133a75bc --- /dev/null +++ b/rust/chains/tw_bitcoincash/src/cash_address/checksum.rs @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_memory::Data; + +pub const CHECKSUM_LEN: usize = 8; +pub const PHANTOM_CHECKSUM: [u8; CHECKSUM_LEN] = [0; CHECKSUM_LEN]; + +// CalculateChecksum calculates a BCH checksum for a nibble-packed cashaddress +// that properly includes the network prefix. +pub fn calculate_checksum(prefix: &str, payload: &[u8]) -> u64 { + // Convert the prefix string to a byte array with each element + // being the corresponding character's right-most 5 bits. + let mut raw_data: Vec<_> = prefix.as_bytes().iter().map(|x| x & 0b11111).collect(); + // Add a null termination byte after the prefix. + raw_data.push(0); + raw_data.extend(payload); + poly_mod(&raw_data) +} + +pub fn cacl_and_append_checksum(prefix: &str, payload: &[u8]) -> Data { + // The checksum sits in the last eight bytes. + // Append the phantom checksum to calculate an actual value. + let mut payload_with_checksum: Vec<_> = payload + .into_iter() + .copied() + .chain(PHANTOM_CHECKSUM) + .collect(); + + let checksum = calculate_checksum(prefix, &payload_with_checksum); + + let mut checksum_starts_at = payload_with_checksum.len() - CHECKSUM_LEN; + // Rewrite the checksum. + for i in 0..CHECKSUM_LEN { + payload_with_checksum[checksum_starts_at] = ((checksum >> (5 * (7 - i))) & 0x1F) as u8; + checksum_starts_at += 1; + } + + payload_with_checksum +} + +/// The poly_mod is a BCH-encoding checksum function per the CashAddr specification. +/// See https://github.com/bitcoincashorg/bitcoincash.org/blob/master/spec/cashaddr.md#checksum +pub fn poly_mod(raw_data: &[u8]) -> u64 { + let mut c = 1u64; + for d in raw_data { + let c0 = c >> 35; + c = ((c & 0x07ffffffff) << 5) ^ (*d as u64); + + if c0 & 0x01 != 0 { + c ^= 0x98f2bc8e61; + } + if c0 & 0x02 != 0 { + c ^= 0x79b76d99e2; + } + if c0 & 0x04 != 0 { + c ^= 0xf33e5fb3c4; + } + if c0 & 0x08 != 0 { + c ^= 0xae2eabe2a8; + } + if c0 & 0x10 != 0 { + c ^= 0x1e4f43e470; + } + } + + c ^ 1 +} diff --git a/rust/chains/tw_bitcoincash/src/cash_address/mod.rs b/rust/chains/tw_bitcoincash/src/cash_address/mod.rs new file mode 100644 index 00000000000..94a6ae34215 --- /dev/null +++ b/rust/chains/tw_bitcoincash/src/cash_address/mod.rs @@ -0,0 +1,298 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use std::fmt; +use tw_coin_entry::coin_context::CoinContext; +use tw_coin_entry::coin_entry::CoinAddress; +use tw_coin_entry::error::prelude::*; +use tw_encoding::bech32; +use tw_hash::hasher::sha256_ripemd; +use tw_hash::H160; +use tw_keypair::ecdsa; +use tw_memory::Data; +use tw_utxo::address::legacy::LegacyAddress; + +/// BitcoinCash specific base32 format. +mod cash_base32; +mod checksum; + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum CashAddressType { + P2PKH, + P2SH, +} + +#[derive(Debug, Eq, PartialEq)] +pub struct CashAddress { + hrp: String, + ty: CashAddressType, + key_hash: H160, + /// An address string created from this `hrp`, `ty` and `key_hash`. + address_str: String, +} + +impl CashAddress { + pub fn p2pkh_with_public_key( + hrp: String, + public_key: &ecdsa::secp256k1::PublicKey, + ) -> AddressResult { + let public_key_hash = sha256_ripemd(public_key.compressed().as_slice()); + let key_hash = + H160::try_from(public_key_hash.as_slice()).expect("sha256_ripemd returns 20 bytes"); + let address_str = Self::encode(&hrp, CashAddressType::P2PKH, &key_hash)?; + Ok(CashAddress { + hrp, + ty: CashAddressType::P2PKH, + key_hash: H160::try_from(public_key_hash.as_slice()) + .expect("sha256_ripemd returns 20 bytes"), + address_str, + }) + } + + pub fn from_str_with_coin_and_hrp( + coin: &dyn CoinContext, + address_str: &str, + expected_hrp: Option<&str>, + ) -> AddressResult { + let expected_hrp = match expected_hrp { + Some(hrp) => hrp.to_string(), + None => coin.hrp().ok_or(AddressError::InvalidRegistry)?, + }; + + Self::from_str_with_hrp(address_str, &expected_hrp) + } + + pub fn from_str_with_hrp(address_str: &str, expected_hrp: &str) -> AddressResult { + let address_str = address_str.to_lowercase(); + let (prefix, encoded_payload) = split_address(&address_str)?; + + // Cash address can have no prefix. If it's set, we should check whether it's expected. + if let Some(prefix) = prefix { + if prefix.to_lowercase() != expected_hrp { + return Err(AddressError::InvalidHrp); + } + } + + let payload_with_checksum = + cash_base32::decode(encoded_payload).map_err(|_| AddressError::InvalidInput)?; + + // Ensure the checksum is zero when decoding. + let checksum = checksum::calculate_checksum(&expected_hrp, &payload_with_checksum); + if checksum != 0 { + return Err(AddressError::InvalidChecksum); + } + + // Get the payload without the checksum. + let payload_u5 = + &payload_with_checksum[..payload_with_checksum.len() - checksum::CHECKSUM_LEN]; + + let payload_data = { + let from = 5; + let to = 8; + let pad = false; + bech32::convert_bits(payload_u5, from, to, pad) + .map_err(|_| AddressError::InvalidInput)? + }; + + let version_byte = payload_data[0]; + let ty = get_address_type(version_byte)?; + let key_hash = + H160::try_from(&payload_data[1..]).map_err(|_| AddressError::InvalidInput)?; + + // `encoded_payload` is checked already, and it contains the valid checksum at the end. + let address_str = format!("{expected_hrp}:{encoded_payload}"); + Ok(CashAddress { + hrp: expected_hrp.to_string(), + ty, + key_hash, + address_str, + }) + } + + pub fn to_legacy(&self, p2pkh_prefix: u8, p2sh_prefix: u8) -> AddressResult { + match self.ty { + CashAddressType::P2PKH => LegacyAddress::new(p2pkh_prefix, self.key_hash.as_slice()), + CashAddressType::P2SH => LegacyAddress::new(p2sh_prefix, self.key_hash.as_slice()), + } + } + + fn encode(hrp: &str, ty: CashAddressType, key_hash: &H160) -> AddressResult { + let mut payload = Vec::with_capacity(1 + H160::LEN + checksum::CHECKSUM_LEN); + payload.push(Self::version_byte(ty)); + payload.extend_from_slice(key_hash.as_slice()); + + let payload_u5 = { + let from = 8; + let to = 5; + let pad = true; + bech32::convert_bits(&payload, from, to, pad).map_err(|_| AddressError::InvalidInput)? + }; + + let payload_with_checksum = checksum::cacl_and_append_checksum(hrp, &payload_u5); + let encoded_payload = + cash_base32::encode(&payload_with_checksum).map_err(|_| AddressError::InvalidInput)?; + Ok(format!("{hrp}:{encoded_payload}")) + } + + fn version_byte(ty: CashAddressType) -> u8 { + let en_address_type: u8 = match ty { + CashAddressType::P2PKH => 0, + CashAddressType::P2SH => 1, + }; + + // Please note we always serialize the key hash length as 0 here, + // as the length is always 20 (160 bits). + // https://github.com/bitcoincashorg/bitcoincash.org/blob/master/spec/cashaddr.md#version-byte + let serialized_hash_len = 0; + + let mut version_byte = en_address_type; + version_byte <<= 3; + version_byte |= serialized_hash_len; + + version_byte + } +} + +impl CoinAddress for CashAddress { + #[inline] + fn data(&self) -> Data { + self.key_hash.to_vec() + } +} + +impl fmt::Display for CashAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.address_str) + } +} + +fn split_address(addr: &str) -> AddressResult<(Option<&str>, &str)> { + let tokens: Vec<&str> = addr.split(':').collect(); + if tokens.len() == 1 { + Ok((None, tokens[0])) + } else if tokens.len() == 2 { + Ok((Some(tokens[0]), tokens[1])) + } else { + Err(AddressError::InvalidInput) + } +} + +fn get_address_type(version_byte: u8) -> AddressResult { + match version_byte & 8 { + 0 => Ok(CashAddressType::P2PKH), + 8 => Ok(CashAddressType::P2SH), + _ => Err(AddressError::UnexpectedAddressPrefix), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[track_caller] + fn test_address_from_str_impl(s: &str, expected: CashAddress) { + let expected_hrp = &expected.hrp; + let actual = CashAddress::from_str_with_hrp(s, expected_hrp).unwrap(); + assert_eq!(actual, expected); + } + + #[test] + fn test_cash_address_from_str() { + test_address_from_str_impl( + "bitcoincash:pq4ql3ph6738xuv2cycduvkpu4rdwqge5q2uxdfg6f", + CashAddress { + hrp: "bitcoincash".to_string(), + ty: CashAddressType::P2SH, + key_hash: H160::from("2a0fc437d7a273718ac130de32c1e546d70119a0"), + address_str: "bitcoincash:pq4ql3ph6738xuv2cycduvkpu4rdwqge5q2uxdfg6f".to_string(), + }, + ); + + test_address_from_str_impl( + "qrplwyx7kueqkrh6dmd3fclta6u32hafp5tnpkchx2", + CashAddress { + hrp: "bitcoincash".to_string(), + ty: CashAddressType::P2PKH, + key_hash: H160::from("c3f710deb7320b0efa6edb14e3ebeeb9155fa90d"), + address_str: "bitcoincash:qrplwyx7kueqkrh6dmd3fclta6u32hafp5tnpkchx2".to_string(), + }, + ); + + test_address_from_str_impl( + "BitCoinCash:QRPLWYX7KUEQKRH6DMD3FCLTA6U32HAFP5TNPKCHX2", + CashAddress { + hrp: "bitcoincash".to_string(), + ty: CashAddressType::P2PKH, + key_hash: H160::from("c3f710deb7320b0efa6edb14e3ebeeb9155fa90d"), + address_str: "bitcoincash:qrplwyx7kueqkrh6dmd3fclta6u32hafp5tnpkchx2".to_string(), + }, + ); + + test_address_from_str_impl( + "bchtest:qqjr7yu573z4faxw8ltgvjwpntwys08fysk07zmvce", + CashAddress { + hrp: "bchtest".to_string(), + ty: CashAddressType::P2PKH, + key_hash: H160::from("243f1394f44554f4ce3fd68649c19adc483ce924"), + address_str: "bchtest:qqjr7yu573z4faxw8ltgvjwpntwys08fysk07zmvce".to_string(), + }, + ); + } + + struct AddressToLegacyTest<'a> { + p2pkh_prefix: u8, + p2sh_prefixes: u8, + hrp: &'a str, + address: &'a str, + legacy: &'a str, + } + + #[track_caller] + fn test_address_to_legacy_impl(input: AddressToLegacyTest) { + let cash = CashAddress::from_str_with_hrp(input.address, input.hrp).unwrap(); + let legacy = cash + .to_legacy(input.p2pkh_prefix, input.p2sh_prefixes) + .unwrap(); + assert_eq!(legacy.to_string(), input.legacy); + } + + #[test] + fn test_cash_address_to_legacy() { + // P2PKH + test_address_to_legacy_impl(AddressToLegacyTest { + p2pkh_prefix: 0, + p2sh_prefixes: 5, + hrp: "bitcoincash", + address: "bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a", + legacy: "1BpEi6DfDAUFd7GtittLSdBeYJvcoaVggu", + }); + + // P2PKH + test_address_to_legacy_impl(AddressToLegacyTest { + p2pkh_prefix: 0, + p2sh_prefixes: 5, + hrp: "bitcoincash", + address: "qr95sy3j9xwd2ap32xkykttr4cvcu7as4y0qverfuy", + legacy: "1KXrWXciRDZUpQwQmuM1DbwsKDLYAYsVLR", + }); + + // P2SH + test_address_to_legacy_impl(AddressToLegacyTest { + p2pkh_prefix: 0, + p2sh_prefixes: 5, + hrp: "bitcoincash", + address: "bitcoincash:pqq3728yw0y47sqn6l2na30mcw6zm78dzq5ucqzc37", + legacy: "31nwvkZwyPdgzjBJZXfDmSWsC4ZLKpYyUw", + }); + + // P2PKH testnet + test_address_to_legacy_impl(AddressToLegacyTest { + p2pkh_prefix: 111, + p2sh_prefixes: 196, + hrp: "bchtest", + address: "bchtest:qpcgc96kles6v35a2525dppaapzk2htdkv72dv08qf", + legacy: "mqn3oF7tcju9uqoG1H1AZt4M6CP1PTaELo", + }); + } +} diff --git a/rust/chains/tw_bitcoincash/src/entry.rs b/rust/chains/tw_bitcoincash/src/entry.rs index 846ebd17dd6..fe357238297 100644 --- a/rust/chains/tw_bitcoincash/src/entry.rs +++ b/rust/chains/tw_bitcoincash/src/entry.rs @@ -2,8 +2,7 @@ // // Copyright © 2017 Trust Wallet. -use crate::address::BitcoinCashAddress; -use std::str::FromStr; +use crate::address::Address; use tw_bitcoin::modules::compiler::BitcoinCompiler; use tw_bitcoin::modules::planner::BitcoinPlanner; use tw_bitcoin::modules::signer::BitcoinSigner; @@ -23,9 +22,9 @@ use tw_proto::BitcoinV2::Proto; pub struct BitcoinCashEntry; impl CoinEntry for BitcoinCashEntry { - // TODO BitcoinCashPrefix + // TODO `BitcoinCash` should have its own prefix enum with an HRP and Base58 prefixes. type AddressPrefix = NoPrefix; - type Address = BitcoinCashAddress; + type Address = Address; type SigningInput<'a> = Proto::SigningInput<'a>; type SigningOutput = Proto::SigningOutput<'static>; type PreSigningOutput = Proto::PreSigningOutput<'static>; @@ -41,20 +40,20 @@ impl CoinEntry for BitcoinCashEntry { #[inline] fn parse_address( &self, - _coin: &dyn CoinContext, - _address: &str, + coin: &dyn CoinContext, + address: &str, _prefix: Option, ) -> AddressResult { - todo!() + Address::from_str_with_coin(coin, address) } #[inline] fn parse_address_unchecked( &self, _coin: &dyn CoinContext, - address: &str, + _address: &str, ) -> AddressResult { - BitcoinCashAddress::from_str(address) + todo!() } #[inline] diff --git a/rust/chains/tw_bitcoincash/src/lib.rs b/rust/chains/tw_bitcoincash/src/lib.rs index 249f8bc72ca..edbc1236b33 100644 --- a/rust/chains/tw_bitcoincash/src/lib.rs +++ b/rust/chains/tw_bitcoincash/src/lib.rs @@ -2,5 +2,6 @@ // // Copyright © 2017 Trust Wallet. +pub mod cash_address; pub mod address; pub mod entry; diff --git a/rust/tw_encoding/src/bech32.rs b/rust/tw_encoding/src/bech32.rs index c0365763baa..97e184e5513 100644 --- a/rust/tw_encoding/src/bech32.rs +++ b/rust/tw_encoding/src/bech32.rs @@ -5,7 +5,7 @@ use bech32::{FromBase32, ToBase32, Variant}; use tw_memory::Data; -pub use bech32::Error as Bech32Error; +pub use bech32::{convert_bits, CheckBase32, Error as Bech32Error}; pub type Bech32Result = Result; From 70a1f96740636a130d2f3770dedab608be21d41d Mon Sep 17 00:00:00 2001 From: Satoshi Otomakan Date: Wed, 2 Oct 2024 10:49:03 +0200 Subject: [PATCH 03/12] feat(bch): Finalize BitcoinCash `Address` * Add `UncheckedCashAddress` * TODO test `Address::from_str_unchecked` --- rust/chains/tw_bitcoincash/src/address.rs | 18 ++- .../tw_bitcoincash/src/cash_address/mod.rs | 87 +++-------- .../src/cash_address/unchecked.rs | 147 ++++++++++++++++++ rust/chains/tw_bitcoincash/src/entry.rs | 15 +- .../chains/bitcoincash/bitcoincash_address.rs | 77 ++++++++- 5 files changed, 258 insertions(+), 86 deletions(-) create mode 100644 rust/chains/tw_bitcoincash/src/cash_address/unchecked.rs diff --git a/rust/chains/tw_bitcoincash/src/address.rs b/rust/chains/tw_bitcoincash/src/address.rs index 5de23b3edd1..c32f747051a 100644 --- a/rust/chains/tw_bitcoincash/src/address.rs +++ b/rust/chains/tw_bitcoincash/src/address.rs @@ -26,6 +26,16 @@ impl Address { } Err(AddressError::InvalidInput) } + + pub fn from_str_unchecked(address_str: &str) -> AddressResult { + if let Ok(cash) = CashAddress::from_str_unchecked(address_str) { + return Ok(Address::Cash(cash)); + } + if let Ok(legacy) = LegacyAddress::from_str(address_str) { + return Ok(Address::Legacy(legacy)); + } + Err(AddressError::InvalidInput) + } } impl CoinAddress for Address { @@ -37,14 +47,6 @@ impl CoinAddress for Address { } } -impl FromStr for Address { - type Err = AddressError; - - fn from_str(_s: &str) -> Result { - todo!() - } -} - impl fmt::Display for Address { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { diff --git a/rust/chains/tw_bitcoincash/src/cash_address/mod.rs b/rust/chains/tw_bitcoincash/src/cash_address/mod.rs index 94a6ae34215..f060724fc40 100644 --- a/rust/chains/tw_bitcoincash/src/cash_address/mod.rs +++ b/rust/chains/tw_bitcoincash/src/cash_address/mod.rs @@ -3,6 +3,7 @@ // Copyright © 2017 Trust Wallet. use std::fmt; +use std::str::FromStr; use tw_coin_entry::coin_context::CoinContext; use tw_coin_entry::coin_entry::CoinAddress; use tw_coin_entry::error::prelude::*; @@ -16,6 +17,7 @@ use tw_utxo::address::legacy::LegacyAddress; /// BitcoinCash specific base32 format. mod cash_base32; mod checksum; +mod unchecked; #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum CashAddressType { @@ -33,7 +35,15 @@ pub struct CashAddress { } impl CashAddress { - pub fn p2pkh_with_public_key( + pub fn p2pkh_with_coin( + coin: &dyn CoinContext, + public_key: &ecdsa::secp256k1::PublicKey, + ) -> AddressResult { + let hrp = coin.hrp().ok_or(AddressError::InvalidRegistry)?; + Self::p2pkh_with_hrp(hrp, public_key) + } + + pub fn p2pkh_with_hrp( hrp: String, public_key: &ecdsa::secp256k1::PublicKey, ) -> AddressResult { @@ -60,54 +70,15 @@ impl CashAddress { None => coin.hrp().ok_or(AddressError::InvalidRegistry)?, }; - Self::from_str_with_hrp(address_str, &expected_hrp) + Self::from_str_with_hrp(address_str, expected_hrp) } - pub fn from_str_with_hrp(address_str: &str, expected_hrp: &str) -> AddressResult { - let address_str = address_str.to_lowercase(); - let (prefix, encoded_payload) = split_address(&address_str)?; - - // Cash address can have no prefix. If it's set, we should check whether it's expected. - if let Some(prefix) = prefix { - if prefix.to_lowercase() != expected_hrp { - return Err(AddressError::InvalidHrp); - } - } - - let payload_with_checksum = - cash_base32::decode(encoded_payload).map_err(|_| AddressError::InvalidInput)?; - - // Ensure the checksum is zero when decoding. - let checksum = checksum::calculate_checksum(&expected_hrp, &payload_with_checksum); - if checksum != 0 { - return Err(AddressError::InvalidChecksum); - } - - // Get the payload without the checksum. - let payload_u5 = - &payload_with_checksum[..payload_with_checksum.len() - checksum::CHECKSUM_LEN]; - - let payload_data = { - let from = 5; - let to = 8; - let pad = false; - bech32::convert_bits(payload_u5, from, to, pad) - .map_err(|_| AddressError::InvalidInput)? - }; - - let version_byte = payload_data[0]; - let ty = get_address_type(version_byte)?; - let key_hash = - H160::try_from(&payload_data[1..]).map_err(|_| AddressError::InvalidInput)?; + pub fn from_str_with_hrp(address_str: &str, expected_hrp: String) -> AddressResult { + unchecked::UncheckedCashAddress::from_str(address_str)?.checked_with_prefix(expected_hrp) + } - // `encoded_payload` is checked already, and it contains the valid checksum at the end. - let address_str = format!("{expected_hrp}:{encoded_payload}"); - Ok(CashAddress { - hrp: expected_hrp.to_string(), - ty, - key_hash, - address_str, - }) + pub fn from_str_unchecked(address_str: &str) -> AddressResult { + unchecked::UncheckedCashAddress::from_str(address_str)?.partly_checked() } pub fn to_legacy(&self, p2pkh_prefix: u8, p2sh_prefix: u8) -> AddressResult { @@ -167,33 +138,13 @@ impl fmt::Display for CashAddress { } } -fn split_address(addr: &str) -> AddressResult<(Option<&str>, &str)> { - let tokens: Vec<&str> = addr.split(':').collect(); - if tokens.len() == 1 { - Ok((None, tokens[0])) - } else if tokens.len() == 2 { - Ok((Some(tokens[0]), tokens[1])) - } else { - Err(AddressError::InvalidInput) - } -} - -fn get_address_type(version_byte: u8) -> AddressResult { - match version_byte & 8 { - 0 => Ok(CashAddressType::P2PKH), - 8 => Ok(CashAddressType::P2SH), - _ => Err(AddressError::UnexpectedAddressPrefix), - } -} - #[cfg(test)] mod tests { use super::*; #[track_caller] fn test_address_from_str_impl(s: &str, expected: CashAddress) { - let expected_hrp = &expected.hrp; - let actual = CashAddress::from_str_with_hrp(s, expected_hrp).unwrap(); + let actual = CashAddress::from_str_with_hrp(s, expected.hrp.clone()).unwrap(); assert_eq!(actual, expected); } @@ -250,7 +201,7 @@ mod tests { #[track_caller] fn test_address_to_legacy_impl(input: AddressToLegacyTest) { - let cash = CashAddress::from_str_with_hrp(input.address, input.hrp).unwrap(); + let cash = CashAddress::from_str_with_hrp(input.address, input.hrp.to_string()).unwrap(); let legacy = cash .to_legacy(input.p2pkh_prefix, input.p2sh_prefixes) .unwrap(); diff --git a/rust/chains/tw_bitcoincash/src/cash_address/unchecked.rs b/rust/chains/tw_bitcoincash/src/cash_address/unchecked.rs new file mode 100644 index 00000000000..c9c5f9c556d --- /dev/null +++ b/rust/chains/tw_bitcoincash/src/cash_address/unchecked.rs @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::cash_address::checksum::{calculate_checksum, CHECKSUM_LEN}; +use crate::cash_address::{cash_base32, CashAddress, CashAddressType}; +use std::fmt; +use std::str::FromStr; +use tw_coin_entry::error::prelude::{AddressError, AddressResult}; +use tw_encoding::bech32; +use tw_hash::H160; +use tw_memory::Data; + +/// BitcoinCash address may or may not have a prefix. +struct CashAddressParts { + prefix: Option, + payload: String, +} + +impl FromStr for CashAddressParts { + type Err = AddressError; + + fn from_str(s: &str) -> Result { + let address_str = s.to_lowercase(); + let tokens: Vec<&str> = address_str.split(':').collect(); + + if tokens.len() == 1 { + Ok(CashAddressParts { + prefix: None, + payload: tokens[0].to_string(), + }) + } else if tokens.len() == 2 { + Ok(CashAddressParts { + prefix: Some(tokens[0].to_string()), + payload: tokens[1].to_string(), + }) + } else { + Err(AddressError::InvalidInput) + } + } +} + +impl fmt::Display for CashAddressParts { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.prefix { + Some(ref prefix) => write!(f, "{prefix}:{}", self.payload), + None => write!(f, "{}", self.payload), + } + } +} + +/// BitcoinCash address with an unchecked checksum and prefix. +/// Consider using [`CashAddress`] directly instead. +pub struct UncheckedCashAddress { + parts: CashAddressParts, + ty: CashAddressType, + key_hash: H160, + /// Original 5-bit packed array decoded from base32. + /// Used to validate the checksum along with a prefix. + unchecked_payload_with_checksum: Data, +} + +impl UncheckedCashAddress { + /// Checks whether the address has an expected prefix, and verifies the address checksum. + pub fn checked_with_prefix(self, expected_prefix: String) -> AddressResult { + // Validate the prefix if it was present. + if let Some(ref prefix) = self.parts.prefix { + if *prefix != expected_prefix { + return Err(AddressError::InvalidHrp); + } + } + + // Ensure the checksum is zero when decoding. + let checksum = calculate_checksum(&expected_prefix, &self.unchecked_payload_with_checksum); + if checksum != 0 { + return Err(AddressError::InvalidChecksum); + } + + let address_str = format!("{expected_prefix}:{}", self.parts.payload); + Ok(CashAddress { + hrp: expected_prefix, + ty: self.ty, + key_hash: self.key_hash, + address_str, + }) + } + + /// Tries to verify the address checksum if it contains a prefix, otherwise do not verify at all. + /// Please note this method doesn't validate if the prefix belongs to a network. + /// + /// Consider using [`UncheckedCashAddress::checked_with_prefix`] instead. + pub fn partly_checked(self) -> AddressResult { + match self.parts.prefix.clone() { + Some(prefix) => self.checked_with_prefix(prefix), + // Do not check validity of the address and return as is. + None => Ok(CashAddress { + hrp: String::default(), + ty: self.ty, + key_hash: self.key_hash, + address_str: self.parts.to_string(), + }), + } + } +} + +impl FromStr for UncheckedCashAddress { + type Err = AddressError; + + fn from_str(address_str: &str) -> Result { + let address_parts = CashAddressParts::from_str(&address_str)?; + + let payload_with_checksum = + cash_base32::decode(&address_parts.payload).map_err(|_| AddressError::InvalidInput)?; + + // Get the payload without the checksum. + let payload_u5 = &payload_with_checksum[..payload_with_checksum.len() - CHECKSUM_LEN]; + + let payload_data = { + let from = 5; + let to = 8; + let pad = false; + bech32::convert_bits(payload_u5, from, to, pad) + .map_err(|_| AddressError::InvalidInput)? + }; + + let version_byte = payload_data[0]; + let ty = get_address_type(version_byte)?; + let key_hash = + H160::try_from(&payload_data[1..]).map_err(|_| AddressError::InvalidInput)?; + + // `encoded_payload` is checked already, and it contains the valid checksum at the end. + Ok(UncheckedCashAddress { + parts: address_parts, + ty, + key_hash, + unchecked_payload_with_checksum: payload_with_checksum, + }) + } +} + +fn get_address_type(version_byte: u8) -> AddressResult { + match version_byte & 8 { + 0 => Ok(CashAddressType::P2PKH), + 8 => Ok(CashAddressType::P2SH), + _ => Err(AddressError::UnexpectedAddressPrefix), + } +} diff --git a/rust/chains/tw_bitcoincash/src/entry.rs b/rust/chains/tw_bitcoincash/src/entry.rs index fe357238297..946233c729d 100644 --- a/rust/chains/tw_bitcoincash/src/entry.rs +++ b/rust/chains/tw_bitcoincash/src/entry.rs @@ -3,6 +3,7 @@ // Copyright © 2017 Trust Wallet. use crate::address::Address; +use crate::cash_address::CashAddress; use tw_bitcoin::modules::compiler::BitcoinCompiler; use tw_bitcoin::modules::planner::BitcoinPlanner; use tw_bitcoin::modules::signer::BitcoinSigner; @@ -51,20 +52,24 @@ impl CoinEntry for BitcoinCashEntry { fn parse_address_unchecked( &self, _coin: &dyn CoinContext, - _address: &str, + address: &str, ) -> AddressResult { - todo!() + Address::from_str_unchecked(address) } #[inline] fn derive_address( &self, - _coin: &dyn CoinContext, - _public_key: PublicKey, + coin: &dyn CoinContext, + public_key: PublicKey, _derivation: Derivation, _prefix: Option, ) -> AddressResult { - todo!() + let public_key = public_key + .to_secp256k1() + .ok_or(AddressError::PublicKeyTypeMismatch)?; + let cash_addr = CashAddress::p2pkh_with_coin(coin, public_key)?; + Ok(Address::Cash(cash_addr)) } #[inline] diff --git a/rust/tw_tests/tests/chains/bitcoincash/bitcoincash_address.rs b/rust/tw_tests/tests/chains/bitcoincash/bitcoincash_address.rs index d94de0d342c..350966398b8 100644 --- a/rust/tw_tests/tests/chains/bitcoincash/bitcoincash_address.rs +++ b/rust/tw_tests/tests/chains/bitcoincash/bitcoincash_address.rs @@ -10,25 +10,92 @@ use tw_coin_registry::coin_type::CoinType; #[test] fn test_bitcoincash_address_derive() { - test_address_derive(CoinType::BitcoinCash, "PRIVATE_KEY", "EXPECTED ADDRESS"); + test_address_derive( + CoinType::BitcoinCash, + "28071bf4e2b0340db41b807ed8a5514139e5d6427ff9d58dbd22b7ed187103a4", + "bitcoincash:qruxj7zq6yzpdx8dld0e9hfvt7u47zrw9gfr5hy0vh", + ); } #[test] fn test_bitcoincash_address_normalization() { - test_address_normalization(CoinType::BitcoinCash, "DENORMALIZED", "EXPECTED"); + test_address_normalization( + CoinType::BitcoinCash, + "BitCoinCash:QRPLWYX7KUEQKRH6DMD3FCLTA6U32HAFP5TNPKCHX2", + "bitcoincash:qrplwyx7kueqkrh6dmd3fclta6u32hafp5tnpkchx2", + ); + // Already normalized. + test_address_normalization( + CoinType::BitcoinCash, + "bitcoincash:qruxj7zq6yzpdx8dld0e9hfvt7u47zrw9gfr5hy0vh", + "bitcoincash:qruxj7zq6yzpdx8dld0e9hfvt7u47zrw9gfr5hy0vh", + ); } #[test] fn test_bitcoincash_address_is_valid() { - test_address_valid(CoinType::BitcoinCash, "VALID ADDRESS"); + test_address_valid( + CoinType::BitcoinCash, + "bitcoincash:qr6m7j9njldwwzlg9v7v53unlr4jkmx6eylep8ekg2", + ); + test_address_valid( + CoinType::BitcoinCash, + "bitcoincash:qqa2qx0d8tegw32xk8u75ws055en4x3h2u0e6k46y4", + ); + test_address_valid( + CoinType::BitcoinCash, + "bitcoincash:pqx578nanz2h2estzmkr53zqdg6qt8xyqvwhn6qeyc", + ); + test_address_valid( + CoinType::BitcoinCash, + "pqx578nanz2h2estzmkr53zqdg6qt8xyqvwhn6qeyc", + ); + test_address_valid( + CoinType::BitcoinCash, + "bitcoincash:qr6m7j9njldwwzlg9v7v53unlr4jkmx6eylep8ekg2", + ); } #[test] fn test_bitcoincash_address_invalid() { - test_address_invalid(CoinType::BitcoinCash, "INVALID ADDRESS"); + // Wrong checksum + test_address_invalid( + CoinType::BitcoinCash, + "pqx578nanz2h2estzmkr53zqdg6qt8xyqvffffffff", + ); + test_address_invalid( + CoinType::BitcoinCash, + "bitcoincash:pqx578nanz2h2estzmkr53zqdg6qt8xyqvffffffff", + ); + + // Valid eCash addresses are invalid for BCH + test_address_invalid( + CoinType::BitcoinCash, + "pqx578nanz2h2estzmkr53zqdg6qt8xyqvh683mrz0", + ); + test_address_invalid( + CoinType::BitcoinCash, + "ecash:pqx578nanz2h2estzmkr53zqdg6qt8xyqvh683mrz0", + ); + + // Wrong prefix + test_address_invalid( + CoinType::BitcoinCash, + "bcash:pqx578nanz2h2estzmkr53zqdg6qt8xyqvwhn6qeyc", + ); + + // Wrong base 32 (characters o, i) + test_address_invalid( + CoinType::BitcoinCash, + "poi578nanz2h2estzmkr53zqdg6qt8xyqvwhn6qeyc", + ); } #[test] fn test_bitcoincash_address_get_data() { - test_address_get_data(CoinType::BitcoinCash, "ADDRESS", "HEX(DATA)"); + test_address_get_data( + CoinType::BitcoinCash, + "bitcoincash:qpk05r5kcd8uuzwqunn8rlx5xvuvzjqju5rch3tc0u", + "6cfa0e96c34fce09c0e4e671fcd43338c14812e5", + ); } From d03861294111df95972f3640e71f5d266ee4cd69 Mon Sep 17 00:00:00 2001 From: Satoshi Otomakan Date: Wed, 2 Oct 2024 23:01:29 +0200 Subject: [PATCH 04/12] feat(bch): Add `UtxoContext` to allow custom Address types --- rust/chains/tw_bitcoin/src/context.rs | 28 ++++++ rust/chains/tw_bitcoin/src/entry.rs | 11 ++- rust/chains/tw_bitcoin/src/lib.rs | 1 + .../chains/tw_bitcoin/src/modules/compiler.rs | 18 ++-- .../tw_bitcoin/src/modules/planner/mod.rs | 15 ++- .../src/modules/planner/psbt_planner.rs | 12 ++- .../src/modules/psbt_request/mod.rs | 15 ++- rust/chains/tw_bitcoin/src/modules/signer.rs | 13 ++- .../src/modules/signing_request/mod.rs | 16 ++-- .../tw_bitcoin/src/modules/tx_builder/mod.rs | 11 +++ .../src/modules/tx_builder/output_protobuf.rs | 79 ++++----------- .../src/modules/tx_builder/utxo_protobuf.rs | 95 +++---------------- rust/chains/tw_bitcoincash/src/address.rs | 6 +- rust/chains/tw_bitcoincash/src/context.rs | 30 ++++++ rust/chains/tw_bitcoincash/src/entry.rs | 12 ++- rust/chains/tw_bitcoincash/src/lib.rs | 3 +- rust/frameworks/tw_utxo/src/address/legacy.rs | 18 +++- rust/frameworks/tw_utxo/src/address/segwit.rs | 25 ++++- .../tw_utxo/src/address/standard_bitcoin.rs | 2 +- .../frameworks/tw_utxo/src/address/taproot.rs | 13 ++- .../tw_utxo/src/address/witness_program.rs | 2 +- rust/frameworks/tw_utxo/src/context.rs | 21 ++++ rust/frameworks/tw_utxo/src/lib.rs | 1 + rust/frameworks/tw_utxo/src/script/mod.rs | 6 ++ 24 files changed, 265 insertions(+), 188 deletions(-) create mode 100644 rust/chains/tw_bitcoin/src/context.rs create mode 100644 rust/chains/tw_bitcoincash/src/context.rs create mode 100644 rust/frameworks/tw_utxo/src/context.rs diff --git a/rust/chains/tw_bitcoin/src/context.rs b/rust/chains/tw_bitcoin/src/context.rs new file mode 100644 index 00000000000..8ecdd4f8e55 --- /dev/null +++ b/rust/chains/tw_bitcoin/src/context.rs @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_coin_entry::error::prelude::SigningResult; +use tw_utxo::address::standard_bitcoin::StandardBitcoinAddress; +use tw_utxo::context::{AddressPrefixes, UtxoContext}; +use tw_utxo::script::Script; + +#[derive(Default)] +pub struct StandardBitcoinContext; + +impl UtxoContext for StandardBitcoinContext { + type Address = StandardBitcoinAddress; + + fn addr_to_script_pubkey( + addr: &Self::Address, + prefixes: AddressPrefixes, + ) -> SigningResult