Skip to content

Commit

Permalink
Add contracts module
Browse files Browse the repository at this point in the history
  • Loading branch information
stevenroose committed May 26, 2020
1 parent 5232363 commit 0b2fb26
Show file tree
Hide file tree
Showing 3 changed files with 205 additions and 5 deletions.
11 changes: 8 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ repository = "https://github.com/ElementsProject/rust-elements/"
documentation = "https://docs.rs/elements/"

[features]
default = [ "contracts" ]

contracts = [ "serde", "serde_cbor", "serde_json" ]
"serde-feature" = [
"bitcoin/use-serde",
"serde"
Expand All @@ -22,9 +25,11 @@ bitcoin = "0.23"
# to avoid requiring two version of bitcoin_hashes.
bitcoin_hashes = "0.7.6"

[dependencies.serde]
version = "1.0"
optional = true
serde = { version = "1.0", optional = true, features = ["derive"] }

# 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"
Expand Down
192 changes: 192 additions & 0 deletions src/contracts.rs
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)?)
},
}
}
}
7 changes: 5 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(test)] 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;
Expand Down

0 comments on commit 0b2fb26

Please sign in to comment.