|
| 1 | +//! Handling asset contracts. |
| 2 | +
|
| 3 | +use std::collections::BTreeMap; |
| 4 | +use std::{error, fmt}; |
| 5 | + |
| 6 | +use serde_cbor; |
| 7 | +use serde_json; |
| 8 | +use bitcoin::hashes::Hash; |
| 9 | + |
| 10 | +use ::ContractHash; |
| 11 | + |
| 12 | +/// The maximum precision of an asset. |
| 13 | +pub const MAX_PRECISION: u8 = 8; |
| 14 | + |
| 15 | +/// The maximum ticker string length. |
| 16 | +pub const MAX_TICKER_LENGTH: usize = 5; |
| 17 | + |
| 18 | +/// An asset contract error. |
| 19 | +#[derive(Debug)] |
| 20 | +pub enum Error { |
| 21 | + /// The contract was empty. |
| 22 | + Empty, |
| 23 | + /// The CBOR format was invalid. |
| 24 | + InvalidCbor(serde_cbor::Error), |
| 25 | + /// the JSON format was invalid. |
| 26 | + InvalidJson(serde_json::Error), |
| 27 | + /// The contract's content are invalid. |
| 28 | + InvalidContract(&'static str), |
| 29 | + /// An unknown contract version was encountered. |
| 30 | + UnknownVersion(u8), |
| 31 | +} |
| 32 | + |
| 33 | +impl fmt::Display for Error { |
| 34 | + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
| 35 | + match *self { |
| 36 | + Error::Empty => write!(f, "the contract was empty"), |
| 37 | + Error::InvalidCbor(ref e) => write!(f, "invalid CBOR format: {}", e), |
| 38 | + Error::InvalidJson(ref e) => write!(f, "invalid JSON format: {}", e), |
| 39 | + Error::InvalidContract(ref e) => write!(f, "invalid contract: {}", e), |
| 40 | + Error::UnknownVersion(v) => write!(f, "unknown contract version: {}", v), |
| 41 | + } |
| 42 | + } |
| 43 | +} |
| 44 | + |
| 45 | +impl error::Error for Error { |
| 46 | + fn cause(&self) -> Option<&error::Error> { |
| 47 | + match *self { |
| 48 | + Error::InvalidCbor(ref e) => Some(e), |
| 49 | + Error::InvalidJson(ref e) => Some(e), |
| 50 | + _ => None, |
| 51 | + } |
| 52 | + } |
| 53 | + |
| 54 | + fn description(&self) -> &str { |
| 55 | + "a contract error" |
| 56 | + } |
| 57 | +} |
| 58 | + |
| 59 | +/// The structure of a legacy (JSON) contract. |
| 60 | +#[derive(Debug, Clone, Deserialize)] |
| 61 | +struct LegacyContract { |
| 62 | + precision: u8, |
| 63 | + ticker: String, |
| 64 | + #[serde(flatten)] |
| 65 | + other: BTreeMap<String, serde_json::Value>, |
| 66 | +} |
| 67 | + |
| 68 | +/// The contents of an asset contract. |
| 69 | +#[derive(Debug, Clone)] |
| 70 | +enum Content { |
| 71 | + Legacy(LegacyContract), |
| 72 | + Modern { |
| 73 | + precision: u8, |
| 74 | + ticker: String, |
| 75 | + //TODO(stevenroose) consider requiring String keys |
| 76 | + other: BTreeMap<serde_cbor::Value, serde_cbor::Value>, |
| 77 | + }, |
| 78 | +} |
| 79 | + |
| 80 | +impl Content { |
| 81 | + fn from_bytes(contract: &[u8]) -> Result<Content, Error> { |
| 82 | + if contract.len() < 1 { |
| 83 | + return Err(Error::Empty); |
| 84 | + } |
| 85 | + |
| 86 | + if contract[0] == '{' as u8 { |
| 87 | + let content: LegacyContract = |
| 88 | + serde_json::from_slice(contract).map_err(Error::InvalidJson)?; |
| 89 | + if content.precision > MAX_PRECISION { |
| 90 | + return Err(Error::InvalidContract("invalid precision")); |
| 91 | + } |
| 92 | + if content.ticker.len() > MAX_TICKER_LENGTH { |
| 93 | + return Err(Error::InvalidContract("ticker too long")); |
| 94 | + } |
| 95 | + Ok(Content::Legacy(content)) |
| 96 | + } else if contract[0] == 1 { |
| 97 | + let content: Vec<serde_cbor::Value> = |
| 98 | + serde_cbor::from_slice(contract).map_err(Error::InvalidCbor)?; |
| 99 | + if content.len() != 3 { |
| 100 | + return Err(Error::InvalidContract("CBOR value must be array of 3 elements")); |
| 101 | + } |
| 102 | + let mut iter = content.into_iter(); |
| 103 | + Ok(Content::Modern { |
| 104 | + precision: if let serde_cbor::Value::Integer(i) = iter.next().unwrap() { |
| 105 | + if i < 0 || i > MAX_PRECISION as i128 { |
| 106 | + return Err(Error::InvalidContract("invalid precision")); |
| 107 | + } |
| 108 | + i as u8 |
| 109 | + } else { |
| 110 | + return Err(Error::InvalidContract("first CBOR value must be integer")); |
| 111 | + }, |
| 112 | + ticker: if let serde_cbor::Value::Text(t) = iter.next().unwrap() { |
| 113 | + if t.len() > MAX_TICKER_LENGTH { |
| 114 | + return Err(Error::InvalidContract("ticker too long")); |
| 115 | + } |
| 116 | + t |
| 117 | + } else { |
| 118 | + return Err(Error::InvalidContract("second CBOR value must be string")); |
| 119 | + }, |
| 120 | + other: if let serde_cbor::Value::Map(m) = iter.next().unwrap() { |
| 121 | + m |
| 122 | + } else { |
| 123 | + return Err(Error::InvalidContract("third CBOR value must be map")); |
| 124 | + }, |
| 125 | + }) |
| 126 | + } else { |
| 127 | + Err(Error::UnknownVersion(contract[0])) |
| 128 | + } |
| 129 | + } |
| 130 | +} |
| 131 | + |
| 132 | +/// An asset contract. |
| 133 | +#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] |
| 134 | +pub struct Contract(Vec<u8>); |
| 135 | + |
| 136 | +impl Contract { |
| 137 | + /// Parse an asset contract from bytes. |
| 138 | + pub fn from_bytes(contract: &[u8]) -> Result<Contract, Error> { |
| 139 | + // Check for validity and then store raw contract. |
| 140 | + let _ = Content::from_bytes(contract)?; |
| 141 | + Ok(Contract(contract.to_vec())) |
| 142 | + } |
| 143 | + |
| 144 | + /// Get the binary representation of the asset contract. |
| 145 | + pub fn as_bytes(&self) -> &[u8] { |
| 146 | + &self.0 |
| 147 | + } |
| 148 | + |
| 149 | + /// Get the contract hash of this asset contract. |
| 150 | + pub fn contract_hash(&self) -> ContractHash { |
| 151 | + ContractHash::hash(self.as_bytes()) |
| 152 | + } |
| 153 | + |
| 154 | + /// Get the precision of the asset. |
| 155 | + pub fn precision(&self) -> u8 { |
| 156 | + match Content::from_bytes(&self.as_bytes()).expect("invariant") { |
| 157 | + Content::Legacy(c) => c.precision, |
| 158 | + Content::Modern { precision, .. } => precision, |
| 159 | + } |
| 160 | + } |
| 161 | + |
| 162 | + /// Get the ticker of the asset. |
| 163 | + pub fn ticker(&self) -> String { |
| 164 | + match Content::from_bytes(&self.as_bytes()).expect("invariant") { |
| 165 | + Content::Legacy(c) => c.ticker, |
| 166 | + Content::Modern { ticker, .. } => ticker, |
| 167 | + } |
| 168 | + } |
| 169 | + |
| 170 | + /// Retrieve a property from the contract. |
| 171 | + /// For precision and ticker, use the designated methods instead. |
| 172 | + pub fn property<T: serde::de::DeserializeOwned>(&self, key: &str) -> Result<Option<T>, Error> { |
| 173 | + match Content::from_bytes(&self.as_bytes()).expect("invariant") { |
| 174 | + Content::Legacy(c) => { |
| 175 | + let value = match c.other.get(key) { |
| 176 | + Some(v) => v, |
| 177 | + None => return Ok(None), |
| 178 | + }; |
| 179 | + Ok(serde_json::from_value(value.clone()).map_err(Error::InvalidJson)?) |
| 180 | + }, |
| 181 | + Content::Modern { other, .. } => { |
| 182 | + let value = match other.get(&key.to_owned().into()) { |
| 183 | + Some(v) => v, |
| 184 | + None => return Ok(None), |
| 185 | + }; |
| 186 | + //TODO(stevenroose) optimize this when serde_cbor implements from_value |
| 187 | + let bytes = serde_cbor::to_vec(&value).map_err(Error::InvalidCbor)?; |
| 188 | + Ok(serde_cbor::from_slice(&bytes).map_err(Error::InvalidCbor)?) |
| 189 | + }, |
| 190 | + } |
| 191 | + } |
| 192 | +} |
0 commit comments