Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -1343,6 +1343,7 @@
"p2shPrefix": 189,
"publicKeyHasher": "sha256ripemd",
"base58Hasher": "sha256d",
"hrp": "tex",
"explorer": {
"url": "https://blockchair.com/zcash",
"txPath": "/transaction/",
Expand Down
2 changes: 2 additions & 0 deletions rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions rust/chains/tw_zcash/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ version = "0.1.0"
edition = "2021"

[dependencies]
bech32 = "0.9.1"
tw_base58_address = { path = "../../tw_base58_address" }
tw_bech32_address = { path = "../../tw_bech32_address" }
tw_bitcoin = { path = "../../chains/tw_bitcoin" }
tw_coin_entry = { path = "../../tw_coin_entry" }
tw_encoding = { path = "../../tw_encoding" }
Expand Down
101 changes: 101 additions & 0 deletions rust/chains/tw_zcash/src/address.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright © 2017 Trust Wallet.

use crate::t_address::TAddress;
use crate::tex_address::TexAddress;
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::{AddressError, AddressResult};
use tw_keypair::ecdsa;
use tw_memory::Data;
use tw_utxo::address::standard_bitcoin::StandardBitcoinPrefix;

pub enum ZcashAddress {
T(TAddress),
Tex(TexAddress),
}

impl ZcashAddress {
pub fn from_str_with_coin_and_prefix(
coin: &dyn CoinContext,
address_str: &str,
prefix: Option<StandardBitcoinPrefix>,
) -> AddressResult<Self> {
// Check whether the prefix is set and specifies the legacy address prefixes.
match prefix {
Some(StandardBitcoinPrefix::Base58(prefix)) => {
return TAddress::from_str_with_coin_and_prefix(coin, address_str, Some(prefix))
.map(ZcashAddress::T);
},
Some(StandardBitcoinPrefix::Bech32(prefix)) => {
return TexAddress::from_str_with_coin_and_prefix(coin, address_str, Some(prefix))
.map(ZcashAddress::Tex);
},
None => (),
}

// Otherwise, try to parse address as either Cash or Legacy.
if let Ok(t) = TAddress::from_str_with_coin_and_prefix(coin, address_str, None) {
return Ok(ZcashAddress::T(t));
}
if let Ok(tex) = TexAddress::from_str_with_coin_and_prefix(coin, address_str, None) {
return Ok(ZcashAddress::Tex(tex));
}
Err(AddressError::InvalidInput)
}

pub fn address_with_coin_and_prefix(
coin: &dyn CoinContext,
public_key: &ecdsa::secp256k1::PublicKey,
prefix: Option<StandardBitcoinPrefix>,
) -> AddressResult<Self> {
match prefix {
// Check whether the prefix is set and specifies the legacy address prefixes.
Some(StandardBitcoinPrefix::Base58(prefix)) => {
TAddress::p2pkh_with_public_key(prefix.p2pkh, public_key).map(ZcashAddress::T)
},
Some(StandardBitcoinPrefix::Bech32(prefix)) => {
TexAddress::with_public_key(prefix.hrp, public_key).map(ZcashAddress::Tex)
},
None => {
let p2pkh_prefix = coin.p2pkh_prefix().ok_or(AddressError::InvalidRegistry)?;
TAddress::p2pkh_with_public_key(p2pkh_prefix, public_key).map(ZcashAddress::T)
},
}
}
}

impl FromStr for ZcashAddress {
type Err = AddressError;

fn from_str(address_str: &str) -> Result<Self, Self::Err> {
if let Ok(t) = TAddress::from_str(address_str) {
return Ok(ZcashAddress::T(t));
}
if let Ok(tex) = TexAddress::from_str(address_str) {
return Ok(ZcashAddress::Tex(tex));
}
Err(AddressError::InvalidInput)
}
}

impl CoinAddress for ZcashAddress {
fn data(&self) -> Data {
match self {
ZcashAddress::T(t) => t.data(),
ZcashAddress::Tex(tex) => tex.data(),
}
}
}

impl fmt::Display for ZcashAddress {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ZcashAddress::T(t) => write!(f, "{t}"),
ZcashAddress::Tex(tex) => write!(f, "{tex}"),
}
}
}
9 changes: 6 additions & 3 deletions rust/chains/tw_zcash/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
//
// Copyright © 2017 Trust Wallet.

use crate::address::ZcashAddress;
use crate::modules::protobuf_builder::ZcashProtobufBuilder;
use crate::modules::signing_request::ZcashSigningRequestBuilder;
use crate::modules::zcash_fee_estimator::ZcashFeeEstimator;
use crate::t_address::TAddress;
use crate::transaction::ZcashTransaction;
use tw_bitcoin::context::BitcoinSigningContext;
use tw_bitcoin::modules::psbt_request::NoPsbtRequestBuilder;
Expand All @@ -17,15 +17,18 @@ use tw_utxo::script::Script;
pub struct ZcashContext;

impl UtxoContext for ZcashContext {
type Address = TAddress;
type Address = ZcashAddress;
type Transaction = ZcashTransaction;
type FeeEstimator = ZcashFeeEstimator;

fn addr_to_script_pubkey(
addr: &Self::Address,
prefixes: AddressPrefixes,
) -> SigningResult<Script> {
addr.to_script_pubkey(prefixes.p2pkh_prefix, prefixes.p2sh_prefix)
match addr {
ZcashAddress::T(t) => t.to_script_pubkey(prefixes.p2pkh_prefix, prefixes.p2sh_prefix),
ZcashAddress::Tex(tex) => tex.to_script_pubkey(),
}
}
}

Expand Down
17 changes: 10 additions & 7 deletions rust/chains/tw_zcash/src/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
//
// Copyright © 2017 Trust Wallet.

use crate::address::ZcashAddress;
use crate::context::ZcashContext;
use crate::t_address::TAddress;
use std::str::FromStr;
use tw_bitcoin::modules::compiler::BitcoinCompiler;
use tw_bitcoin::modules::planner::BitcoinPlanner;
Expand All @@ -17,15 +17,15 @@ use tw_coin_entry::modules::message_signer::NoMessageSigner;
use tw_coin_entry::modules::transaction_decoder::NoTransactionDecoder;
use tw_coin_entry::modules::transaction_util::NoTransactionUtil;
use tw_coin_entry::modules::wallet_connector::NoWalletConnector;
use tw_coin_entry::prefix::BitcoinBase58Prefix;
use tw_keypair::tw::PublicKey;
use tw_proto::BitcoinV2::Proto as BitcoinV2Proto;
use tw_utxo::address::standard_bitcoin::StandardBitcoinPrefix;

pub struct ZcashEntry;

impl CoinEntry for ZcashEntry {
type AddressPrefix = BitcoinBase58Prefix;
type Address = TAddress;
type AddressPrefix = StandardBitcoinPrefix;
type Address = ZcashAddress;
type SigningInput<'a> = BitcoinV2Proto::SigningInput<'a>;
type SigningOutput = BitcoinV2Proto::SigningOutput<'static>;
type PreSigningOutput = BitcoinV2Proto::PreSigningOutput<'static>;
Expand All @@ -45,12 +45,12 @@ impl CoinEntry for ZcashEntry {
address: &str,
prefix: Option<Self::AddressPrefix>,
) -> AddressResult<Self::Address> {
TAddress::from_str_with_coin_and_prefix(coin, address, prefix)
ZcashAddress::from_str_with_coin_and_prefix(coin, address, prefix)
}

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

#[inline]
Expand All @@ -61,7 +61,10 @@ impl CoinEntry for ZcashEntry {
_derivation: Derivation,
prefix: Option<Self::AddressPrefix>,
) -> AddressResult<Self::Address> {
TAddress::p2pkh_with_coin_and_prefix(coin, &public_key, prefix)
let public_key = public_key
.to_secp256k1()
.ok_or(AddressError::PublicKeyTypeMismatch)?;
ZcashAddress::address_with_coin_and_prefix(coin, public_key, prefix)
}

#[inline]
Expand Down
2 changes: 2 additions & 0 deletions rust/chains/tw_zcash/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,10 @@
//! assert.equal(output.signingResultV2!.error, TW.Common.Proto.SigningError.OK);
//! ```

pub mod address;
pub mod context;
pub mod entry;
pub mod modules;
pub mod t_address;
pub mod tex_address;
pub mod transaction;
5 changes: 4 additions & 1 deletion rust/chains/tw_zcash/src/t_address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,8 @@ use tw_utxo::address::statically_prefixed_base58_address::StaticPrefixedB58Addre

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

pub type TAddress = StaticPrefixedB58Address<ZCASH_T_PREFIX, Sha256Ripemd, Sha256d>;
pub type TAddress =
StaticPrefixedB58Address<ZCASH_T_PREFIX, ZcashPublicKeyHasher, ZcashChecksumHasher>;
126 changes: 126 additions & 0 deletions rust/chains/tw_zcash/src/tex_address.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright © 2017 Trust Wallet.

use crate::t_address::{TAddress, ZcashPublicKeyHasher};
use bech32::FromBase32;
use core::fmt;
use std::str::FromStr;
use tw_bech32_address::bech32_prefix::Bech32Prefix;
use tw_coin_entry::coin_context::CoinContext;
use tw_coin_entry::coin_entry::CoinAddress;
use tw_coin_entry::error::prelude::{AddressError, AddressResult, SigningResult};
use tw_hash::hasher::StaticHasher;
use tw_hash::H160;
use tw_keypair::ecdsa;
use tw_memory::Data;
use tw_utxo::script::standard_script::conditions;
use tw_utxo::script::Script;

pub const BECH32_VARIANT: bech32::Variant = bech32::Variant::Bech32m;

/// A TEX Address, also called a Transparent-Source-Only Address, is a Bech32m reencoding of a transparent Zcash P2PKH address.
pub struct TexAddress {
hrp: String,
bytes: H160,
address_str: String,
}

impl TexAddress {
pub fn new(hrp: String, bytes: H160) -> AddressResult<TexAddress> {
let address_str = Self::fmt_internal(&hrp, &bytes)?;
Ok(TexAddress {
hrp,
bytes,
address_str,
})
}

pub fn with_public_key(
hrp: String,
public_key: &ecdsa::secp256k1::PublicKey,
) -> AddressResult<Self> {
let public_key_hash = ZcashPublicKeyHasher::hash(public_key.compressed().as_slice());
let bytes =
H160::try_from(public_key_hash.as_slice()).map_err(|_| AddressError::InvalidInput)?;
TexAddress::new(hrp, bytes)
}

pub fn from_t_address_with_hrp(t_address: &TAddress, hrp: String) -> AddressResult<TexAddress> {
let bytes = t_address.payload();
TexAddress::new(hrp, bytes)
}

pub fn from_str_with_coin_and_prefix(
coin: &dyn CoinContext,
s: &str,
prefix: Option<Bech32Prefix>,
) -> AddressResult<TexAddress> {
let hrp = match prefix {
Some(Bech32Prefix { hrp }) => hrp,
None => coin.hrp().ok_or(AddressError::InvalidRegistry)?,
};
TexAddress::from_str_checked(s, &hrp)
}

pub fn from_str_checked(s: &str, expected_hrp: &str) -> AddressResult<TexAddress> {
let address = Self::from_str(s)?;
if address.hrp != expected_hrp {
return Err(AddressError::InvalidHrp);
}
Ok(address)
}

pub fn to_script_pubkey(&self) -> SigningResult<Script> {
Ok(conditions::new_p2pkh(&self.bytes))
}

pub fn to_t_address(&self, p2pkh_prefix: u8) -> AddressResult<TAddress> {
TAddress::new(p2pkh_prefix, self.bytes.as_slice())
}

fn fmt_internal(hrp: &str, bytes: &H160) -> AddressResult<String> {
const STRING_CAPACITY: usize = 100;

let mut result_addr = String::with_capacity(STRING_CAPACITY);

{
let mut bech32_writer =
bech32::Bech32Writer::new(hrp, BECH32_VARIANT, &mut result_addr)
.map_err(|_| AddressError::FromBech32Error)?;
bech32::ToBase32::write_base32(&bytes, &mut bech32_writer)
.map_err(|_| AddressError::FromBech32Error)?;
}

Ok(result_addr)
}
}

impl fmt::Display for TexAddress {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.address_str)
}
}

impl FromStr for TexAddress {
type Err = AddressError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let (hrp, payload_u5, checksum_variant) =
bech32::decode(s).map_err(|_| AddressError::FromBech32Error)?;

if checksum_variant != BECH32_VARIANT {
return Err(AddressError::FromBech32Error);
}
let payload = Data::from_base32(&payload_u5).map_err(|_| AddressError::FromBech32Error)?;
let bytes =
H160::try_from(payload.as_slice()).map_err(|_| AddressError::FromBech32Error)?;
TexAddress::new(hrp, bytes)
}
}

impl CoinAddress for TexAddress {
fn data(&self) -> Data {
self.bytes.to_vec()
}
}
21 changes: 21 additions & 0 deletions rust/chains/tw_zcash/tests/tex_address.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright © 2017 Trust Wallet.

use std::str::FromStr;
use tw_zcash::tex_address::TexAddress;

/// https://zips.z.cash/zip-0320#reference-implementation
#[test]
fn test_from_to_t_address() {
const TEX_ADDRESS: &str = "tex1s2rt77ggv6q989lr49rkgzmh5slsksa9khdgte";
const T_ADDRESS: &str = "t1VmmGiyjVNeCjxDZzg7vZmd99WyzVby9yC";
const TEX_HRP: &str = "tex";

let tex_address = TexAddress::from_str(TEX_ADDRESS).unwrap();
let t_address = tex_address.to_t_address(184).unwrap();
assert_eq!(t_address.to_string(), T_ADDRESS);
let parsed_tex_address =
TexAddress::from_t_address_with_hrp(&t_address, TEX_HRP.to_string()).unwrap();
assert_eq!(parsed_tex_address.to_string(), TEX_ADDRESS);
}
Loading
Loading