-
Notifications
You must be signed in to change notification settings - Fork 33
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
5232363
commit 0b2fb26
Showing
3 changed files
with
205 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
//! Handling asset contracts. | ||
use std::collections::BTreeMap; | ||
use std::{error, fmt}; | ||
|
||
use serde_cbor; | ||
use serde_json; | ||
use bitcoin::hashes::Hash; | ||
|
||
use ::ContractHash; | ||
|
||
/// The maximum precision of an asset. | ||
pub const MAX_PRECISION: u8 = 8; | ||
|
||
/// The maximum ticker string length. | ||
pub const MAX_TICKER_LENGTH: usize = 5; | ||
|
||
/// 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), | ||
} | ||
} | ||
} | ||
|
||
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 { | ||
"a contract error" | ||
} | ||
} | ||
|
||
/// The structure of a legacy (JSON) contract. | ||
#[derive(Debug, Clone, Deserialize)] | ||
struct LegacyContract { | ||
precision: u8, | ||
ticker: String, | ||
#[serde(flatten)] | ||
other: BTreeMap<String, serde_json::Value>, | ||
} | ||
|
||
/// 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<serde_cbor::Value, serde_cbor::Value>, | ||
}, | ||
} | ||
|
||
impl Content { | ||
fn from_bytes(contract: &[u8]) -> Result<Content, Error> { | ||
if contract.len() < 1 { | ||
return Err(Error::Empty); | ||
} | ||
|
||
if contract[0] == '{' as u8 { | ||
let content: LegacyContract = | ||
serde_json::from_slice(contract).map_err(Error::InvalidJson)?; | ||
if content.precision > MAX_PRECISION { | ||
return Err(Error::InvalidContract("invalid precision")); | ||
} | ||
if content.ticker.len() > MAX_TICKER_LENGTH { | ||
return Err(Error::InvalidContract("ticker too long")); | ||
} | ||
Ok(Content::Legacy(content)) | ||
} else if contract[0] == 1 { | ||
let content: Vec<serde_cbor::Value> = | ||
serde_cbor::from_slice(contract).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() { | ||
if i < 0 || i > MAX_PRECISION as i128 { | ||
return Err(Error::InvalidContract("invalid precision")); | ||
} | ||
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() { | ||
if t.len() > MAX_TICKER_LENGTH { | ||
return Err(Error::InvalidContract("ticker too long")); | ||
} | ||
t | ||
} else { | ||
return Err(Error::InvalidContract("second CBOR value must be string")); | ||
}, | ||
other: if let serde_cbor::Value::Map(m) = iter.next().unwrap() { | ||
m | ||
} 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<u8>); | ||
|
||
impl Contract { | ||
/// Parse an asset contract from bytes. | ||
pub fn from_bytes(contract: &[u8]) -> Result<Contract, Error> { | ||
// 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()) | ||
} | ||
|
||
/// 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<T: serde::de::DeserializeOwned>(&self, key: &str) -> Result<Option<T>, 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.to_owned().into()) { | ||
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)?) | ||
}, | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters