Skip to content

Commit c68c24b

Browse files
committed
Add contracts module
1 parent 5232363 commit c68c24b

File tree

3 files changed

+231
-5
lines changed

3 files changed

+231
-5
lines changed

Cargo.toml

Lines changed: 8 additions & 3 deletions
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

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

src/lib.rs

Lines changed: 5 additions & 2 deletions
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)