use core::fmt;
use core::marker::PhantomData;
use core::str::FromStr;
use hashes::Hash;
use internals::write_err;
use super::{Address, AddressType, NetworkChecked, NetworkUnchecked, NetworkValidation};
use crate::blockdata::constants::MAX_SCRIPT_ELEMENT_SIZE;
use crate::blockdata::script::{Script, ScriptBuf, ScriptHash, ScriptHash32};
use crate::crypto::key::PubkeyHash;
use crate::network::{Network, NetworkKind};
use crate::prelude::*;
const CHARSET: [u8; 32] = *b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
const CHARSET_REV: [i8; 128] = {
let mut rev = [-1i8; 128];
let mut i = 0;
while i < 32 {
rev[CHARSET[i] as usize] = i as i8;
rev[CHARSET[i].to_ascii_uppercase() as usize] = i as i8;
i += 1;
}
rev
};
const HASH_SIZES: [usize; 8] = [20, 24, 28, 32, 40, 48, 56, 64];
const TYPE_P2PKH: u8 = 0;
const TYPE_P2SH: u8 = 1;
const TYPE_P2PKH_TOKEN: u8 = 2;
const TYPE_P2SH_TOKEN: u8 = 3;
fn polymod_step(c: u64, d: u8) -> u64 {
let c0 = (c >> 35) as u8;
let mut c = ((c & 0x0007_ffff_ffff) << 5) ^ u64::from(d);
if c0 & 0x01 != 0 {
c ^= 0x0098_f2bc_8e61;
}
if c0 & 0x02 != 0 {
c ^= 0x0079_b76d_99e2;
}
if c0 & 0x04 != 0 {
c ^= 0x00f3_3e5f_b3c4;
}
if c0 & 0x08 != 0 {
c ^= 0x00ae_2eab_e2a8;
}
if c0 & 0x10 != 0 {
c ^= 0x001e_4f43_e470;
}
c
}
fn polymod_prefix(mut c: u64, prefix: &str) -> u64 {
for b in prefix.bytes() {
c = polymod_step(c, b & 0x1f);
}
polymod_step(c, 0)
}
fn char_value(c: char) -> Option<u8> {
let v = *CHARSET_REV.get(c as usize)?;
u8::try_from(v).ok()
}
fn check_case(s: &str) -> Result<(), DecodeError> {
let mut lower = false;
let mut upper = false;
for c in s.chars() {
lower |= c.is_ascii_lowercase();
upper |= c.is_ascii_uppercase();
}
if lower && upper {
Err(DecodeError::MixedCase)
} else {
Ok(())
}
}
fn check_prefix(prefix: &str) -> Result<(), EncodeError> {
if prefix.is_empty() || !prefix.bytes().all(|b| b.is_ascii_lowercase()) {
return Err(EncodeError::InvalidPrefix);
}
Ok(())
}
fn version_byte(type_bits: u8, len: usize) -> Result<u8, EncodeError> {
if type_bits > 15 {
return Err(EncodeError::InvalidTypeBits(type_bits));
}
let size_bits = HASH_SIZES
.iter()
.position(|&size| size == len)
.ok_or(EncodeError::InvalidPayloadLength(len))?;
Ok((type_bits << 3) | size_bits as u8)
}
fn encode_to_fmt(
f: &mut dyn fmt::Write,
prefix: &str,
version_byte: u8,
data: &[u8],
uppercase: bool,
) -> fmt::Result {
let write_value = |f: &mut dyn fmt::Write, v: u8| {
let c = CHARSET[usize::from(v)];
f.write_char(char::from(if uppercase { c.to_ascii_uppercase() } else { c }))
};
let mut chk = polymod_prefix(1, prefix);
if uppercase {
for c in prefix.chars() {
f.write_char(c.to_ascii_uppercase())?;
}
} else {
f.write_str(prefix)?;
}
f.write_char(':')?;
let mut acc = 0u32;
let mut bits = 0u32;
for &b in core::iter::once(&version_byte).chain(data) {
acc = (acc << 8) | u32::from(b);
bits += 8;
while bits >= 5 {
bits -= 5;
let v = ((acc >> bits) & 0x1f) as u8;
chk = polymod_step(chk, v);
write_value(f, v)?;
}
acc &= (1 << bits) - 1;
}
if bits > 0 {
let v = ((acc << (5 - bits)) & 0x1f) as u8;
chk = polymod_step(chk, v);
write_value(f, v)?;
}
for _ in 0..8 {
chk = polymod_step(chk, 0);
}
let chk = chk ^ 1;
for i in (0..8).rev() {
write_value(f, ((chk >> (5 * i)) & 0x1f) as u8)?;
}
Ok(())
}
pub fn encode(prefix: &str, type_bits: u8, data: &[u8]) -> Result<String, EncodeError> {
check_prefix(prefix)?;
let version_byte = version_byte(type_bits, data.len())?;
let payload_len = ((1 + data.len()) * 8 + 4) / 5; let mut s = String::with_capacity(prefix.len() + 1 + payload_len + 8);
encode_to_fmt(&mut s, prefix, version_byte, data, false)
.expect("writing to string is infallible");
Ok(s)
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub struct DecodedCashAddr {
pub prefix: String,
pub type_bits: u8,
pub data: Vec<u8>,
}
pub fn decode(addr: &str) -> Result<DecodedCashAddr, DecodeError> {
let (prefix, body) = addr.split_once(':').ok_or(DecodeError::MissingPrefix)?;
check_case(addr)?;
let prefix = prefix.to_ascii_lowercase();
if check_prefix(&prefix).is_err() {
return Err(match prefix.chars().find(|c| !c.is_ascii_lowercase()) {
Some(c) => DecodeError::InvalidChar(c),
None => DecodeError::InvalidPrefix,
});
}
let (type_bits, data) = decode_body(&prefix, body)?;
Ok(DecodedCashAddr { prefix, type_bits, data })
}
pub fn decode_with_prefix(prefix: &str, body: &str) -> Result<DecodedCashAddr, DecodeError> {
if check_prefix(prefix).is_err() {
return Err(match prefix.chars().find(|c| !c.is_ascii_lowercase()) {
Some(c) => DecodeError::InvalidChar(c),
None => DecodeError::InvalidPrefix,
});
}
check_case(body)?;
let (type_bits, data) = decode_body(prefix, body)?;
Ok(DecodedCashAddr { prefix: prefix.to_owned(), type_bits, data })
}
fn decode_body(prefix: &str, body: &str) -> Result<(u8, Vec<u8>), DecodeError> {
if body.len() < 10 {
return Err(DecodeError::InvalidLength(body.len()));
}
let mut values = Vec::with_capacity(body.len());
for c in body.chars() {
values.push(char_value(c).ok_or(DecodeError::InvalidChar(c))?);
}
let mut chk = polymod_prefix(1, prefix);
for &v in &values {
chk = polymod_step(chk, v);
}
if chk != 1 {
return Err(DecodeError::Checksum);
}
let payload = &values[..values.len() - 8];
let mut data = Vec::with_capacity(payload.len() * 5 / 8);
let mut acc = 0u32;
let mut bits = 0u32;
for &v in payload {
acc = (acc << 5) | u32::from(v);
bits += 5;
if bits >= 8 {
bits -= 8;
data.push((acc >> bits) as u8);
acc &= (1 << bits) - 1;
}
}
if bits >= 5 || acc != 0 {
return Err(DecodeError::InvalidPadding);
}
let version_byte = data.remove(0);
if version_byte & 0x80 != 0 {
return Err(DecodeError::ReservedBitSet);
}
let expected = HASH_SIZES[usize::from(version_byte & 0x07)];
if data.len() != expected {
return Err(DecodeError::InvalidLength(data.len()));
}
Ok((version_byte >> 3, data))
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum KnownPrefix {
Mainnet,
Testnets,
Regtest,
}
impl KnownPrefix {
fn from_network(network: Network) -> Self {
use Network::*;
match network {
Bitcoin => Self::Mainnet,
Testnet | Testnet4 | Scalenet | Chipnet => Self::Testnets,
Regtest => Self::Regtest,
}
}
fn from_prefix_str(prefix: &str) -> Option<Self> {
if prefix.eq_ignore_ascii_case("bitcoincash") {
Some(Self::Mainnet)
} else if prefix.eq_ignore_ascii_case("bchtest") {
Some(Self::Testnets)
} else if prefix.eq_ignore_ascii_case("bchreg") {
Some(Self::Regtest)
} else {
None
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::Mainnet => "bitcoincash",
Self::Testnets => "bchtest",
Self::Regtest => "bchreg",
}
}
}
impl From<Network> for KnownPrefix {
fn from(n: Network) -> Self { Self::from_network(n) }
}
impl From<NetworkKind> for KnownPrefix {
fn from(n: NetworkKind) -> Self {
match n {
NetworkKind::Main => Self::Mainnet,
NetworkKind::Test => Self::Testnets,
}
}
}
impl fmt::Display for KnownPrefix {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str(self.as_str()) }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum CashPayload {
P2pkh(PubkeyHash),
P2sh(ScriptHash),
P2sh32(ScriptHash32),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct CashAddressInner {
payload: CashPayload,
token_aware: bool,
prefix: KnownPrefix,
}
impl CashAddressInner {
fn type_bits(&self) -> u8 {
match (&self.payload, self.token_aware) {
(CashPayload::P2pkh(_), false) => TYPE_P2PKH,
(CashPayload::P2pkh(_), true) => TYPE_P2PKH_TOKEN,
(CashPayload::P2sh(_) | CashPayload::P2sh32(_), false) => TYPE_P2SH,
(CashPayload::P2sh(_) | CashPayload::P2sh32(_), true) => TYPE_P2SH_TOKEN,
}
}
fn data(&self) -> &[u8] {
match &self.payload {
CashPayload::P2pkh(hash) => hash.as_ref(),
CashPayload::P2sh(hash) => hash.as_ref(),
CashPayload::P2sh32(hash) => hash.as_ref(),
}
}
}
impl fmt::Display for CashAddressInner {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let data = self.data();
let version_byte =
version_byte(self.type_bits(), data.len()).expect("payload sizes are always valid");
let uppercase = f.alternate();
encode_to_fmt(f, self.prefix.as_str(), version_byte, data, uppercase)
}
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(transparent)]
pub struct CashAddress<V = NetworkChecked>(CashAddressInner, PhantomData<V>)
where
V: NetworkValidation;
#[cfg(feature = "serde")]
struct DisplayUnchecked<'a, N: NetworkValidation>(&'a CashAddress<N>);
#[cfg(feature = "serde")]
impl<N: NetworkValidation> fmt::Display for DisplayUnchecked<'_, N> {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0 .0, fmt) }
}
#[cfg(feature = "serde")]
crate::serde_utils::serde_string_deserialize_impl!(
CashAddress<NetworkUnchecked>,
"a Bitcoin Cash cashaddr address"
);
#[cfg(feature = "serde")]
impl<N: NetworkValidation> serde::Serialize for CashAddress<N> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.collect_str(&DisplayUnchecked(self))
}
}
impl<V: NetworkValidation> CashAddress<V> {
pub fn as_unchecked(&self) -> &CashAddress<NetworkUnchecked> {
unsafe { &*(self as *const CashAddress<V> as *const CashAddress<NetworkUnchecked>) }
}
pub fn into_unchecked(self) -> CashAddress<NetworkUnchecked> {
CashAddress(self.0, PhantomData)
}
pub fn is_token_aware(&self) -> bool { self.0.token_aware }
pub fn with_token_awareness(mut self, token_aware: bool) -> Self {
self.0.token_aware = token_aware;
self
}
pub fn address_type(&self) -> AddressType {
match &self.0.payload {
CashPayload::P2pkh(_) => AddressType::P2pkh,
CashPayload::P2sh(_) => AddressType::P2sh,
CashPayload::P2sh32(_) => AddressType::P2sh32,
}
}
}
impl CashAddress {
#[inline]
pub fn p2pkh(pk: impl Into<PubkeyHash>, prefix: impl Into<KnownPrefix>) -> CashAddress {
let inner = CashAddressInner {
payload: CashPayload::P2pkh(pk.into()),
token_aware: false,
prefix: prefix.into(),
};
Self(inner, PhantomData)
}
#[inline]
pub fn p2sh(
script: &Script,
prefix: impl Into<KnownPrefix>,
) -> Result<CashAddress, super::P2shError> {
if script.len() > MAX_SCRIPT_ELEMENT_SIZE {
return Err(super::P2shError::ExcessiveScriptSize);
}
Ok(Self::p2sh_from_hash(script.script_hash(), prefix))
}
pub fn p2sh_from_hash(hash: ScriptHash, prefix: impl Into<KnownPrefix>) -> CashAddress {
let inner = CashAddressInner {
payload: CashPayload::P2sh(hash),
token_aware: false,
prefix: prefix.into(),
};
Self(inner, PhantomData)
}
#[inline]
pub fn p2sh32(
script: &Script,
prefix: impl Into<KnownPrefix>,
) -> Result<CashAddress, super::P2shError> {
if script.len() > MAX_SCRIPT_ELEMENT_SIZE {
return Err(super::P2shError::ExcessiveScriptSize);
}
Ok(Self::p2sh32_from_hash(script.script_hash32(), prefix))
}
pub fn p2sh32_from_hash(hash: ScriptHash32, prefix: impl Into<KnownPrefix>) -> CashAddress {
let inner = CashAddressInner {
payload: CashPayload::P2sh32(hash),
token_aware: false,
prefix: prefix.into(),
};
Self(inner, PhantomData)
}
pub fn from_script(
script: &Script,
prefix: impl Into<KnownPrefix>,
) -> Result<CashAddress, super::FromScriptError> {
if script.is_p2pkh() {
let bytes = script.as_bytes()[3..23].try_into().expect("statically 20B long");
Ok(CashAddress::p2pkh(PubkeyHash::from_byte_array(bytes), prefix))
} else if script.is_p2sh() {
let bytes = script.as_bytes()[2..22].try_into().expect("statically 20B long");
Ok(CashAddress::p2sh_from_hash(ScriptHash::from_byte_array(bytes), prefix))
} else if script.is_p2sh32() {
let bytes = script.as_bytes()[2..34].try_into().expect("statically 32B long");
Ok(CashAddress::p2sh32_from_hash(ScriptHash32::from_byte_array(bytes), prefix))
} else {
Err(super::FromScriptError::UnrecognizedScript)
}
}
pub fn script_pubkey(&self) -> ScriptBuf {
match &self.0.payload {
CashPayload::P2pkh(hash) => ScriptBuf::new_p2pkh(hash),
CashPayload::P2sh(hash) => ScriptBuf::new_p2sh(hash),
CashPayload::P2sh32(hash) => ScriptBuf::new_p2sh32(hash),
}
}
pub fn matches_script_pubkey(&self, script: &Script) -> bool {
match &self.0.payload {
CashPayload::P2pkh(hash) if script.is_p2pkh() =>
&script.as_bytes()[3..23] == <PubkeyHash as AsRef<[u8; 20]>>::as_ref(hash),
CashPayload::P2sh(hash) if script.is_p2sh() =>
&script.as_bytes()[2..22] == <ScriptHash as AsRef<[u8; 20]>>::as_ref(hash),
CashPayload::P2sh32(hash) if script.is_p2sh32() =>
&script.as_bytes()[2..34] == <ScriptHash32 as AsRef<[u8; 32]>>::as_ref(hash),
_ => false,
}
}
pub fn prefix(&self) -> KnownPrefix { self.0.prefix }
pub fn pubkey_hash(&self) -> Option<PubkeyHash> {
match &self.0.payload {
CashPayload::P2pkh(hash) => Some(*hash),
_ => None,
}
}
pub fn script_hash(&self) -> Option<ScriptHash> {
match &self.0.payload {
CashPayload::P2sh(hash) => Some(*hash),
_ => None,
}
}
pub fn script_hash32(&self) -> Option<ScriptHash32> {
match &self.0.payload {
CashPayload::P2sh32(hash) => Some(*hash),
_ => None,
}
}
pub fn to_legacy(&self) -> Option<Address> {
let network = match self.0.prefix {
KnownPrefix::Mainnet => NetworkKind::Main,
KnownPrefix::Testnets | KnownPrefix::Regtest => NetworkKind::Test,
};
match &self.0.payload {
CashPayload::P2pkh(hash) => Some(Address::p2pkh(*hash, network)),
CashPayload::P2sh(hash) => Some(Address::p2sh_from_hash(*hash, network)),
CashPayload::P2sh32(_) => None,
}
}
}
impl CashAddress<NetworkUnchecked> {
pub fn assume_checked_ref(&self) -> &CashAddress {
unsafe { &*(self as *const CashAddress<NetworkUnchecked> as *const CashAddress) }
}
pub fn is_valid_for_network(&self, n: Network) -> bool {
self.0.prefix == KnownPrefix::from_network(n)
}
#[inline]
pub fn require_network(self, required: Network) -> Result<CashAddress, ParseError> {
if self.is_valid_for_network(required) {
Ok(self.assume_checked())
} else {
Err(NetworkValidationError { required, address: self }.into())
}
}
#[inline]
pub fn assume_checked(self) -> CashAddress { CashAddress(self.0, PhantomData) }
}
impl From<CashAddress> for ScriptBuf {
fn from(a: CashAddress) -> Self { a.script_pubkey() }
}
impl fmt::Display for CashAddress {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0, fmt) }
}
impl<V: NetworkValidation> fmt::Debug for CashAddress<V> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if V::IS_CHECKED {
fmt::Display::fmt(&self.0, f)
} else {
write!(f, "CashAddress<NetworkUnchecked>(")?;
fmt::Display::fmt(&self.0, f)?;
write!(f, ")")
}
}
}
fn interpret(
prefix: KnownPrefix,
type_bits: u8,
data: &[u8],
) -> Result<CashAddressInner, ParseError> {
let token_aware = match type_bits {
TYPE_P2PKH | TYPE_P2SH => false,
TYPE_P2PKH_TOKEN | TYPE_P2SH_TOKEN => true,
_ => return Err(UnsupportedTypeError { type_bits }.into()),
};
let is_script_hash = type_bits & 0x01 != 0;
let payload = match (is_script_hash, data.len()) {
(false, 20) => CashPayload::P2pkh(PubkeyHash::from_byte_array(
data.try_into().expect("length just checked"),
)),
(true, 20) => CashPayload::P2sh(ScriptHash::from_byte_array(
data.try_into().expect("length just checked"),
)),
(true, 32) => CashPayload::P2sh32(ScriptHash32::from_byte_array(
data.try_into().expect("length just checked"),
)),
(_, length) => return Err(UnsupportedHashLengthError { type_bits, length }.into()),
};
Ok(CashAddressInner { payload, token_aware, prefix })
}
impl FromStr for CashAddress<NetworkUnchecked> {
type Err = ParseError;
fn from_str(s: &str) -> Result<CashAddress<NetworkUnchecked>, ParseError> {
check_case(s).map_err(ParseError::Decode)?;
let inner = match s.split_once(':') {
Some((prefix, body)) => {
let known = KnownPrefix::from_prefix_str(prefix)
.ok_or_else(|| UnknownPrefixError(prefix.to_owned()))?;
let (type_bits, data) = decode_body(known.as_str(), body)?;
interpret(known, type_bits, &data)?
}
None => {
let mut result: Result<CashAddressInner, ParseError> =
Err(DecodeError::Checksum.into());
for known in [KnownPrefix::Mainnet, KnownPrefix::Testnets, KnownPrefix::Regtest] {
match decode_body(known.as_str(), s) {
Ok((type_bits, data)) => {
result = Ok(interpret(known, type_bits, &data)?);
break;
}
Err(DecodeError::Checksum) => continue,
Err(e) => {
result = Err(e.into());
break;
}
}
}
result?
}
};
Ok(CashAddress(inner, PhantomData))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum EncodeError {
InvalidPayloadLength(usize),
InvalidTypeBits(u8),
InvalidPrefix,
}
internals::impl_from_infallible!(EncodeError);
impl fmt::Display for EncodeError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use EncodeError::*;
match *self {
InvalidPayloadLength(len) => write!(
f,
"cashaddr payload length {} is not one of 20, 24, 28, 32, 40, 48, 56, 64",
len
),
InvalidTypeBits(bits) => write!(f, "cashaddr type bits {} exceed four bits", bits),
InvalidPrefix => write!(f, "cashaddr prefix must be non-empty lowercase ASCII letters"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for EncodeError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None }
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum DecodeError {
MixedCase,
InvalidChar(char),
Checksum,
InvalidLength(usize),
InvalidPadding,
ReservedBitSet,
MissingPrefix,
InvalidPrefix,
}
internals::impl_from_infallible!(DecodeError);
impl fmt::Display for DecodeError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use DecodeError::*;
match *self {
MixedCase => write!(f, "cashaddr strings must not mix lower and upper case"),
InvalidChar(c) => write!(f, "invalid cashaddr character {:?}", c),
Checksum => write!(f, "cashaddr checksum verification failed"),
InvalidLength(len) => write!(f, "invalid cashaddr payload length {}", len),
InvalidPadding => write!(f, "invalid padding in cashaddr payload"),
ReservedBitSet => write!(f, "reserved bit of the cashaddr version byte is set"),
MissingPrefix => write!(f, "cashaddr string has no prefix"),
InvalidPrefix => write!(f, "cashaddr prefix is empty"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for DecodeError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None }
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct UnknownPrefixError(pub String);
impl fmt::Display for UnknownPrefixError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "unknown cashaddr prefix: {}", self.0)
}
}
#[cfg(feature = "std")]
impl std::error::Error for UnknownPrefixError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None }
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct UnsupportedTypeError {
pub type_bits: u8,
}
impl fmt::Display for UnsupportedTypeError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "unsupported cashaddr address type bits: {}", self.type_bits)
}
}
#[cfg(feature = "std")]
impl std::error::Error for UnsupportedTypeError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None }
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct UnsupportedHashLengthError {
pub type_bits: u8,
pub length: usize,
}
impl fmt::Display for UnsupportedHashLengthError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"unsupported hash length {} for cashaddr address type bits {}",
self.length, self.type_bits
)
}
}
#[cfg(feature = "std")]
impl std::error::Error for UnsupportedHashLengthError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None }
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NetworkValidationError {
required: Network,
address: CashAddress<NetworkUnchecked>,
}
impl fmt::Display for NetworkValidationError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "cashaddr address ")?;
fmt::Display::fmt(&self.address.0, f)?;
write!(f, " is not valid on {}", self.required)
}
}
#[cfg(feature = "std")]
impl std::error::Error for NetworkValidationError {}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ParseError {
Decode(DecodeError),
UnknownPrefix(UnknownPrefixError),
UnsupportedType(UnsupportedTypeError),
UnsupportedHashLength(UnsupportedHashLengthError),
NetworkValidation(NetworkValidationError),
}
internals::impl_from_infallible!(ParseError);
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use ParseError::*;
match self {
Decode(e) => write_err!(f, "cashaddr decoding error"; e),
UnknownPrefix(e) => write_err!(f, "cashaddr unknown prefix"; e),
UnsupportedType(e) => write_err!(f, "cashaddr unsupported type"; e),
UnsupportedHashLength(e) => write_err!(f, "cashaddr unsupported hash length"; e),
NetworkValidation(e) => write_err!(f, "cashaddr network validation error"; e),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for ParseError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
use ParseError::*;
match self {
Decode(e) => Some(e),
UnknownPrefix(e) => Some(e),
UnsupportedType(e) => Some(e),
UnsupportedHashLength(e) => Some(e),
NetworkValidation(e) => Some(e),
}
}
}
impl From<DecodeError> for ParseError {
fn from(e: DecodeError) -> Self { Self::Decode(e) }
}
impl From<UnknownPrefixError> for ParseError {
fn from(e: UnknownPrefixError) -> Self { Self::UnknownPrefix(e) }
}
impl From<UnsupportedTypeError> for ParseError {
fn from(e: UnsupportedTypeError) -> Self { Self::UnsupportedType(e) }
}
impl From<UnsupportedHashLengthError> for ParseError {
fn from(e: UnsupportedHashLengthError) -> Self { Self::UnsupportedHashLength(e) }
}
impl From<NetworkValidationError> for ParseError {
fn from(e: NetworkValidationError) -> Self { Self::NetworkValidation(e) }
}
#[cfg(test)]
mod tests {
use hex::FromHex;
use super::*;
fn parse(s: &str) -> CashAddress { s.parse::<CashAddress<_>>().unwrap().assume_checked() }
fn build_raw(prefix: &str, values: &[u8]) -> String {
let mut chk = polymod_prefix(1, prefix);
for &v in values {
chk = polymod_step(chk, v);
}
for _ in 0..8 {
chk = polymod_step(chk, 0);
}
let chk = chk ^ 1;
let mut s = String::from(prefix);
s.push(':');
for &v in values {
s.push(char::from(CHARSET[usize::from(v)]));
}
for i in (0..8).rev() {
s.push(char::from(CHARSET[((chk >> (5 * i)) & 0x1f) as usize]));
}
s
}
const SPEC_VECTORS: &[(&str, u8, &str)] = &[
("bitcoincash:qr6m7j9njldwwzlg9v7v53unlr4jkmx6eylep8ekg2", 0, "F5BF48B397DAE70BE82B3CCA4793F8EB2B6CDAC9"),
("bchtest:pr6m7j9njldwwzlg9v7v53unlr4jkmx6eyvwc0uz5t", 1, "F5BF48B397DAE70BE82B3CCA4793F8EB2B6CDAC9"),
("pref:pr6m7j9njldwwzlg9v7v53unlr4jkmx6ey65nvtks5", 1, "F5BF48B397DAE70BE82B3CCA4793F8EB2B6CDAC9"),
("prefix:0r6m7j9njldwwzlg9v7v53unlr4jkmx6ey3qnjwsrf", 15, "F5BF48B397DAE70BE82B3CCA4793F8EB2B6CDAC9"),
("bitcoincash:q9adhakpwzztepkpwp5z0dq62m6u5v5xtyj7j3h2ws4mr9g0", 0, "7ADBF6C17084BC86C1706827B41A56F5CA32865925E946EA"),
("bchtest:p9adhakpwzztepkpwp5z0dq62m6u5v5xtyj7j3h2u94tsynr", 1, "7ADBF6C17084BC86C1706827B41A56F5CA32865925E946EA"),
("pref:p9adhakpwzztepkpwp5z0dq62m6u5v5xtyj7j3h2khlwwk5v", 1, "7ADBF6C17084BC86C1706827B41A56F5CA32865925E946EA"),
("prefix:09adhakpwzztepkpwp5z0dq62m6u5v5xtyj7j3h2p29kc2lp", 15, "7ADBF6C17084BC86C1706827B41A56F5CA32865925E946EA"),
("bitcoincash:qgagf7w02x4wnz3mkwnchut2vxphjzccwxgjvvjmlsxqwkcw59jxxuz", 0, "3A84F9CF51AAE98A3BB3A78BF16A6183790B18719126325BFC0C075B"),
("bchtest:pgagf7w02x4wnz3mkwnchut2vxphjzccwxgjvvjmlsxqwkcvs7md7wt", 1, "3A84F9CF51AAE98A3BB3A78BF16A6183790B18719126325BFC0C075B"),
("pref:pgagf7w02x4wnz3mkwnchut2vxphjzccwxgjvvjmlsxqwkcrsr6gzkn", 1, "3A84F9CF51AAE98A3BB3A78BF16A6183790B18719126325BFC0C075B"),
("prefix:0gagf7w02x4wnz3mkwnchut2vxphjzccwxgjvvjmlsxqwkc5djw8s9g", 15, "3A84F9CF51AAE98A3BB3A78BF16A6183790B18719126325BFC0C075B"),
("bitcoincash:qvch8mmxy0rtfrlarg7ucrxxfzds5pamg73h7370aa87d80gyhqxq5nlegake", 0, "3173EF6623C6B48FFD1A3DCC0CC6489B0A07BB47A37F47CFEF4FE69DE825C060"),
("bchtest:pvch8mmxy0rtfrlarg7ucrxxfzds5pamg73h7370aa87d80gyhqxq7fqng6m6", 1, "3173EF6623C6B48FFD1A3DCC0CC6489B0A07BB47A37F47CFEF4FE69DE825C060"),
("pref:pvch8mmxy0rtfrlarg7ucrxxfzds5pamg73h7370aa87d80gyhqxq4k9m7qf9", 1, "3173EF6623C6B48FFD1A3DCC0CC6489B0A07BB47A37F47CFEF4FE69DE825C060"),
("prefix:0vch8mmxy0rtfrlarg7ucrxxfzds5pamg73h7370aa87d80gyhqxqsh6jgp6w", 15, "3173EF6623C6B48FFD1A3DCC0CC6489B0A07BB47A37F47CFEF4FE69DE825C060"),
("bitcoincash:qnq8zwpj8cq05n7pytfmskuk9r4gzzel8qtsvwz79zdskftrzxtar994cgutavfklv39gr3uvz", 0, "C07138323E00FA4FC122D3B85B9628EA810B3F381706385E289B0B25631197D194B5C238BEB136FB"),
("bchtest:pnq8zwpj8cq05n7pytfmskuk9r4gzzel8qtsvwz79zdskftrzxtar994cgutavfklvmgm6ynej", 1, "C07138323E00FA4FC122D3B85B9628EA810B3F381706385E289B0B25631197D194B5C238BEB136FB"),
("pref:pnq8zwpj8cq05n7pytfmskuk9r4gzzel8qtsvwz79zdskftrzxtar994cgutavfklv0vx5z0w3", 1, "C07138323E00FA4FC122D3B85B9628EA810B3F381706385E289B0B25631197D194B5C238BEB136FB"),
("prefix:0nq8zwpj8cq05n7pytfmskuk9r4gzzel8qtsvwz79zdskftrzxtar994cgutavfklvwsvctzqy", 15, "C07138323E00FA4FC122D3B85B9628EA810B3F381706385E289B0B25631197D194B5C238BEB136FB"),
("bitcoincash:qh3krj5607v3qlqh5c3wq3lrw3wnuxw0sp8dv0zugrrt5a3kj6ucysfz8kxwv2k53krr7n933jfsunqex2w82sl", 0, "E361CA9A7F99107C17A622E047E3745D3E19CF804ED63C5C40C6BA763696B98241223D8CE62AD48D863F4CB18C930E4C"),
("bchtest:ph3krj5607v3qlqh5c3wq3lrw3wnuxw0sp8dv0zugrrt5a3kj6ucysfz8kxwv2k53krr7n933jfsunqnzf7mt6x", 1, "E361CA9A7F99107C17A622E047E3745D3E19CF804ED63C5C40C6BA763696B98241223D8CE62AD48D863F4CB18C930E4C"),
("pref:ph3krj5607v3qlqh5c3wq3lrw3wnuxw0sp8dv0zugrrt5a3kj6ucysfz8kxwv2k53krr7n933jfsunqjntdfcwg", 1, "E361CA9A7F99107C17A622E047E3745D3E19CF804ED63C5C40C6BA763696B98241223D8CE62AD48D863F4CB18C930E4C"),
("prefix:0h3krj5607v3qlqh5c3wq3lrw3wnuxw0sp8dv0zugrrt5a3kj6ucysfz8kxwv2k53krr7n933jfsunqakcssnmn", 15, "E361CA9A7F99107C17A622E047E3745D3E19CF804ED63C5C40C6BA763696B98241223D8CE62AD48D863F4CB18C930E4C"),
("bitcoincash:qmvl5lzvdm6km38lgga64ek5jhdl7e3aqd9895wu04fvhlnare5937w4ywkq57juxsrhvw8ym5d8qx7sz7zz0zvcypqscw8jd03f", 0, "D9FA7C4C6EF56DC4FF423BAAE6D495DBFF663D034A72D1DC7D52CBFE7D1E6858F9D523AC0A7A5C34077638E4DD1A701BD017842789982041"),
("bchtest:pmvl5lzvdm6km38lgga64ek5jhdl7e3aqd9895wu04fvhlnare5937w4ywkq57juxsrhvw8ym5d8qx7sz7zz0zvcypqs6kgdsg2g", 1, "D9FA7C4C6EF56DC4FF423BAAE6D495DBFF663D034A72D1DC7D52CBFE7D1E6858F9D523AC0A7A5C34077638E4DD1A701BD017842789982041"),
("pref:pmvl5lzvdm6km38lgga64ek5jhdl7e3aqd9895wu04fvhlnare5937w4ywkq57juxsrhvw8ym5d8qx7sz7zz0zvcypqsammyqffl", 1, "D9FA7C4C6EF56DC4FF423BAAE6D495DBFF663D034A72D1DC7D52CBFE7D1E6858F9D523AC0A7A5C34077638E4DD1A701BD017842789982041"),
("prefix:0mvl5lzvdm6km38lgga64ek5jhdl7e3aqd9895wu04fvhlnare5937w4ywkq57juxsrhvw8ym5d8qx7sz7zz0zvcypqsgjrqpnw8", 15, "D9FA7C4C6EF56DC4FF423BAAE6D495DBFF663D034A72D1DC7D52CBFE7D1E6858F9D523AC0A7A5C34077638E4DD1A701BD017842789982041"),
("bitcoincash:qlg0x333p4238k0qrc5ej7rzfw5g8e4a4r6vvzyrcy8j3s5k0en7calvclhw46hudk5flttj6ydvjc0pv3nchp52amk97tqa5zygg96mtky5sv5w", 0, "D0F346310D5513D9E01E299978624BA883E6BDA8F4C60883C10F28C2967E67EC77ECC7EEEAEAFC6DA89FAD72D11AC961E164678B868AEEEC5F2C1DA08884175B"),
("bchtest:plg0x333p4238k0qrc5ej7rzfw5g8e4a4r6vvzyrcy8j3s5k0en7calvclhw46hudk5flttj6ydvjc0pv3nchp52amk97tqa5zygg96mc773cwez", 1, "D0F346310D5513D9E01E299978624BA883E6BDA8F4C60883C10F28C2967E67EC77ECC7EEEAEAFC6DA89FAD72D11AC961E164678B868AEEEC5F2C1DA08884175B"),
("pref:plg0x333p4238k0qrc5ej7rzfw5g8e4a4r6vvzyrcy8j3s5k0en7calvclhw46hudk5flttj6ydvjc0pv3nchp52amk97tqa5zygg96mg7pj3lh8", 1, "D0F346310D5513D9E01E299978624BA883E6BDA8F4C60883C10F28C2967E67EC77ECC7EEEAEAFC6DA89FAD72D11AC961E164678B868AEEEC5F2C1DA08884175B"),
("prefix:0lg0x333p4238k0qrc5ej7rzfw5g8e4a4r6vvzyrcy8j3s5k0en7calvclhw46hudk5flttj6ydvjc0pv3nchp52amk97tqa5zygg96ms92w6845", 15, "D0F346310D5513D9E01E299978624BA883E6BDA8F4C60883C10F28C2967E67EC77ECC7EEEAEAFC6DA89FAD72D11AC961E164678B868AEEEC5F2C1DA08884175B"),
];
const CASHTOKENS_PAIRS: &[(&str, &str, &str)] = &[
(
"bitcoincash:qr7fzmep8g7h7ymfxy74lgc0v950j3r2959lhtxxsl",
"bitcoincash:zr7fzmep8g7h7ymfxy74lgc0v950j3r295z4y4gq0v",
"fc916f213a3d7f1369313d5fa30f6168f9446a2d",
),
(
"bchtest:qr7fzmep8g7h7ymfxy74lgc0v950j3r295pdnvy3hr",
"bchtest:zr7fzmep8g7h7ymfxy74lgc0v950j3r295x8qj2hgs",
"fc916f213a3d7f1369313d5fa30f6168f9446a2d",
),
(
"bchreg:qr7fzmep8g7h7ymfxy74lgc0v950j3r295m39d8z59",
"bchreg:zr7fzmep8g7h7ymfxy74lgc0v950j3r295umknfytk",
"fc916f213a3d7f1369313d5fa30f6168f9446a2d",
),
(
"bitcoincash:qpagr634w55t4wp56ftxx53xukhqgl24yse53qxdge",
"bitcoincash:zpagr634w55t4wp56ftxx53xukhqgl24ys77z7gth2",
"7a81ea357528bab834d256635226e5ae047d5524",
),
(
"bitcoincash:qq9l9e2dgkx0hp43qm3c3h252e9euugrfc6vlt3r9e",
"bitcoincash:zq9l9e2dgkx0hp43qm3c3h252e9euugrfcaxv4l962",
"0bf2e54d458cfb86b106e388dd54564b9e71034e",
),
(
"bitcoincash:qre24q38ghy6k3pegpyvtxahu8q8hqmxmqqn28z85p",
"bitcoincash:zre24q38ghy6k3pegpyvtxahu8q8hqmxmq8eeevptj",
"f2aa822745c9ab44394048c59bb7e1c07b8366d8",
),
(
"bitcoincash:qz7xc0vl85nck65ffrsx5wvewjznp9lflgktxc5878",
"bitcoincash:zz7xc0vl85nck65ffrsx5wvewjznp9lflg3p4x6pp5",
"bc6c3d9f3d278b6a8948e06a399974853097e9fa",
),
(
"bitcoincash:ppawqn2h74a4t50phuza84kdp3794pq3ccvm92p8sh",
"bitcoincash:rpawqn2h74a4t50phuza84kdp3794pq3cct3k50p0y",
"7ae04d57f57b55d1e1bf05d3d6cd0c7c5a8411c6",
),
(
"bitcoincash:pqv53dwyatxse2xh7nnlqhyr6ryjgfdtagkd4vc388",
"bitcoincash:rqv53dwyatxse2xh7nnlqhyr6ryjgfdtag38xjkhc5",
"1948b5c4eacd0ca8d7f4e7f05c83d0c92425abea",
),
(
"bitcoincash:prseh0a4aejjcewhc665wjqhppgwrz2lw5txgn666a",
"bitcoincash:rrseh0a4aejjcewhc665wjqhppgwrz2lw5vvmd5u9w",
"e19bbfb5ee652c65d7c6b54748170850e1895f75",
),
(
"bitcoincash:pzltaslh7xnrsxeqm7qtvh0v53n3gfk0v5wwf6d7j4",
"bitcoincash:rzltaslh7xnrsxeqm7qtvh0v53n3gfk0v5fy6yrcdx",
"bebec3f7f1a6381b20df80b65deca4671426cf65",
),
(
"bitcoincash:pvqqqqqqqqqqqqqqqqqqqqqqzg69v7ysqqqqqqqqqqqqqqqqqqqqqpkp7fqn0",
"bitcoincash:rvqqqqqqqqqqqqqqqqqqqqqqzg69v7ysqqqqqqqqqqqqqqqqqqqqqn9alsp2y",
"0000000000000000000000000000123456789000000000000000000000000000",
),
(
"bitcoincash:pdzyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3jh2p5nn",
"bitcoincash:rdzyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygrpttc42c",
"4444444444444444444444444444444444444444444444444444444444444444",
),
(
"bitcoincash:pwyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsh3sujgcr",
"bitcoincash:rwyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9zvatfpg",
"8888888888888888888888888888888888888888888888888888888888888888",
),
(
"bitcoincash:p0xvenxvenxvenxvenxvenxvenxvenxvenxvenxvenxvenxvenxvcm6gz4t77",
"bitcoincash:r0xvenxvenxvenxvenxvenxvenxvenxvenxvenxvenxvenxvenxvcff5rv284",
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
),
(
"bitcoincash:p0llllllllllllllllllllllllllllllllllllllllllllllllll7x3vthu35",
"bitcoincash:r0llllllllllllllllllllllllllllllllllllllllllllllllll75zs2wagl",
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
),
];
const LEGACY_PAIRS: &[(&str, &str)] = &[
(
"1BpEi6DfDAUFd7GtittLSdBeYJvcoaVggu",
"bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a",
),
(
"1KXrWXciRDZUpQwQmuM1DbwsKDLYAYsVLR",
"bitcoincash:qr95sy3j9xwd2ap32xkykttr4cvcu7as4y0qverfuy",
),
(
"16w1D5WRVKJuZUsSRzdLp9w3YGcgoxDXb",
"bitcoincash:qqq3728yw0y47sqn6l2na30mcw6zm78dzqre909m2r",
),
(
"3CWFddi6m4ndiGyKqzYvsFYagqDLPVMTzC",
"bitcoincash:ppm2qsznhks23z7629mms6s4cwef74vcwvn0h829pq",
),
(
"3LDsS579y7sruadqu11beEJoTjdFiFCdX4",
"bitcoincash:pr95sy3j9xwd2ap32xkykttr4cvcu7as4yc93ky28e",
),
(
"31nwvkZwyPdgzjBJZXfDmSWsC4ZLKpYyUw",
"bitcoincash:pqq3728yw0y47sqn6l2na30mcw6zm78dzq5ucqzc37",
),
];
#[test]
fn spec_checksum_vectors() {
for addr in [
"prefix:x64nx6hz",
"p:gpf8m4h7",
"bitcoincash:qpzry9x8gf2tvdw0s3jn54khce6mua7lcw20ayyn",
"bchtest:testnetaddress4d6njnut",
"bchreg:555555555555555555555555555555555555555555555udxmlmrz",
] {
let (prefix, body) = addr.split_once(':').unwrap();
let mut chk = polymod_prefix(1, prefix);
for c in body.chars() {
chk = polymod_step(chk, char_value(c).unwrap());
}
assert_eq!(chk, 1, "checksum failed for {}", addr);
}
}
#[test]
fn spec_larger_test_vectors() {
for &(addr, type_bits, hex) in SPEC_VECTORS {
let data = Vec::<u8>::from_hex(hex).unwrap();
let (prefix, _) = addr.split_once(':').unwrap();
assert_eq!(encode(prefix, type_bits, &data).unwrap(), addr);
let decoded = decode(addr).unwrap();
assert_eq!(decoded.prefix, prefix);
assert_eq!(decoded.type_bits, type_bits);
assert_eq!(decoded.data, data);
}
}
#[test]
fn decode_with_prefix_roundtrip() {
for &(addr, type_bits, hex) in SPEC_VECTORS {
let data = Vec::<u8>::from_hex(hex).unwrap();
let (prefix, body) = addr.split_once(':').unwrap();
let decoded = decode_with_prefix(prefix, body).unwrap();
assert_eq!(decoded.type_bits, type_bits);
assert_eq!(decoded.data, data);
}
let (_, body) =
"bitcoincash:qr6m7j9njldwwzlg9v7v53unlr4jkmx6eylep8ekg2".split_once(':').unwrap();
assert_eq!(decode_with_prefix("bchtest", body), Err(DecodeError::Checksum));
}
#[test]
fn cashtokens_vectors() {
for &(plain, token, hex) in CASHTOKENS_PAIRS {
let data = Vec::<u8>::from_hex(hex).unwrap();
let plain_addr = parse(plain);
let token_addr = parse(token);
assert!(!plain_addr.is_token_aware());
assert!(token_addr.is_token_aware());
assert_eq!(plain_addr.to_string(), plain);
assert_eq!(token_addr.to_string(), token);
assert_eq!(plain_addr.clone().with_token_awareness(true), token_addr);
assert_eq!(token_addr.clone().with_token_awareness(false), plain_addr);
assert_eq!(plain_addr.script_pubkey(), token_addr.script_pubkey());
let payload: &[u8] = match plain_addr.address_type() {
AddressType::P2pkh => plain_addr.pubkey_hash().unwrap().to_byte_array().to_vec(),
AddressType::P2sh => plain_addr.script_hash().unwrap().to_byte_array().to_vec(),
AddressType::P2sh32 => plain_addr.script_hash32().unwrap().to_byte_array().to_vec(),
other => panic!("unexpected address type {}", other),
}
.leak();
assert_eq!(payload, &data[..]);
}
}
#[test]
fn parse_without_prefix() {
for (with_prefix, expected_prefix) in [
("bitcoincash:qr6m7j9njldwwzlg9v7v53unlr4jkmx6eylep8ekg2", KnownPrefix::Mainnet),
("bchtest:pr6m7j9njldwwzlg9v7v53unlr4jkmx6eyvwc0uz5t", KnownPrefix::Testnets),
("bchreg:qr7fzmep8g7h7ymfxy74lgc0v950j3r295m39d8z59", KnownPrefix::Regtest),
] {
let (_, body) = with_prefix.split_once(':').unwrap();
let addr = parse(body);
assert_eq!(addr.prefix(), expected_prefix);
assert_eq!(addr.to_string(), with_prefix);
assert_eq!(addr, parse(with_prefix));
}
}
#[test]
fn uppercase() {
let lower = "bitcoincash:qr6m7j9njldwwzlg9v7v53unlr4jkmx6eylep8ekg2";
let upper = "BITCOINCASH:QR6M7J9NJLDWWZLG9V7V53UNLR4JKMX6EYLEP8EKG2";
let addr = parse(upper);
assert_eq!(addr, parse(lower));
assert_eq!(addr.to_string(), lower);
assert_eq!(format!("{:#}", addr), upper);
}
#[test]
fn network_validation() {
use crate::network::Network::*;
let addr: CashAddress<NetworkUnchecked> =
"bchtest:pr6m7j9njldwwzlg9v7v53unlr4jkmx6eyvwc0uz5t".parse().unwrap();
for network in [Testnet, Testnet4, Scalenet, Chipnet] {
assert!(addr.is_valid_for_network(network));
}
assert!(!addr.is_valid_for_network(Bitcoin));
assert!(!addr.is_valid_for_network(Regtest));
assert!(matches!(addr.require_network(Bitcoin), Err(ParseError::NetworkValidation(_))));
let addr: CashAddress<NetworkUnchecked> =
"bitcoincash:qr6m7j9njldwwzlg9v7v53unlr4jkmx6eylep8ekg2".parse().unwrap();
assert!(addr.is_valid_for_network(Bitcoin));
assert!(addr.require_network(Bitcoin).is_ok());
}
#[test]
fn legacy_conversion() {
use core::str::FromStr;
for &(legacy, cashaddr) in LEGACY_PAIRS {
let legacy_addr = Address::from_str(legacy).unwrap().assume_checked();
assert_eq!(legacy_addr.to_cashaddr().unwrap().to_string(), cashaddr);
let cash_addr = parse(cashaddr);
assert_eq!(cash_addr.to_legacy().unwrap().to_string(), legacy);
assert_eq!(cash_addr.script_pubkey(), legacy_addr.script_pubkey());
}
let p2sh32 =
parse("bitcoincash:p0llllllllllllllllllllllllllllllllllllllllllllllllll7x3vthu35");
assert_eq!(p2sh32.to_legacy(), None);
let segwit = Address::from_str("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4")
.unwrap()
.assume_checked();
assert_eq!(segwit.to_cashaddr(), None);
}
#[test]
fn script_roundtrip() {
let redeem = ScriptBuf::from_bytes(vec![0x51]);
let p2pkh = parse("bitcoincash:qr6m7j9njldwwzlg9v7v53unlr4jkmx6eylep8ekg2");
let spk = p2pkh.script_pubkey();
assert!(spk.is_p2pkh());
assert!(p2pkh.matches_script_pubkey(&spk));
assert_eq!(CashAddress::from_script(&spk, KnownPrefix::Mainnet).unwrap(), p2pkh);
let p2sh = CashAddress::p2sh(&redeem, Network::Bitcoin).unwrap();
assert_eq!(p2sh.script_hash().unwrap(), redeem.script_hash());
let spk = p2sh.script_pubkey();
assert!(spk.is_p2sh());
assert!(p2sh.matches_script_pubkey(&spk));
assert_eq!(CashAddress::from_script(&spk, KnownPrefix::Mainnet).unwrap(), p2sh);
let p2sh32 = CashAddress::p2sh32(&redeem, Network::Bitcoin).unwrap();
assert_eq!(p2sh32.address_type(), AddressType::P2sh32);
assert_eq!(p2sh32.script_hash32().unwrap(), redeem.script_hash32());
let spk = p2sh32.script_pubkey();
assert_eq!(spk, redeem.to_p2sh32());
assert!(spk.is_p2sh32());
assert_eq!(spk.len(), 35);
assert_eq!(spk.as_bytes()[0], 0xaa); assert_eq!(spk.as_bytes()[1], 0x20); assert_eq!(spk.as_bytes()[34], 0x87); assert!(p2sh32.matches_script_pubkey(&spk));
assert_eq!(CashAddress::from_script(&spk, KnownPrefix::Mainnet).unwrap(), p2sh32);
let hash = ScriptHash32::from_byte_array([0xff; 32]);
let addr = CashAddress::p2sh32_from_hash(hash, Network::Bitcoin);
assert_eq!(
addr.to_string(),
"bitcoincash:p0llllllllllllllllllllllllllllllllllllllllllllllllll7x3vthu35"
);
let excessive = ScriptBuf::from_bytes(vec![0x51; MAX_SCRIPT_ELEMENT_SIZE + 1]);
assert!(CashAddress::p2sh(&excessive, Network::Bitcoin).is_err());
assert!(CashAddress::p2sh32(&excessive, Network::Bitcoin).is_err());
}
#[test]
fn parse_errors() {
fn parse_err(s: &str) -> ParseError { s.parse::<CashAddress<_>>().unwrap_err() }
assert_eq!(
parse_err("bitcoincash:qr6m7j9njldwwzlg9v7v53unlr4jkMX6eylep8ekg2"),
ParseError::Decode(DecodeError::MixedCase)
);
assert_eq!(
parse_err("Bitcoincash:qr6m7j9njldwwzlg9v7v53unlr4jkmx6eylep8ekg2"),
ParseError::Decode(DecodeError::MixedCase)
);
assert_eq!(
parse_err("bitcoincash:qr6m7j9njldwwzlg9v7v53unlr4jkmx6eylep8ekb2"),
ParseError::Decode(DecodeError::InvalidChar('b'))
);
assert_eq!(
parse_err("bitcoincash:qr6m7j9njldwwzlg9v7v53unlr4jkmx6eylep8ekg3"),
ParseError::Decode(DecodeError::Checksum)
);
assert_eq!(
parse_err("qr6m7j9njldwwzlg9v7v53unlr4jkmx6eylep8ekg3"),
ParseError::Decode(DecodeError::Checksum)
);
assert!(matches!(
parse_err("pref:pr6m7j9njldwwzlg9v7v53unlr4jkmx6ey65nvtks5"),
ParseError::UnknownPrefix(_)
));
let unknown_type = encode("bitcoincash", 15, &[0x42; 20]).unwrap();
assert!(matches!(
parse_err(&unknown_type),
ParseError::UnsupportedType(UnsupportedTypeError { type_bits: 15, .. })
));
let bad_length = encode("bitcoincash", 0, &[0x42; 24]).unwrap();
assert!(matches!(
parse_err(&bad_length),
ParseError::UnsupportedHashLength(UnsupportedHashLengthError {
type_bits: 0,
length: 24,
..
})
));
let bad_length = encode("bitcoincash", 0, &[0x42; 32]).unwrap();
assert!(matches!(
parse_err(&bad_length),
ParseError::UnsupportedHashLength(UnsupportedHashLengthError {
type_bits: 0,
length: 32,
..
})
));
assert_eq!(
parse_err("bitcoincash:qqqqq"),
ParseError::Decode(DecodeError::InvalidLength(5))
);
}
#[test]
fn decode_errors() {
assert_eq!(
decode("qr6m7j9njldwwzlg9v7v53unlr4jkmx6eylep8ekg2"),
Err(DecodeError::MissingPrefix)
);
assert_eq!(
decode(":qr6m7j9njldwwzlg9v7v53unlr4jkmx6eylep8ekg2"),
Err(DecodeError::InvalidPrefix)
);
assert_eq!(
decode_with_prefix("", "qr6m7j9njldwwzlg9v7v53unlr4jkmx6eylep8ekg2"),
Err(DecodeError::InvalidPrefix)
);
assert_eq!(
decode("b1tcoincash:qr6m7j9njldwwzlg9v7v53unlr4jkmx6eylep8ekg2"),
Err(DecodeError::InvalidChar('1'))
);
assert_eq!(
decode_with_prefix("BITCOINCASH", "qr6m7j9njldwwzlg9v7v53unlr4jkmx6eylep8ekg2"),
Err(DecodeError::InvalidChar('B'))
);
let mut values = [0u8; 34]; values[33] = 1; assert_eq!(decode(&build_raw("bitcoincash", &values)), Err(DecodeError::InvalidPadding));
let values = [0u8; 35];
assert_eq!(decode(&build_raw("bitcoincash", &values)), Err(DecodeError::InvalidPadding));
let mut s = String::new();
encode_to_fmt(&mut s, "bitcoincash", 0x80, &[0; 20], false).unwrap();
assert_eq!(decode(&s), Err(DecodeError::ReservedBitSet));
let mut s = String::new();
encode_to_fmt(&mut s, "bitcoincash", 0x03, &[0; 20], false).unwrap();
assert_eq!(decode(&s), Err(DecodeError::InvalidLength(20)));
}
#[test]
fn encode_errors() {
assert_eq!(encode("bitcoincash", 0, &[0; 21]), Err(EncodeError::InvalidPayloadLength(21)));
assert_eq!(encode("bitcoincash", 16, &[0; 20]), Err(EncodeError::InvalidTypeBits(16)));
assert_eq!(encode("Bitcoincash", 0, &[0; 20]), Err(EncodeError::InvalidPrefix));
assert_eq!(encode("bch1", 0, &[0; 20]), Err(EncodeError::InvalidPrefix));
assert_eq!(encode("", 0, &[0; 20]), Err(EncodeError::InvalidPrefix));
}
#[test]
fn address_type_p2sh32() {
use core::str::FromStr;
assert_eq!(AddressType::from_str("p2sh32").unwrap(), AddressType::P2sh32);
assert_eq!(AddressType::P2sh32.to_string(), "p2sh32");
}
#[cfg(feature = "serde")]
#[test]
fn serde_roundtrip() {
let addr = parse("bitcoincash:zr7fzmep8g7h7ymfxy74lgc0v950j3r295z4y4gq0v");
let json = serde_json::to_string(&addr).unwrap();
assert_eq!(json, "\"bitcoincash:zr7fzmep8g7h7ymfxy74lgc0v950j3r295z4y4gq0v\"");
let back: CashAddress<NetworkUnchecked> = serde_json::from_str(&json).unwrap();
assert_eq!(back.assume_checked(), addr);
}
}