|
| 1 | +//! Handling asset contracts. |
| 2 | +
|
| 3 | +use std::collections::BTreeMap; |
| 4 | +use std::{error, fmt, str}; |
| 5 | + |
| 6 | +use serde_cbor; |
| 7 | +use serde_json; |
| 8 | +use bitcoin::hashes::Hash; |
| 9 | + |
| 10 | +use issuance::{AssetId, ContractHash}; |
| 11 | +use transaction::OutPoint; |
| 12 | + |
| 13 | +/// The maximum precision of an asset. |
| 14 | +pub const MAX_PRECISION: u8 = 8; |
| 15 | + |
| 16 | +/// The maximum ticker string length. |
| 17 | +pub const MAX_TICKER_LENGTH: usize = 5; |
| 18 | + |
| 19 | +/// The contract version byte for legacy JSON contracts. |
| 20 | +const CONTRACT_VERSION_JSON: u8 = '{' as u8; |
| 21 | + |
| 22 | +/// The contract version byte for CBOR contracts. |
| 23 | +const CONTRACT_VERSION_CBOR: u8 = 1; |
| 24 | + |
| 25 | +/// An asset contract error. |
| 26 | +#[derive(Debug)] |
| 27 | +pub enum Error { |
| 28 | + /// The contract was empty. |
| 29 | + Empty, |
| 30 | + /// The CBOR format was invalid. |
| 31 | + InvalidCbor(serde_cbor::Error), |
| 32 | + /// the JSON format was invalid. |
| 33 | + InvalidJson(serde_json::Error), |
| 34 | + /// The contract's content are invalid. |
| 35 | + InvalidContract(&'static str), |
| 36 | + /// An unknown contract version was encountered. |
| 37 | + UnknownVersion(u8), |
| 38 | +} |
| 39 | + |
| 40 | +impl fmt::Display for Error { |
| 41 | + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
| 42 | + match *self { |
| 43 | + Error::Empty => write!(f, "the contract was empty"), |
| 44 | + Error::InvalidCbor(ref e) => write!(f, "invalid CBOR format: {}", e), |
| 45 | + Error::InvalidJson(ref e) => write!(f, "invalid JSON format: {}", e), |
| 46 | + Error::InvalidContract(ref e) => write!(f, "invalid contract: {}", e), |
| 47 | + Error::UnknownVersion(v) => write!(f, "unknown contract version: {}", v), |
| 48 | + } |
| 49 | + } |
| 50 | +} |
| 51 | + |
| 52 | +#[allow(deprecated)] |
| 53 | +impl error::Error for Error { |
| 54 | + fn cause(&self) -> Option<&error::Error> { |
| 55 | + match *self { |
| 56 | + Error::InvalidCbor(ref e) => Some(e), |
| 57 | + Error::InvalidJson(ref e) => Some(e), |
| 58 | + _ => None, |
| 59 | + } |
| 60 | + } |
| 61 | + |
| 62 | + fn description(&self) -> &str { |
| 63 | + "description() is deprecated; use Display" |
| 64 | + } |
| 65 | +} |
| 66 | + |
| 67 | +/// The issuing entity of an asset. |
| 68 | +#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)] |
| 69 | +pub struct ContractDetailsEntity { |
| 70 | + /// The domain name of the issuer. |
| 71 | + pub domain: Option<String>, |
| 72 | +} |
| 73 | + |
| 74 | +/// Some well-known details encapsulated inside an asset contract. |
| 75 | +#[derive(Debug, Clone, Serialize, Deserialize)] |
| 76 | +pub struct ContractDetails { |
| 77 | + /// The precision of the asset values. |
| 78 | + pub precision: u8, |
| 79 | + /// The ticker of the asset. |
| 80 | + pub ticker: String, |
| 81 | + |
| 82 | + /// The name of the asset. |
| 83 | + #[serde(skip_serializing_if = "Option::is_none")] |
| 84 | + pub name: Option<String>, |
| 85 | + /// The issuing entity. |
| 86 | + #[serde(skip_serializing_if = "Option::is_none")] |
| 87 | + pub entity: Option<ContractDetailsEntity>, |
| 88 | + /// The public key of the issuer. |
| 89 | + #[serde(skip_serializing_if = "Option::is_none")] |
| 90 | + pub issuer_pubkey: Option<bitcoin::PublicKey>, |
| 91 | +} |
| 92 | + |
| 93 | +/// The structure of a legacy (JSON) contract. |
| 94 | +#[derive(Debug, Clone, Deserialize)] |
| 95 | +struct LegacyContract { |
| 96 | + precision: u8, |
| 97 | + ticker: String, |
| 98 | + #[serde(flatten)] |
| 99 | + other: BTreeMap<String, serde_json::Value>, |
| 100 | +} |
| 101 | + |
| 102 | +/// The contents of an asset contract. |
| 103 | +#[derive(Debug, Clone)] |
| 104 | +enum Content { |
| 105 | + Legacy(LegacyContract), |
| 106 | + Modern { |
| 107 | + precision: u8, |
| 108 | + ticker: String, |
| 109 | + //TODO(stevenroose) consider requiring String keys |
| 110 | + other: BTreeMap<String, serde_cbor::Value>, |
| 111 | + }, |
| 112 | +} |
| 113 | + |
| 114 | +/// Check a precision value. |
| 115 | +#[inline] |
| 116 | +fn check_precision<P: PartialOrd + From<u8>>(p: P) -> Result<(), Error> { |
| 117 | + if p < 0.into() || p > MAX_PRECISION.into() { |
| 118 | + return Err(Error::InvalidContract("invalid precision")); |
| 119 | + } |
| 120 | + Ok(()) |
| 121 | +} |
| 122 | + |
| 123 | +/// Check a ticker value. |
| 124 | +#[inline] |
| 125 | +fn check_ticker(t: &str) -> Result<(), Error> { |
| 126 | + if t.len() > MAX_TICKER_LENGTH { |
| 127 | + return Err(Error::InvalidContract("ticker too long")); |
| 128 | + } |
| 129 | + Ok(()) |
| 130 | +} |
| 131 | + |
| 132 | +/// Check a key value. |
| 133 | +#[inline] |
| 134 | +fn check_key(k: &str) -> Result<(), Error> { |
| 135 | + if !k.is_ascii() { |
| 136 | + return Err(Error::InvalidContract("keys must be ASCII")); |
| 137 | + } |
| 138 | + Ok(()) |
| 139 | +} |
| 140 | + |
| 141 | +impl Content { |
| 142 | + fn from_bytes(contract: &[u8]) -> Result<Content, Error> { |
| 143 | + if contract.len() < 1 { |
| 144 | + return Err(Error::Empty); |
| 145 | + } |
| 146 | + |
| 147 | + if contract[0] == CONTRACT_VERSION_JSON { |
| 148 | + let content: LegacyContract = |
| 149 | + serde_json::from_slice(contract).map_err(Error::InvalidJson)?; |
| 150 | + check_precision(content.precision)?; |
| 151 | + check_ticker(&content.ticker)?; |
| 152 | + for key in content.other.keys() { |
| 153 | + check_key(key)?; |
| 154 | + } |
| 155 | + Ok(Content::Legacy(content)) |
| 156 | + } else if contract[0] == CONTRACT_VERSION_CBOR { |
| 157 | + let content: Vec<serde_cbor::Value> = |
| 158 | + serde_cbor::from_slice(&contract[1..]).map_err(Error::InvalidCbor)?; |
| 159 | + if content.len() != 3 { |
| 160 | + return Err(Error::InvalidContract("CBOR value must be array of 3 elements")); |
| 161 | + } |
| 162 | + let mut iter = content.into_iter(); |
| 163 | + Ok(Content::Modern { |
| 164 | + precision: if let serde_cbor::Value::Integer(i) = iter.next().unwrap() { |
| 165 | + check_precision(i)?; |
| 166 | + i as u8 |
| 167 | + } else { |
| 168 | + return Err(Error::InvalidContract("first CBOR value must be integer")); |
| 169 | + }, |
| 170 | + ticker: if let serde_cbor::Value::Text(t) = iter.next().unwrap() { |
| 171 | + check_ticker(&t)?; |
| 172 | + t |
| 173 | + } else { |
| 174 | + return Err(Error::InvalidContract("second CBOR value must be string")); |
| 175 | + }, |
| 176 | + other: if let serde_cbor::Value::Map(m) = iter.next().unwrap() { |
| 177 | + let mut other = BTreeMap::new(); |
| 178 | + for (key, value) in m.into_iter() { |
| 179 | + // Use utility methods here after this PR is released: |
| 180 | + // https://github.com/pyfisch/cbor/pull/191 |
| 181 | + match key { |
| 182 | + serde_cbor::Value::Text(t) => { |
| 183 | + check_key(&t)?; |
| 184 | + other.insert(t, value) |
| 185 | + }, |
| 186 | + _ => return Err(Error::InvalidContract("keys must be strings")), |
| 187 | + }; |
| 188 | + } |
| 189 | + other |
| 190 | + } else { |
| 191 | + return Err(Error::InvalidContract("third CBOR value must be map")); |
| 192 | + }, |
| 193 | + }) |
| 194 | + } else { |
| 195 | + Err(Error::UnknownVersion(contract[0])) |
| 196 | + } |
| 197 | + } |
| 198 | +} |
| 199 | + |
| 200 | +/// An asset contract. |
| 201 | +#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] |
| 202 | +pub struct Contract(Vec<u8>); |
| 203 | + |
| 204 | +impl Contract { |
| 205 | + /// Generate a contract from the contract details. |
| 206 | + pub fn from_details( |
| 207 | + mut details: ContractDetails, |
| 208 | + extra_fields: BTreeMap<String, serde_cbor::Value>, |
| 209 | + ) -> Result<Contract, Error> { |
| 210 | + check_precision(details.precision)?; |
| 211 | + check_ticker(&details.ticker)?; |
| 212 | + |
| 213 | + // Add known fields from details. |
| 214 | + let mut props = BTreeMap::new(); |
| 215 | + if let Some(name) = details.name.take() { |
| 216 | + props.insert("name".to_owned().into(), name.into()); |
| 217 | + } |
| 218 | + if let Some(mut entity) = details.entity.take() { |
| 219 | + let mut ent = BTreeMap::new(); |
| 220 | + if let Some(domain) = entity.domain.take() { |
| 221 | + ent.insert("domain".to_owned().into(), domain.into()); |
| 222 | + } |
| 223 | + props.insert("entity".to_owned().into(), ent.into()); |
| 224 | + } |
| 225 | + if let Some(issuer_pubkey) = details.issuer_pubkey.take() { |
| 226 | + props.insert("issuer_pubkey".to_owned().into(), issuer_pubkey.to_bytes().into()); |
| 227 | + } |
| 228 | + |
| 229 | + // Add extra fields. |
| 230 | + for (key, value) in extra_fields.into_iter() { |
| 231 | + check_key(&key)?; |
| 232 | + if props.insert(key.into(), value).is_some() { |
| 233 | + return Err(Error::InvalidContract("extra field reused key from details")); |
| 234 | + } |
| 235 | + } |
| 236 | + |
| 237 | + let cbor: Vec<serde_cbor::Value> = vec![ |
| 238 | + details.precision.into(), |
| 239 | + details.ticker.into(), |
| 240 | + props.into(), |
| 241 | + ]; |
| 242 | + |
| 243 | + let mut buffer = vec![CONTRACT_VERSION_CBOR]; |
| 244 | + serde_cbor::to_writer(&mut buffer, &cbor).map_err(Error::InvalidCbor)?; |
| 245 | + Ok(Contract(buffer)) |
| 246 | + } |
| 247 | + |
| 248 | + /// Generate a legacy contract from the contract details. |
| 249 | + #[deprecated] |
| 250 | + pub fn legacy_from_details( |
| 251 | + mut details: ContractDetails, |
| 252 | + extra_fields: BTreeMap<String, serde_json::Value>, |
| 253 | + ) -> Result<Contract, Error> { |
| 254 | + check_precision(details.precision)?; |
| 255 | + check_ticker(&details.ticker)?; |
| 256 | + |
| 257 | + // We will use the extra_fields hashmap to serialize the JSON later. |
| 258 | + for key in extra_fields.keys() { |
| 259 | + check_key(key)?; |
| 260 | + } |
| 261 | + let mut props = extra_fields; |
| 262 | + |
| 263 | + // Add known fields from details. |
| 264 | + if props.insert("precision".into(), details.precision.into()).is_some() { |
| 265 | + return Err(Error::InvalidContract("extra field reused key from details")); |
| 266 | + } |
| 267 | + if props.insert("ticker".into(), details.ticker.into()).is_some() { |
| 268 | + return Err(Error::InvalidContract("extra field reused key from details")); |
| 269 | + } |
| 270 | + if let Some(name) = details.name.take() { |
| 271 | + if props.insert("name".into(), name.into()).is_some() { |
| 272 | + return Err(Error::InvalidContract("extra field reused key from details")); |
| 273 | + } |
| 274 | + } |
| 275 | + if let Some(entity) = details.entity.take() { |
| 276 | + if props.insert("entity".into(), serde_json::to_value(&entity).unwrap()).is_some() { |
| 277 | + return Err(Error::InvalidContract("extra field reused key from details")); |
| 278 | + } |
| 279 | + } |
| 280 | + if let Some(issuer_pubkey) = details.issuer_pubkey.take() { |
| 281 | + if props.insert("issuer_pubkey".into(), issuer_pubkey.to_string().into()).is_some() { |
| 282 | + return Err(Error::InvalidContract("extra field reused key from details")); |
| 283 | + } |
| 284 | + } |
| 285 | + |
| 286 | + Ok(Contract(serde_json::to_vec(&props).map_err(Error::InvalidJson)?)) |
| 287 | + } |
| 288 | + |
| 289 | + /// Parse an asset contract from bytes. |
| 290 | + pub fn from_bytes(contract: &[u8]) -> Result<Contract, Error> { |
| 291 | + // Check for validity and then store raw contract. |
| 292 | + let _ = Content::from_bytes(contract)?; |
| 293 | + Ok(Contract(contract.to_vec())) |
| 294 | + } |
| 295 | + |
| 296 | + /// Get the binary representation of the asset contract. |
| 297 | + pub fn as_bytes(&self) -> &[u8] { |
| 298 | + &self.0 |
| 299 | + } |
| 300 | + |
| 301 | + /// Get the contract hash of this asset contract. |
| 302 | + pub fn contract_hash(&self) -> ContractHash { |
| 303 | + ContractHash::hash(self.as_bytes()) |
| 304 | + } |
| 305 | + |
| 306 | + /// Calculate the asset ID of an asset issued with this contract. |
| 307 | + pub fn asset_id(&self, prevout: OutPoint) -> AssetId { |
| 308 | + AssetId::from_entropy(AssetId::generate_asset_entropy(prevout, self.contract_hash())) |
| 309 | + } |
| 310 | + |
| 311 | + /// Get the precision of the asset. |
| 312 | + pub fn precision(&self) -> u8 { |
| 313 | + match Content::from_bytes(&self.as_bytes()).expect("invariant") { |
| 314 | + Content::Legacy(c) => c.precision, |
| 315 | + Content::Modern { precision, .. } => precision, |
| 316 | + } |
| 317 | + } |
| 318 | + |
| 319 | + /// Get the ticker of the asset. |
| 320 | + pub fn ticker(&self) -> String { |
| 321 | + match Content::from_bytes(&self.as_bytes()).expect("invariant") { |
| 322 | + Content::Legacy(c) => c.ticker, |
| 323 | + Content::Modern { ticker, .. } => ticker, |
| 324 | + } |
| 325 | + } |
| 326 | + |
| 327 | + /// Retrieve a property from the contract. |
| 328 | + /// For precision and ticker, use the designated methods instead. |
| 329 | + pub fn property<T: serde::de::DeserializeOwned>(&self, key: &str) -> Result<Option<T>, Error> { |
| 330 | + match Content::from_bytes(&self.as_bytes()).expect("invariant") { |
| 331 | + Content::Legacy(c) => { |
| 332 | + let value = match c.other.get(key) { |
| 333 | + Some(v) => v, |
| 334 | + None => return Ok(None), |
| 335 | + }; |
| 336 | + Ok(serde_json::from_value(value.clone()).map_err(Error::InvalidJson)?) |
| 337 | + }, |
| 338 | + Content::Modern { other, .. } => { |
| 339 | + let value = match other.get(key) { |
| 340 | + Some(v) => v, |
| 341 | + None => return Ok(None), |
| 342 | + }; |
| 343 | + //TODO(stevenroose) optimize this when serde_cbor implements from_value |
| 344 | + let bytes = serde_cbor::to_vec(&value).map_err(Error::InvalidCbor)?; |
| 345 | + Ok(serde_cbor::from_slice(&bytes).map_err(Error::InvalidCbor)?) |
| 346 | + }, |
| 347 | + } |
| 348 | + } |
| 349 | +} |
| 350 | + |
| 351 | +impl fmt::Display for Contract { |
| 352 | + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
| 353 | + // We will display legacy contracts as JSON and others as hex. |
| 354 | + if self.as_bytes()[0] == CONTRACT_VERSION_JSON { |
| 355 | + write!(f, "{}", str::from_utf8(self.as_bytes()).expect("invariant")) |
| 356 | + } else { |
| 357 | + for b in self.as_bytes() { |
| 358 | + write!(f, "{:02x}", b)?; |
| 359 | + } |
| 360 | + Ok(()) |
| 361 | + } |
| 362 | + } |
| 363 | +} |
| 364 | + |
| 365 | +impl fmt::Debug for Contract { |
| 366 | + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
| 367 | + write!(f, "Contract({:?})", Content::from_bytes(self.as_bytes()).expect("invariant")) |
| 368 | + } |
| 369 | +} |
0 commit comments