Skip to content

Commit 0b2fb26

Browse files
committed
Add contracts module
1 parent 5232363 commit 0b2fb26

File tree

3 files changed

+205
-5
lines changed

3 files changed

+205
-5
lines changed

Cargo.toml

+8-3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ repository = "https://github.com/ElementsProject/rust-elements/"
99
documentation = "https://docs.rs/elements/"
1010

1111
[features]
12+
default = [ "contracts" ]
13+
14+
contracts = [ "serde", "serde_cbor", "serde_json" ]
1215
"serde-feature" = [
1316
"bitcoin/use-serde",
1417
"serde"
@@ -22,9 +25,11 @@ bitcoin = "0.23"
2225
# to avoid requiring two version of bitcoin_hashes.
2326
bitcoin_hashes = "0.7.6"
2427

25-
[dependencies.serde]
26-
version = "1.0"
27-
optional = true
28+
serde = { version = "1.0", optional = true, features = ["derive"] }
29+
30+
# Used for contracts module.
31+
serde_cbor = { version = "0.11.1", optional = true }
32+
serde_json = { version = "<=1.0.44", optional = true }
2833

2934
[dev-dependencies]
3035
rand = "0.6.5"

src/contracts.rs

+192
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
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+
}

src/lib.rs

+5-2
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,19 @@
2828
pub extern crate bitcoin;
2929
#[macro_use]
3030
pub extern crate bitcoin_hashes;
31-
#[cfg(feature = "serde")] extern crate serde;
31+
#[cfg(feature = "serde")] #[macro_use] extern crate serde;
32+
#[cfg(feature = "serde_cbor")] extern crate serde_cbor;
33+
#[cfg(feature = "serde_json")] extern crate serde_json;
3234

3335
#[cfg(test)] extern crate rand;
34-
#[cfg(test)] extern crate serde_json;
3536

3637
#[macro_use] mod internal_macros;
3738
pub mod address;
3839
pub mod blech32;
3940
mod block;
4041
pub mod confidential;
42+
#[cfg(feature = "contracts")]
43+
pub mod contracts;
4144
pub mod dynafed;
4245
pub mod encode;
4346
mod fast_merkle_root;

0 commit comments

Comments
 (0)