diff --git a/Cargo.toml b/Cargo.toml index e5089819..039b1e13 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,9 +9,9 @@ repository = "https://github.com/ElementsProject/rust-elements/" documentation = "https://docs.rs/elements/" [features] -default = [ "json-contract" ] +default = [ "contracts" ] -json-contract = [ "serde_json" ] +contracts = [ "serde", "serde_cbor", "serde_json" ] "serde-feature" = [ "bitcoin/use-serde", "serde" @@ -25,13 +25,13 @@ bitcoin = "0.23" # to avoid requiring two version of bitcoin_hashes. bitcoin_hashes = "0.7.6" -# Used for ContractHash::from_json_contract. -serde_json = { version = "<=1.0.44", optional = true } +serde = { version = "1.0", optional = true, features = ["derive"] } -[dependencies.serde] -version = "1.0" -optional = true +# Used for contracts module. +serde_cbor = { version = "0.11.1", optional = true } +serde_json = { version = "<=1.0.44", optional = true } [dev-dependencies] rand = "0.6.5" serde_json = "<=1.0.44" +bitcoin = { version = "0.23", features = ["use-serde"] } diff --git a/src/address.rs b/src/address.rs index 807e523e..d90f115c 100644 --- a/src/address.rs +++ b/src/address.rs @@ -30,7 +30,7 @@ use bitcoin::util::base58; use bitcoin::PublicKey; use bitcoin::hashes::Hash; use bitcoin::secp256k1; -#[cfg(feature = "serde")] +#[cfg(feature = "serde-feature")] use serde; use blech32; @@ -75,6 +75,7 @@ impl fmt::Display for AddressError { } } +#[allow(deprecated)] impl error::Error for AddressError { fn cause(&self) -> Option<&error::Error> { match *self { @@ -87,16 +88,7 @@ impl error::Error for AddressError { } fn description(&self) -> &str { - match *self { - AddressError::Base58(ref e) => e.description(), - AddressError::Bech32(ref e) => e.description(), - AddressError::Blech32(ref e) => e.description(), - AddressError::InvalidAddress(..) => "was unable to parse the address", - AddressError::UnsupportedWitnessVersion(..) => "unsupported witness version", - AddressError::InvalidBlindingPubKey(..) => "an invalid blinding pubkey was encountered", - AddressError::InvalidWitnessProgramLength => "program length incompatible with version", - AddressError::InvalidWitnessVersion => "invalid witness script version", - } + "description() is deprecated; use Display" } } @@ -603,7 +595,7 @@ impl FromStr for Address { } } -#[cfg(feature = "serde")] +#[cfg(feature = "serde-feature")] impl<'de> serde::Deserialize<'de> for Address { #[inline] fn deserialize(deserializer: D) -> Result @@ -646,7 +638,7 @@ impl<'de> serde::Deserialize<'de> for Address { } } -#[cfg(feature = "serde")] +#[cfg(feature = "serde-feature")] impl serde::Serialize for Address { fn serialize(&self, serializer: S) -> Result where @@ -662,8 +654,6 @@ mod test { use bitcoin::util::key; use bitcoin::Script; use bitcoin::secp256k1::{PublicKey, Secp256k1}; - #[cfg(feature = "serde")] - use serde_json; fn roundtrips(addr: &Address) { assert_eq!( @@ -678,9 +668,9 @@ mod test { "script round-trip failed for {}", addr, ); - #[cfg(feature = "serde")] + #[cfg(feature = "serde-feature")] assert_eq!( - serde_json::from_value::
(serde_json::to_value(&addr).unwrap()).ok().as_ref(), + ::serde_json::from_value::
(serde_json::to_value(&addr).unwrap()).ok().as_ref(), Some(addr) ); } diff --git a/src/block.rs b/src/block.rs index a95a65ed..8cb3a14f 100644 --- a/src/block.rs +++ b/src/block.rs @@ -21,8 +21,8 @@ use bitcoin; use bitcoin::blockdata::script::Script; use bitcoin::{BitcoinHash, BlockHash, VarInt}; use bitcoin::hashes::{Hash, sha256}; -#[cfg(feature = "serde")] use serde::{Deserialize, Deserializer, Serialize, Serializer}; -#[cfg(feature = "serde")] use std::fmt; +#[cfg(feature = "serde-feature")] use serde::{Deserialize, Deserializer, Serialize, Serializer}; +#[cfg(feature = "serde-feature")] use std::fmt; use dynafed; use Transaction; @@ -49,7 +49,7 @@ pub enum ExtData { }, } -#[cfg(feature = "serde")] +#[cfg(feature = "serde-feature")] impl<'de> Deserialize<'de> for ExtData { fn deserialize>(d: D) -> Result { use serde::de; @@ -149,7 +149,7 @@ impl<'de> Deserialize<'de> for ExtData { } } -#[cfg(feature = "serde")] +#[cfg(feature = "serde-feature")] impl Serialize for ExtData { fn serialize(&self, s: S) -> Result { use serde::ser::SerializeStruct; diff --git a/src/confidential.rs b/src/confidential.rs index 0bdab9ce..4f980273 100644 --- a/src/confidential.rs +++ b/src/confidential.rs @@ -17,7 +17,7 @@ //! Structures representing Pedersen commitments of various types //! -#[cfg(feature = "serde")] +#[cfg(feature = "serde-feature")] use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::{io, fmt}; @@ -137,7 +137,7 @@ macro_rules! impl_confidential_commitment { } } - #[cfg(feature = "serde")] + #[cfg(feature = "serde-feature")] impl Serialize for $name { fn serialize(&self, s: S) -> Result { use serde::ser::SerializeSeq; @@ -161,7 +161,7 @@ macro_rules! impl_confidential_commitment { } } - #[cfg(feature = "serde")] + #[cfg(feature = "serde-feature")] impl<'de> Deserialize<'de> for $name { fn deserialize>(d: D) -> Result { use serde::de::{Error, Visitor, SeqAccess}; diff --git a/src/contracts.rs b/src/contracts.rs new file mode 100644 index 00000000..e8c20009 --- /dev/null +++ b/src/contracts.rs @@ -0,0 +1,536 @@ +//! Handling asset contracts. + +use std::collections::BTreeMap; +use std::{error, fmt, str}; + +use serde_cbor; +use serde_json; +use bitcoin::hashes::Hash; + +use issuance::{AssetId, ContractHash}; +use transaction::OutPoint; + +/// The maximum precision of an asset. +pub const MAX_PRECISION: u8 = 8; + +/// The maximum ticker string length. +pub const MAX_TICKER_LENGTH: usize = 5; + +/// The contract version byte for legacy JSON contracts. +const CONTRACT_VERSION_JSON: u8 = '{' as u8; + +/// The contract version byte for CBOR contracts. +const CONTRACT_VERSION_CBOR: u8 = 1; + +/// An asset contract error. +#[derive(Debug)] +pub enum Error { + /// The contract was empty. + Empty, + /// The CBOR format was invalid. + InvalidCbor(serde_cbor::Error), + /// the JSON format was invalid. + InvalidJson(serde_json::Error), + /// The contract's content are invalid. + InvalidContract(&'static str), + /// An unknown contract version was encountered. + UnknownVersion(u8), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Error::Empty => write!(f, "the contract was empty"), + Error::InvalidCbor(ref e) => write!(f, "invalid CBOR format: {}", e), + Error::InvalidJson(ref e) => write!(f, "invalid JSON format: {}", e), + Error::InvalidContract(ref e) => write!(f, "invalid contract: {}", e), + Error::UnknownVersion(v) => write!(f, "unknown contract version: {}", v), + } + } +} + +#[allow(deprecated)] +impl error::Error for Error { + fn cause(&self) -> Option<&error::Error> { + match *self { + Error::InvalidCbor(ref e) => Some(e), + Error::InvalidJson(ref e) => Some(e), + _ => None, + } + } + + fn description(&self) -> &str { + "description() is deprecated; use Display" + } +} + +/// The issuing entity of an asset. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)] +pub struct ContractDetailsEntity { + /// The domain name of the issuer. + pub domain: Option, +} + +/// Some well-known details encapsulated inside an asset contract. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ContractDetails { + /// The precision of the asset values. + pub precision: u8, + /// The ticker of the asset. + pub ticker: String, + + /// The name of the asset. + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + /// The issuing entity. + #[serde(skip_serializing_if = "Option::is_none")] + pub entity: Option, + /// The public key of the issuer. + #[serde(skip_serializing_if = "Option::is_none")] + pub issuer_pubkey: Option, +} + +/// The structure of a legacy (JSON) contract. +#[derive(Debug, Clone, Deserialize)] +struct LegacyContract { + precision: u8, + ticker: String, + #[serde(flatten)] + other: BTreeMap, +} + +/// The contents of an asset contract. +#[derive(Debug, Clone)] +enum Content { + Legacy(LegacyContract), + Modern { + precision: u8, + ticker: String, + //TODO(stevenroose) consider requiring String keys + other: BTreeMap, + }, +} + +/// Check a precision value. +#[inline] +fn check_precision>(p: P) -> Result<(), Error> { + if p < 0.into() || p > MAX_PRECISION.into() { + return Err(Error::InvalidContract("invalid precision")); + } + Ok(()) +} + +/// Check a ticker value. +#[inline] +fn check_ticker(t: &str) -> Result<(), Error> { + if t.len() > MAX_TICKER_LENGTH { + return Err(Error::InvalidContract("ticker too long")); + } + Ok(()) +} + +/// Check a key value. +#[inline] +fn check_key(k: &str) -> Result<(), Error> { + if !k.is_ascii() { + return Err(Error::InvalidContract("keys must be ASCII")); + } + Ok(()) +} + +impl Content { + fn from_bytes(contract: &[u8]) -> Result { + if contract.len() < 1 { + return Err(Error::Empty); + } + + if contract[0] == CONTRACT_VERSION_JSON { + let content: LegacyContract = + serde_json::from_slice(contract).map_err(Error::InvalidJson)?; + check_precision(content.precision)?; + check_ticker(&content.ticker)?; + for key in content.other.keys() { + check_key(key)?; + } + Ok(Content::Legacy(content)) + } else if contract[0] == CONTRACT_VERSION_CBOR { + let content: Vec = + serde_cbor::from_slice(&contract[1..]).map_err(Error::InvalidCbor)?; + if content.len() != 3 { + return Err(Error::InvalidContract("CBOR value must be array of 3 elements")); + } + let mut iter = content.into_iter(); + Ok(Content::Modern { + precision: if let serde_cbor::Value::Integer(i) = iter.next().unwrap() { + check_precision(i)?; + i as u8 + } else { + return Err(Error::InvalidContract("first CBOR value must be integer")); + }, + ticker: if let serde_cbor::Value::Text(t) = iter.next().unwrap() { + check_ticker(&t)?; + t + } else { + return Err(Error::InvalidContract("second CBOR value must be string")); + }, + other: if let serde_cbor::Value::Map(m) = iter.next().unwrap() { + let mut other = BTreeMap::new(); + for (key, value) in m.into_iter() { + // Use utility methods here after this PR is released: + // https://github.com/pyfisch/cbor/pull/191 + match key { + serde_cbor::Value::Text(t) => { + check_key(&t)?; + other.insert(t, value) + }, + _ => return Err(Error::InvalidContract("keys must be strings")), + }; + } + other + } else { + return Err(Error::InvalidContract("third CBOR value must be map")); + }, + }) + } else { + Err(Error::UnknownVersion(contract[0])) + } + } +} + +/// An asset contract. +#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub struct Contract(Vec); + +impl Contract { + /// Generate a contract from the contract details. + pub fn from_details( + mut details: ContractDetails, + extra_fields: BTreeMap, + ) -> Result { + check_precision(details.precision)?; + check_ticker(&details.ticker)?; + + // Add known fields from details. + let mut props = BTreeMap::new(); + if let Some(name) = details.name.take() { + props.insert("name".to_owned().into(), name.into()); + } + if let Some(mut entity) = details.entity.take() { + let mut ent = BTreeMap::new(); + if let Some(domain) = entity.domain.take() { + ent.insert("domain".to_owned().into(), domain.into()); + } + props.insert("entity".to_owned().into(), ent.into()); + } + if let Some(issuer_pubkey) = details.issuer_pubkey.take() { + props.insert("issuer_pubkey".to_owned().into(), issuer_pubkey.to_bytes().into()); + } + + // Add extra fields. + for (key, value) in extra_fields.into_iter() { + //TODO(stevenroose) should we check the keys of all recursive objects? + check_key(&key)?; + if props.insert(key.into(), value).is_some() { + return Err(Error::InvalidContract("extra field reused key from details")); + } + } + + let cbor: Vec = vec![ + details.precision.into(), + details.ticker.into(), + props.into(), + ]; + + let mut buffer = vec![CONTRACT_VERSION_CBOR]; + serde_cbor::to_writer(&mut buffer, &cbor).map_err(Error::InvalidCbor)?; + Ok(Contract(buffer)) + } + + /// Generate a legacy contract from the contract details. + #[deprecated] + pub fn legacy_from_details( + mut details: ContractDetails, + extra_fields: BTreeMap, + ) -> Result { + check_precision(details.precision)?; + check_ticker(&details.ticker)?; + + // We will use the extra_fields hashmap to serialize the JSON later. + for key in extra_fields.keys() { + check_key(key)?; + } + let mut props = extra_fields; + + // Add known fields from details. + if props.insert("precision".into(), details.precision.into()).is_some() { + return Err(Error::InvalidContract("extra field reused key from details")); + } + if props.insert("ticker".into(), details.ticker.into()).is_some() { + return Err(Error::InvalidContract("extra field reused key from details")); + } + if let Some(name) = details.name.take() { + if props.insert("name".into(), name.into()).is_some() { + return Err(Error::InvalidContract("extra field reused key from details")); + } + } + if let Some(entity) = details.entity.take() { + if props.insert("entity".into(), serde_json::to_value(&entity).unwrap()).is_some() { + return Err(Error::InvalidContract("extra field reused key from details")); + } + } + if let Some(issuer_pubkey) = details.issuer_pubkey.take() { + if props.insert("issuer_pubkey".into(), issuer_pubkey.to_string().into()).is_some() { + return Err(Error::InvalidContract("extra field reused key from details")); + } + } + + Ok(Contract(serde_json::to_vec(&props).map_err(Error::InvalidJson)?)) + } + + /// Parse an asset contract from bytes. + pub fn from_bytes(contract: &[u8]) -> Result { + // Check for validity and then store raw contract. + let _ = Content::from_bytes(contract)?; + Ok(Contract(contract.to_vec())) + } + + /// Get the binary representation of the asset contract. + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } + + /// Get the contract hash of this asset contract. + pub fn contract_hash(&self) -> ContractHash { + ContractHash::hash(self.as_bytes()) + } + + /// Calculate the asset ID of an asset issued with this contract. + pub fn asset_id(&self, prevout: OutPoint) -> AssetId { + AssetId::from_entropy(AssetId::generate_asset_entropy(prevout, self.contract_hash())) + } + + /// Get the precision of the asset. + pub fn precision(&self) -> u8 { + match Content::from_bytes(&self.as_bytes()).expect("invariant") { + Content::Legacy(c) => c.precision, + Content::Modern { precision, .. } => precision, + } + } + + /// Get the ticker of the asset. + pub fn ticker(&self) -> String { + match Content::from_bytes(&self.as_bytes()).expect("invariant") { + Content::Legacy(c) => c.ticker, + Content::Modern { ticker, .. } => ticker, + } + } + + /// Retrieve a property from the contract. + /// For precision and ticker, use the designated methods instead. + pub fn property(&self, key: &str) -> Result, Error> { + match Content::from_bytes(&self.as_bytes()).expect("invariant") { + Content::Legacy(c) => { + let value = match c.other.get(key) { + Some(v) => v, + None => return Ok(None), + }; + Ok(serde_json::from_value(value.clone()).map_err(Error::InvalidJson)?) + }, + Content::Modern { other, .. } => { + let value = match other.get(key) { + Some(v) => v, + None => return Ok(None), + }; + //TODO(stevenroose) optimize this when serde_cbor implements from_value + let bytes = serde_cbor::to_vec(&value).map_err(Error::InvalidCbor)?; + Ok(serde_cbor::from_slice(&bytes).map_err(Error::InvalidCbor)?) + }, + } + } +} + +impl fmt::Display for Contract { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // We will display legacy contracts as JSON and others as hex. + if self.as_bytes()[0] == CONTRACT_VERSION_JSON { + write!(f, "{}", str::from_utf8(self.as_bytes()).expect("invariant")) + } else { + for b in self.as_bytes() { + write!(f, "{:02x}", b)?; + } + Ok(()) + } + } +} + +impl fmt::Debug for Contract { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Contract({:?})", Content::from_bytes(self.as_bytes()).expect("invariant")) + } +} + +#[cfg(test)] +mod test { + use super::*; + use bitcoin::hashes::hex::FromHex; + use std::str::FromStr; + + /// A shorthand method for testing tether properties. + fn assert_has_tether_properties(contract: &Contract) { + assert_eq!(contract.precision(), 8); + assert_eq!(contract.ticker(), "USDt".to_owned()); + + assert_eq!(contract.property("name").unwrap(), Some("Tether USD".to_owned())); + assert_eq!(contract.property("issuer_pubkey").unwrap(), + Some(bitcoin::PublicKey::from_str("0337cceec0beea0232ebe14cba0197a9fbd45fcf2ec946749de920e71434c2b904").unwrap()), + ); + + #[derive(Debug, PartialEq, Eq, Deserialize)] + struct Entity { + pub domain: String, + } + assert_eq!(contract.property("entity").unwrap(), + Some(Entity { domain: "tether.to".into() }), + ); + } + + #[test] + fn test_legacy_parsing() { + let correct = r#"{"entity":{"domain":"tether.to"},"issuer_pubkey":"0337cceec0beea0232ebe14cba0197a9fbd45fcf2ec946749de920e71434c2b904","name":"Tether USD","precision":8,"ticker":"USDt","version":0}"#; + assert!(Contract::from_bytes(correct.as_bytes()).is_ok()); + + let invalid = [ + // missing precision + r#"{"entity":{"domain":"tether.to"},"issuer_pubkey":"0337cceec0beea0232ebe14cba0197a9fbd45fcf2ec946749de920e71434c2b904","name":"Tether USD","ticker":"USDt","version":0}"#, + // precision is string + r#"{"entity":{"domain":"tether.to"},"issuer_pubkey":"0337cceec0beea0232ebe14cba0197a9fbd45fcf2ec946749de920e71434c2b904","name":"Tether USD","precision":"no","ticker":"USDt","version":0}"#, + // negative precision + r#"{"entity":{"domain":"tether.to"},"issuer_pubkey":"0337cceec0beea0232ebe14cba0197a9fbd45fcf2ec946749de920e71434c2b904","name":"Tether USD","precision":-2,"ticker":"USDt","version":0}"#, + // too high precision + r#"{"entity":{"domain":"tether.to"},"issuer_pubkey":"0337cceec0beea0232ebe14cba0197a9fbd45fcf2ec946749de920e71434c2b904","name":"Tether USD","precision":9,"ticker":"USDt","version":0}"#, + // missing ticker + r#"{"entity":{"domain":"tether.to"},"issuer_pubkey":"0337cceec0beea0232ebe14cba0197a9fbd45fcf2ec946749de920e71434c2b904","name":"Tether USD","precision":8,"version":0}"#, + // ticker is int + r#"{"entity":{"domain":"tether.to"},"issuer_pubkey":"0337cceec0beea0232ebe14cba0197a9fbd45fcf2ec946749de920e71434c2b904","name":"Tether USD","precision":8,"ticker":8,"version":0}"#, + // ticker too long + r#"{"entity":{"domain":"tether.to"},"issuer_pubkey":"0337cceec0beea0232ebe14cba0197a9fbd45fcf2ec946749de920e71434c2b904","name":"Tether USD","precision":8,"ticker":"USDtether","version":0}"#, + ]; + for json in &invalid { + assert!(Contract::from_bytes(json.as_bytes()).is_err(), "invalid JSON was accepted: {}", json); + } + } + + #[test] + fn test_tether() { + let json = r#"{"entity":{"domain":"tether.to"},"issuer_pubkey":"0337cceec0beea0232ebe14cba0197a9fbd45fcf2ec946749de920e71434c2b904","name":"Tether USD","precision":8,"ticker":"USDt","version":0}"#; + let tether_id = AssetId::from_str("ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2").unwrap(); + let tether_prevout = OutPoint::from_str("9596d259270ef5bac0020435e6d859aea633409483ba64e232b8ba04ce288668:0").unwrap(); + let tether_contract_hash = ContractHash::from_hex("3c7f0a53c2ff5b99590620d7f6604a7a3a7bfbaaa6aa61f7bfc7833ca03cde82").unwrap(); + + let contract = Contract::from_bytes(json.as_bytes()).unwrap(); + assert_eq!(contract.contract_hash(), tether_contract_hash); + assert_eq!(contract.asset_id(tether_prevout), tether_id); + assert_has_tether_properties(&contract); + } + + #[test] + fn test_create_cbor() { + let details = ContractDetails { + precision: 8, + ticker: "USDt".into(), + name: Some("Tether USD".into()), + entity: Some(ContractDetailsEntity { + domain: Some("tether.to".into()), + }), + issuer_pubkey: Some("0337cceec0beea0232ebe14cba0197a9fbd45fcf2ec946749de920e71434c2b904".parse().unwrap()), + }; + let mut extra = BTreeMap::new(); + extra.insert("foo".to_owned(), "bar".to_owned().into()); + let contract = Contract::from_details(details.clone(), extra.clone()).unwrap(); + + assert_has_tether_properties(&contract); + assert_eq!(contract.property("foo").unwrap(), Some("bar".to_owned())); + + // Some wrong values + let mut det = details.clone(); + det.precision = 9; + assert!(Contract::from_details(det, extra.clone()).is_err()); + + let mut det = details.clone(); + det.ticker = "TICKER".into(); + assert!(Contract::from_details(det, extra.clone()).is_err()); + + let mut ex = extra.clone(); + ex.insert("name".to_owned(), "Not Tether USD".to_owned().into()); + assert!(Contract::from_details(details.clone(), ex).is_err()); + } + + #[test] + #[allow(deprecated)] + fn test_create_legacy() { + let details = ContractDetails { + precision: 8, + ticker: "USDt".into(), + name: Some("Tether USD".into()), + entity: Some(ContractDetailsEntity { + domain: Some("tether.to".into()), + }), + issuer_pubkey: Some("0337cceec0beea0232ebe14cba0197a9fbd45fcf2ec946749de920e71434c2b904".parse().unwrap()), + }; + let mut extra = BTreeMap::new(); + extra.insert("foo".to_owned(), "bar".into()); + let contract = Contract::legacy_from_details(details.clone(), extra.clone()).unwrap(); + + assert_has_tether_properties(&contract); + assert_eq!(contract.property("foo").unwrap(), Some("bar".to_owned())); + + // Some wrong values + let mut det = details.clone(); + det.precision = 9; + assert!(Contract::legacy_from_details(det, extra.clone()).is_err()); + + let mut det = details.clone(); + det.ticker = "TICKER".into(); + assert!(Contract::legacy_from_details(det, extra.clone()).is_err()); + + let mut ex = extra.clone(); + ex.insert("name".to_owned(), "Not Tether USD".into()); + assert!(Contract::legacy_from_details(details.clone(), ex).is_err()); + } + + #[test] + fn test_cbor_wip() { + #[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] + struct Entity { + pub domain: String, + } + #[derive(Debug, Serialize)] + struct ContractExtraContent { + pub entity: Entity, + pub name: String, + pub issuer_pubkey: bitcoin::PublicKey, + } + + let extra = ContractExtraContent { + entity: Entity { + domain: "tether.to".into(), + }, + name: "Tether USD".into(), + issuer_pubkey: "0337cceec0beea0232ebe14cba0197a9fbd45fcf2ec946749de920e71434c2b904".parse().unwrap(), + }; + let cbor_content: Vec = vec![ + 8.into(), + "USDt".to_owned().into(), + //TODO(stevenroose) optimize this as serde_cbor gets to_value + serde_cbor::from_slice::(&serde_cbor::to_vec(&extra).unwrap()).unwrap(), + ]; + + // version byte + let mut buffer = vec![CONTRACT_VERSION_CBOR]; + serde_cbor::to_writer(&mut buffer, &cbor_content).unwrap(); + let contract = Contract::from_bytes(&buffer).unwrap(); + + assert_eq!(contract.contract_hash(), ContractHash::hash(&buffer)); + assert_has_tether_properties(&contract); + } +} diff --git a/src/dynafed.rs b/src/dynafed.rs index 2bab7636..b2409125 100644 --- a/src/dynafed.rs +++ b/src/dynafed.rs @@ -18,8 +18,8 @@ use std::io; use bitcoin; use bitcoin::hashes::{Hash, sha256, sha256d}; -#[cfg(feature = "serde")] use serde::{Deserialize, Deserializer, Serialize, Serializer}; -#[cfg(feature = "serde")] use std::fmt; +#[cfg(feature = "serde-feature")] use serde::{Deserialize, Deserializer, Serialize, Serializer}; +#[cfg(feature = "serde-feature")] use std::fmt; use encode::{self, Encodable, Decodable}; @@ -216,7 +216,7 @@ impl Params { } } -#[cfg(feature = "serde")] +#[cfg(feature = "serde-feature")] impl<'de> Deserialize<'de> for Params { fn deserialize>(d: D) -> Result { use serde::de; @@ -355,7 +355,7 @@ impl<'de> Deserialize<'de> for Params { } } -#[cfg(feature = "serde")] +#[cfg(feature = "serde-feature")] impl Serialize for Params { fn serialize(&self, s: S) -> Result { use serde::ser::SerializeStruct; diff --git a/src/encode.rs b/src/encode.rs index c1e663f0..308de303 100644 --- a/src/encode.rs +++ b/src/encode.rs @@ -57,6 +57,7 @@ impl fmt::Display for Error { } } +#[allow(deprecated)] impl error::Error for Error { fn cause(&self) -> Option<&error::Error> { match *self { @@ -66,7 +67,7 @@ impl error::Error for Error { } fn description(&self) -> &str { - "an Elements encoding error" + "description() is deprecated; use Display" } } diff --git a/src/internal_macros.rs b/src/internal_macros.rs index e9d7c9b3..df0d99da 100644 --- a/src/internal_macros.rs +++ b/src/internal_macros.rs @@ -36,7 +36,7 @@ macro_rules! impl_consensus_encoding { macro_rules! serde_struct_impl { ($name:ident, $($fe:ident),*) => ( - #[cfg(feature = "serde")] + #[cfg(feature = "serde-feature")] impl<'de> $crate::serde::Deserialize<'de> for $name { fn deserialize(deserializer: D) -> Result<$name, D::Error> where @@ -131,7 +131,7 @@ macro_rules! serde_struct_impl { } } - #[cfg(feature = "serde")] + #[cfg(feature = "serde-feature")] impl $crate::serde::Serialize for $name { fn serialize(&self, serializer: S) -> Result where diff --git a/src/issuance.rs b/src/issuance.rs index 3989b21e..1f3de58e 100644 --- a/src/issuance.rs +++ b/src/issuance.rs @@ -39,26 +39,6 @@ const TWO32: [u8; 32] = [ hash_newtype!(ContractHash, sha256::Hash, 32, doc="The hash of an asset contract.", true); -impl ContractHash { - /// Calculate the contract hash of a JSON contract object. - /// - /// This method does not perform any validation of the contents of the contract. - /// After basic JSON syntax validation, the object is formatted in a standard way to calculate - /// the hash. - #[cfg(feature = "json-contract")] - pub fn from_json_contract(json: &str) -> Result { - // Parsing the JSON into a BTreeMap will recursively order object keys - // lexicographically. This order is respected when we later serialize - // it again. - let ordered: ::std::collections::BTreeMap = - ::serde_json::from_str(json)?; - - let mut engine = ContractHash::engine(); - ::serde_json::to_writer(&mut engine, &ordered).expect("engines don't error"); - Ok(ContractHash::from_engine(engine)) - } -} - /// An issued asset ID. #[derive(Copy, Clone, PartialEq, Eq, Default, PartialOrd, Ord, Hash)] pub struct AssetId(sha256::Midstate); @@ -262,34 +242,4 @@ mod test { let token_id = AssetId::from_hex(token_id_hex).unwrap(); assert_eq!(AssetId::reissuance_token_from_entropy(entropy, false), token_id); } - - #[cfg(feature = "json-contract")] - #[test] - fn test_json_contract() { - let tether = ContractHash::from_hex("3c7f0a53c2ff5b99590620d7f6604a7a3a7bfbaaa6aa61f7bfc7833ca03cde82").unwrap(); - - let correct = r#"{"entity":{"domain":"tether.to"},"issuer_pubkey":"0337cceec0beea0232ebe14cba0197a9fbd45fcf2ec946749de920e71434c2b904","name":"Tether USD","precision":8,"ticker":"USDt","version":0}"#; - let expected = ContractHash::hash(correct.as_bytes()); - assert_eq!(tether, expected); - assert_eq!(expected, ContractHash::from_json_contract(&correct).unwrap()); - - let invalid_json = r#"{"entity":{"domain":"tether.to"},"issuer_pubkey:"#; - assert!(ContractHash::from_json_contract(&invalid_json).is_err()); - - let unordered = r#"{"precision":8,"ticker":"USDt","entity":{"domain":"tether.to"},"issuer_pubkey":"0337cceec0beea0232ebe14cba0197a9fbd45fcf2ec946749de920e71434c2b904","name":"Tether USD","version":0}"#; - assert_eq!(expected, ContractHash::from_json_contract(&unordered).unwrap()); - - let unordered = r#"{"precision":8,"name":"Tether USD","ticker":"USDt","entity":{"domain":"tether.to"},"issuer_pubkey":"0337cceec0beea0232ebe14cba0197a9fbd45fcf2ec946749de920e71434c2b904","version":0}"#; - assert_eq!(expected, ContractHash::from_json_contract(&unordered).unwrap()); - - let spaces = r#"{"precision":8, "name" : "Tether USD", "ticker":"USDt", "entity":{"domain":"tether.to" }, "issuer_pubkey" :"0337cceec0beea0232ebe14cba0197a9fbd45fcf2ec946749de920e71434c2b904","version":0} "#; - assert_eq!(expected, ContractHash::from_json_contract(&spaces).unwrap()); - - let nested_correct = r#"{"entity":{"author":"Tether Inc","copyright":2020,"domain":"tether.to","hq":"Mars"},"issuer_pubkey":"0337cceec0beea0232ebe14cba0197a9fbd45fcf2ec946749de920e71434c2b904","name":"Tether USD","precision":8,"ticker":"USDt","version":0}"#; - let nested_expected = ContractHash::hash(nested_correct.as_bytes()); - assert_eq!(nested_expected, ContractHash::from_json_contract(&nested_correct).unwrap()); - - let nested_unordered = r#"{"ticker":"USDt","entity":{"domain":"tether.to","hq":"Mars","author":"Tether Inc","copyright":2020},"issuer_pubkey":"0337cceec0beea0232ebe14cba0197a9fbd45fcf2ec946749de920e71434c2b904","name":"Tether USD","precision":8,"version":0}"#; - assert_eq!(nested_expected, ContractHash::from_json_contract(&nested_unordered).unwrap()); - } } diff --git a/src/lib.rs b/src/lib.rs index e26d2f68..c925d1d9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,16 +28,19 @@ pub extern crate bitcoin; #[macro_use] pub extern crate bitcoin_hashes; -#[cfg(feature = "serde")] extern crate serde; +#[cfg(feature = "serde")] #[macro_use] extern crate serde; +#[cfg(feature = "serde_cbor")] extern crate serde_cbor; +#[cfg(feature = "serde_json")] extern crate serde_json; #[cfg(test)] extern crate rand; -#[cfg(any(test, feature = "serde_json"))] extern crate serde_json; #[macro_use] mod internal_macros; pub mod address; pub mod blech32; mod block; pub mod confidential; +#[cfg(feature = "contracts")] +pub mod contracts; pub mod dynafed; pub mod encode; mod fast_merkle_root;