Skip to content

Commit 181ca6b

Browse files
feat(zcash): Add support for TEX address (trustwallet#4527)
* feat(zcash): Add `TexAddress` in Rust * feat(zcash): Add successfully broadcasted transaction test * feat(zcash): Add successfully broadcasted transaction C++ test * feat(zcash): Fix C++ tests * feat(zcash): Reuse code in `TexAddress::isValid` * feat(zcash): Fix `TexAddress:isValid`
1 parent bb49d06 commit 181ca6b

File tree

20 files changed

+532
-15
lines changed

20 files changed

+532
-15
lines changed

registry.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1343,6 +1343,7 @@
13431343
"p2shPrefix": 189,
13441344
"publicKeyHasher": "sha256ripemd",
13451345
"base58Hasher": "sha256d",
1346+
"hrp": "tex",
13461347
"explorer": {
13471348
"url": "https://blockchair.com/zcash",
13481349
"txPath": "/transaction/",

rust/Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rust/chains/tw_zcash/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ version = "0.1.0"
44
edition = "2021"
55

66
[dependencies]
7+
bech32 = "0.9.1"
78
tw_base58_address = { path = "../../tw_base58_address" }
9+
tw_bech32_address = { path = "../../tw_bech32_address" }
810
tw_bitcoin = { path = "../../chains/tw_bitcoin" }
911
tw_coin_entry = { path = "../../tw_coin_entry" }
1012
tw_encoding = { path = "../../tw_encoding" }
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
//
3+
// Copyright © 2017 Trust Wallet.
4+
5+
use crate::t_address::TAddress;
6+
use crate::tex_address::TexAddress;
7+
use std::fmt;
8+
use std::str::FromStr;
9+
use tw_coin_entry::coin_context::CoinContext;
10+
use tw_coin_entry::coin_entry::CoinAddress;
11+
use tw_coin_entry::error::prelude::{AddressError, AddressResult};
12+
use tw_keypair::ecdsa;
13+
use tw_memory::Data;
14+
use tw_utxo::address::standard_bitcoin::StandardBitcoinPrefix;
15+
16+
pub enum ZcashAddress {
17+
T(TAddress),
18+
Tex(TexAddress),
19+
}
20+
21+
impl ZcashAddress {
22+
pub fn from_str_with_coin_and_prefix(
23+
coin: &dyn CoinContext,
24+
address_str: &str,
25+
prefix: Option<StandardBitcoinPrefix>,
26+
) -> AddressResult<Self> {
27+
// Check whether the prefix is set and specifies the legacy address prefixes.
28+
match prefix {
29+
Some(StandardBitcoinPrefix::Base58(prefix)) => {
30+
return TAddress::from_str_with_coin_and_prefix(coin, address_str, Some(prefix))
31+
.map(ZcashAddress::T);
32+
},
33+
Some(StandardBitcoinPrefix::Bech32(prefix)) => {
34+
return TexAddress::from_str_with_coin_and_prefix(coin, address_str, Some(prefix))
35+
.map(ZcashAddress::Tex);
36+
},
37+
None => (),
38+
}
39+
40+
// Otherwise, try to parse address as either Cash or Legacy.
41+
if let Ok(t) = TAddress::from_str_with_coin_and_prefix(coin, address_str, None) {
42+
return Ok(ZcashAddress::T(t));
43+
}
44+
if let Ok(tex) = TexAddress::from_str_with_coin_and_prefix(coin, address_str, None) {
45+
return Ok(ZcashAddress::Tex(tex));
46+
}
47+
Err(AddressError::InvalidInput)
48+
}
49+
50+
pub fn address_with_coin_and_prefix(
51+
coin: &dyn CoinContext,
52+
public_key: &ecdsa::secp256k1::PublicKey,
53+
prefix: Option<StandardBitcoinPrefix>,
54+
) -> AddressResult<Self> {
55+
match prefix {
56+
// Check whether the prefix is set and specifies the legacy address prefixes.
57+
Some(StandardBitcoinPrefix::Base58(prefix)) => {
58+
TAddress::p2pkh_with_public_key(prefix.p2pkh, public_key).map(ZcashAddress::T)
59+
},
60+
Some(StandardBitcoinPrefix::Bech32(prefix)) => {
61+
TexAddress::with_public_key(prefix.hrp, public_key).map(ZcashAddress::Tex)
62+
},
63+
None => {
64+
let p2pkh_prefix = coin.p2pkh_prefix().ok_or(AddressError::InvalidRegistry)?;
65+
TAddress::p2pkh_with_public_key(p2pkh_prefix, public_key).map(ZcashAddress::T)
66+
},
67+
}
68+
}
69+
}
70+
71+
impl FromStr for ZcashAddress {
72+
type Err = AddressError;
73+
74+
fn from_str(address_str: &str) -> Result<Self, Self::Err> {
75+
if let Ok(t) = TAddress::from_str(address_str) {
76+
return Ok(ZcashAddress::T(t));
77+
}
78+
if let Ok(tex) = TexAddress::from_str(address_str) {
79+
return Ok(ZcashAddress::Tex(tex));
80+
}
81+
Err(AddressError::InvalidInput)
82+
}
83+
}
84+
85+
impl CoinAddress for ZcashAddress {
86+
fn data(&self) -> Data {
87+
match self {
88+
ZcashAddress::T(t) => t.data(),
89+
ZcashAddress::Tex(tex) => tex.data(),
90+
}
91+
}
92+
}
93+
94+
impl fmt::Display for ZcashAddress {
95+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96+
match self {
97+
ZcashAddress::T(t) => write!(f, "{t}"),
98+
ZcashAddress::Tex(tex) => write!(f, "{tex}"),
99+
}
100+
}
101+
}

rust/chains/tw_zcash/src/context.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
//
33
// Copyright © 2017 Trust Wallet.
44

5+
use crate::address::ZcashAddress;
56
use crate::modules::protobuf_builder::ZcashProtobufBuilder;
67
use crate::modules::signing_request::ZcashSigningRequestBuilder;
78
use crate::modules::zcash_fee_estimator::ZcashFeeEstimator;
8-
use crate::t_address::TAddress;
99
use crate::transaction::ZcashTransaction;
1010
use tw_bitcoin::context::BitcoinSigningContext;
1111
use tw_bitcoin::modules::psbt_request::NoPsbtRequestBuilder;
@@ -17,15 +17,18 @@ use tw_utxo::script::Script;
1717
pub struct ZcashContext;
1818

1919
impl UtxoContext for ZcashContext {
20-
type Address = TAddress;
20+
type Address = ZcashAddress;
2121
type Transaction = ZcashTransaction;
2222
type FeeEstimator = ZcashFeeEstimator;
2323

2424
fn addr_to_script_pubkey(
2525
addr: &Self::Address,
2626
prefixes: AddressPrefixes,
2727
) -> SigningResult<Script> {
28-
addr.to_script_pubkey(prefixes.p2pkh_prefix, prefixes.p2sh_prefix)
28+
match addr {
29+
ZcashAddress::T(t) => t.to_script_pubkey(prefixes.p2pkh_prefix, prefixes.p2sh_prefix),
30+
ZcashAddress::Tex(tex) => tex.to_script_pubkey(),
31+
}
2932
}
3033
}
3134

rust/chains/tw_zcash/src/entry.rs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
//
33
// Copyright © 2017 Trust Wallet.
44

5+
use crate::address::ZcashAddress;
56
use crate::context::ZcashContext;
6-
use crate::t_address::TAddress;
77
use std::str::FromStr;
88
use tw_bitcoin::modules::compiler::BitcoinCompiler;
99
use tw_bitcoin::modules::planner::BitcoinPlanner;
@@ -17,15 +17,15 @@ use tw_coin_entry::modules::message_signer::NoMessageSigner;
1717
use tw_coin_entry::modules::transaction_decoder::NoTransactionDecoder;
1818
use tw_coin_entry::modules::transaction_util::NoTransactionUtil;
1919
use tw_coin_entry::modules::wallet_connector::NoWalletConnector;
20-
use tw_coin_entry::prefix::BitcoinBase58Prefix;
2120
use tw_keypair::tw::PublicKey;
2221
use tw_proto::BitcoinV2::Proto as BitcoinV2Proto;
22+
use tw_utxo::address::standard_bitcoin::StandardBitcoinPrefix;
2323

2424
pub struct ZcashEntry;
2525

2626
impl CoinEntry for ZcashEntry {
27-
type AddressPrefix = BitcoinBase58Prefix;
28-
type Address = TAddress;
27+
type AddressPrefix = StandardBitcoinPrefix;
28+
type Address = ZcashAddress;
2929
type SigningInput<'a> = BitcoinV2Proto::SigningInput<'a>;
3030
type SigningOutput = BitcoinV2Proto::SigningOutput<'static>;
3131
type PreSigningOutput = BitcoinV2Proto::PreSigningOutput<'static>;
@@ -45,12 +45,12 @@ impl CoinEntry for ZcashEntry {
4545
address: &str,
4646
prefix: Option<Self::AddressPrefix>,
4747
) -> AddressResult<Self::Address> {
48-
TAddress::from_str_with_coin_and_prefix(coin, address, prefix)
48+
ZcashAddress::from_str_with_coin_and_prefix(coin, address, prefix)
4949
}
5050

5151
#[inline]
5252
fn parse_address_unchecked(&self, address: &str) -> AddressResult<Self::Address> {
53-
TAddress::from_str(address)
53+
ZcashAddress::from_str(address)
5454
}
5555

5656
#[inline]
@@ -61,7 +61,10 @@ impl CoinEntry for ZcashEntry {
6161
_derivation: Derivation,
6262
prefix: Option<Self::AddressPrefix>,
6363
) -> AddressResult<Self::Address> {
64-
TAddress::p2pkh_with_coin_and_prefix(coin, &public_key, prefix)
64+
let public_key = public_key
65+
.to_secp256k1()
66+
.ok_or(AddressError::PublicKeyTypeMismatch)?;
67+
ZcashAddress::address_with_coin_and_prefix(coin, public_key, prefix)
6568
}
6669

6770
#[inline]

rust/chains/tw_zcash/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,10 @@
127127
//! assert.equal(output.signingResultV2!.error, TW.Common.Proto.SigningError.OK);
128128
//! ```
129129
130+
pub mod address;
130131
pub mod context;
131132
pub mod entry;
132133
pub mod modules;
133134
pub mod t_address;
135+
pub mod tex_address;
134136
pub mod transaction;

rust/chains/tw_zcash/src/t_address.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,8 @@ use tw_utxo::address::statically_prefixed_base58_address::StaticPrefixedB58Addre
88

99
/// For now, T-prefix is a constant value.
1010
pub const ZCASH_T_PREFIX: u8 = 0x1C;
11+
pub type ZcashPublicKeyHasher = Sha256Ripemd;
12+
pub type ZcashChecksumHasher = Sha256d;
1113

12-
pub type TAddress = StaticPrefixedB58Address<ZCASH_T_PREFIX, Sha256Ripemd, Sha256d>;
14+
pub type TAddress =
15+
StaticPrefixedB58Address<ZCASH_T_PREFIX, ZcashPublicKeyHasher, ZcashChecksumHasher>;
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
//
3+
// Copyright © 2017 Trust Wallet.
4+
5+
use crate::t_address::{TAddress, ZcashPublicKeyHasher};
6+
use bech32::FromBase32;
7+
use core::fmt;
8+
use std::str::FromStr;
9+
use tw_bech32_address::bech32_prefix::Bech32Prefix;
10+
use tw_coin_entry::coin_context::CoinContext;
11+
use tw_coin_entry::coin_entry::CoinAddress;
12+
use tw_coin_entry::error::prelude::{AddressError, AddressResult, SigningResult};
13+
use tw_hash::hasher::StaticHasher;
14+
use tw_hash::H160;
15+
use tw_keypair::ecdsa;
16+
use tw_memory::Data;
17+
use tw_utxo::script::standard_script::conditions;
18+
use tw_utxo::script::Script;
19+
20+
pub const BECH32_VARIANT: bech32::Variant = bech32::Variant::Bech32m;
21+
22+
/// A TEX Address, also called a Transparent-Source-Only Address, is a Bech32m reencoding of a transparent Zcash P2PKH address.
23+
pub struct TexAddress {
24+
hrp: String,
25+
bytes: H160,
26+
address_str: String,
27+
}
28+
29+
impl TexAddress {
30+
pub fn new(hrp: String, bytes: H160) -> AddressResult<TexAddress> {
31+
let address_str = Self::fmt_internal(&hrp, &bytes)?;
32+
Ok(TexAddress {
33+
hrp,
34+
bytes,
35+
address_str,
36+
})
37+
}
38+
39+
pub fn with_public_key(
40+
hrp: String,
41+
public_key: &ecdsa::secp256k1::PublicKey,
42+
) -> AddressResult<Self> {
43+
let public_key_hash = ZcashPublicKeyHasher::hash(public_key.compressed().as_slice());
44+
let bytes =
45+
H160::try_from(public_key_hash.as_slice()).map_err(|_| AddressError::InvalidInput)?;
46+
TexAddress::new(hrp, bytes)
47+
}
48+
49+
pub fn from_t_address_with_hrp(t_address: &TAddress, hrp: String) -> AddressResult<TexAddress> {
50+
let bytes = t_address.payload();
51+
TexAddress::new(hrp, bytes)
52+
}
53+
54+
pub fn from_str_with_coin_and_prefix(
55+
coin: &dyn CoinContext,
56+
s: &str,
57+
prefix: Option<Bech32Prefix>,
58+
) -> AddressResult<TexAddress> {
59+
let hrp = match prefix {
60+
Some(Bech32Prefix { hrp }) => hrp,
61+
None => coin.hrp().ok_or(AddressError::InvalidRegistry)?,
62+
};
63+
TexAddress::from_str_checked(s, &hrp)
64+
}
65+
66+
pub fn from_str_checked(s: &str, expected_hrp: &str) -> AddressResult<TexAddress> {
67+
let address = Self::from_str(s)?;
68+
if address.hrp != expected_hrp {
69+
return Err(AddressError::InvalidHrp);
70+
}
71+
Ok(address)
72+
}
73+
74+
pub fn to_script_pubkey(&self) -> SigningResult<Script> {
75+
Ok(conditions::new_p2pkh(&self.bytes))
76+
}
77+
78+
pub fn to_t_address(&self, p2pkh_prefix: u8) -> AddressResult<TAddress> {
79+
TAddress::new(p2pkh_prefix, self.bytes.as_slice())
80+
}
81+
82+
fn fmt_internal(hrp: &str, bytes: &H160) -> AddressResult<String> {
83+
const STRING_CAPACITY: usize = 100;
84+
85+
let mut result_addr = String::with_capacity(STRING_CAPACITY);
86+
87+
{
88+
let mut bech32_writer =
89+
bech32::Bech32Writer::new(hrp, BECH32_VARIANT, &mut result_addr)
90+
.map_err(|_| AddressError::FromBech32Error)?;
91+
bech32::ToBase32::write_base32(&bytes, &mut bech32_writer)
92+
.map_err(|_| AddressError::FromBech32Error)?;
93+
}
94+
95+
Ok(result_addr)
96+
}
97+
}
98+
99+
impl fmt::Display for TexAddress {
100+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101+
write!(f, "{}", self.address_str)
102+
}
103+
}
104+
105+
impl FromStr for TexAddress {
106+
type Err = AddressError;
107+
108+
fn from_str(s: &str) -> Result<Self, Self::Err> {
109+
let (hrp, payload_u5, checksum_variant) =
110+
bech32::decode(s).map_err(|_| AddressError::FromBech32Error)?;
111+
112+
if checksum_variant != BECH32_VARIANT {
113+
return Err(AddressError::FromBech32Error);
114+
}
115+
let payload = Data::from_base32(&payload_u5).map_err(|_| AddressError::FromBech32Error)?;
116+
let bytes =
117+
H160::try_from(payload.as_slice()).map_err(|_| AddressError::FromBech32Error)?;
118+
TexAddress::new(hrp, bytes)
119+
}
120+
}
121+
122+
impl CoinAddress for TexAddress {
123+
fn data(&self) -> Data {
124+
self.bytes.to_vec()
125+
}
126+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
//
3+
// Copyright © 2017 Trust Wallet.
4+
5+
use std::str::FromStr;
6+
use tw_zcash::tex_address::TexAddress;
7+
8+
/// https://zips.z.cash/zip-0320#reference-implementation
9+
#[test]
10+
fn test_from_to_t_address() {
11+
const TEX_ADDRESS: &str = "tex1s2rt77ggv6q989lr49rkgzmh5slsksa9khdgte";
12+
const T_ADDRESS: &str = "t1VmmGiyjVNeCjxDZzg7vZmd99WyzVby9yC";
13+
const TEX_HRP: &str = "tex";
14+
15+
let tex_address = TexAddress::from_str(TEX_ADDRESS).unwrap();
16+
let t_address = tex_address.to_t_address(184).unwrap();
17+
assert_eq!(t_address.to_string(), T_ADDRESS);
18+
let parsed_tex_address =
19+
TexAddress::from_t_address_with_hrp(&t_address, TEX_HRP.to_string()).unwrap();
20+
assert_eq!(parsed_tex_address.to_string(), TEX_ADDRESS);
21+
}

0 commit comments

Comments
 (0)