Skip to content

Commit 4adaa8f

Browse files
committed
Add contracts module
1 parent 99a9603 commit 4adaa8f

File tree

3 files changed

+382
-5
lines changed

3 files changed

+382
-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: 369 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,369 @@
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+
/// The contract version byte for legacy JSON contracts.
20+
const CONTRACT_VERSION_JSON: u8 = '{' as u8;
21+
22+
/// The contract version byte for CBOR contracts.
23+
const CONTRACT_VERSION_CBOR: u8 = 1;
24+
25+
/// An asset contract error.
26+
#[derive(Debug)]
27+
pub enum Error {
28+
/// The contract was empty.
29+
Empty,
30+
/// The CBOR format was invalid.
31+
InvalidCbor(serde_cbor::Error),
32+
/// the JSON format was invalid.
33+
InvalidJson(serde_json::Error),
34+
/// The contract's content are invalid.
35+
InvalidContract(&'static str),
36+
/// An unknown contract version was encountered.
37+
UnknownVersion(u8),
38+
}
39+
40+
impl fmt::Display for Error {
41+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
42+
match *self {
43+
Error::Empty => write!(f, "the contract was empty"),
44+
Error::InvalidCbor(ref e) => write!(f, "invalid CBOR format: {}", e),
45+
Error::InvalidJson(ref e) => write!(f, "invalid JSON format: {}", e),
46+
Error::InvalidContract(ref e) => write!(f, "invalid contract: {}", e),
47+
Error::UnknownVersion(v) => write!(f, "unknown contract version: {}", v),
48+
}
49+
}
50+
}
51+
52+
#[allow(deprecated)]
53+
impl error::Error for Error {
54+
fn cause(&self) -> Option<&error::Error> {
55+
match *self {
56+
Error::InvalidCbor(ref e) => Some(e),
57+
Error::InvalidJson(ref e) => Some(e),
58+
_ => None,
59+
}
60+
}
61+
62+
fn description(&self) -> &str {
63+
"description() is deprecated; use Display"
64+
}
65+
}
66+
67+
/// The issuing entity of an asset.
68+
#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
69+
pub struct ContractDetailsEntity {
70+
/// The domain name of the issuer.
71+
pub domain: Option<String>,
72+
}
73+
74+
/// Some well-known details encapsulated inside an asset contract.
75+
#[derive(Debug, Clone, Serialize, Deserialize)]
76+
pub struct ContractDetails {
77+
/// The precision of the asset values.
78+
pub precision: u8,
79+
/// The ticker of the asset.
80+
pub ticker: String,
81+
82+
/// The name of the asset.
83+
#[serde(skip_serializing_if = "Option::is_none")]
84+
pub name: Option<String>,
85+
/// The issuing entity.
86+
#[serde(skip_serializing_if = "Option::is_none")]
87+
pub entity: Option<ContractDetailsEntity>,
88+
/// The public key of the issuer.
89+
#[serde(skip_serializing_if = "Option::is_none")]
90+
pub issuer_pubkey: Option<bitcoin::PublicKey>,
91+
}
92+
93+
/// The structure of a legacy (JSON) contract.
94+
#[derive(Debug, Clone, Deserialize)]
95+
struct LegacyContract {
96+
precision: u8,
97+
ticker: String,
98+
#[serde(flatten)]
99+
other: BTreeMap<String, serde_json::Value>,
100+
}
101+
102+
/// The contents of an asset contract.
103+
#[derive(Debug, Clone)]
104+
enum Content {
105+
Legacy(LegacyContract),
106+
Modern {
107+
precision: u8,
108+
ticker: String,
109+
//TODO(stevenroose) consider requiring String keys
110+
other: BTreeMap<String, serde_cbor::Value>,
111+
},
112+
}
113+
114+
/// Check a precision value.
115+
#[inline]
116+
fn check_precision<P: PartialOrd + From<u8>>(p: P) -> Result<(), Error> {
117+
if p < 0.into() || p > MAX_PRECISION.into() {
118+
return Err(Error::InvalidContract("invalid precision"));
119+
}
120+
Ok(())
121+
}
122+
123+
/// Check a ticker value.
124+
#[inline]
125+
fn check_ticker(t: &str) -> Result<(), Error> {
126+
if t.len() > MAX_TICKER_LENGTH {
127+
return Err(Error::InvalidContract("ticker too long"));
128+
}
129+
Ok(())
130+
}
131+
132+
/// Check a key value.
133+
#[inline]
134+
fn check_key(k: &str) -> Result<(), Error> {
135+
if !k.is_ascii() {
136+
return Err(Error::InvalidContract("keys must be ASCII"));
137+
}
138+
Ok(())
139+
}
140+
141+
impl Content {
142+
fn from_bytes(contract: &[u8]) -> Result<Content, Error> {
143+
if contract.len() < 1 {
144+
return Err(Error::Empty);
145+
}
146+
147+
if contract[0] == CONTRACT_VERSION_JSON {
148+
let content: LegacyContract =
149+
serde_json::from_slice(contract).map_err(Error::InvalidJson)?;
150+
check_precision(content.precision)?;
151+
check_ticker(&content.ticker)?;
152+
for key in content.other.keys() {
153+
check_key(key)?;
154+
}
155+
Ok(Content::Legacy(content))
156+
} else if contract[0] == CONTRACT_VERSION_CBOR {
157+
let content: Vec<serde_cbor::Value> =
158+
serde_cbor::from_slice(&contract[1..]).map_err(Error::InvalidCbor)?;
159+
if content.len() != 3 {
160+
return Err(Error::InvalidContract("CBOR value must be array of 3 elements"));
161+
}
162+
let mut iter = content.into_iter();
163+
Ok(Content::Modern {
164+
precision: if let serde_cbor::Value::Integer(i) = iter.next().unwrap() {
165+
check_precision(i)?;
166+
i as u8
167+
} else {
168+
return Err(Error::InvalidContract("first CBOR value must be integer"));
169+
},
170+
ticker: if let serde_cbor::Value::Text(t) = iter.next().unwrap() {
171+
check_ticker(&t)?;
172+
t
173+
} else {
174+
return Err(Error::InvalidContract("second CBOR value must be string"));
175+
},
176+
other: if let serde_cbor::Value::Map(m) = iter.next().unwrap() {
177+
let mut other = BTreeMap::new();
178+
for (key, value) in m.into_iter() {
179+
// Use utility methods here after this PR is released:
180+
// https://github.com/pyfisch/cbor/pull/191
181+
match key {
182+
serde_cbor::Value::Text(t) => {
183+
check_key(&t)?;
184+
other.insert(t, value)
185+
},
186+
_ => return Err(Error::InvalidContract("keys must be strings")),
187+
};
188+
}
189+
other
190+
} else {
191+
return Err(Error::InvalidContract("third CBOR value must be map"));
192+
},
193+
})
194+
} else {
195+
Err(Error::UnknownVersion(contract[0]))
196+
}
197+
}
198+
}
199+
200+
/// An asset contract.
201+
#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
202+
pub struct Contract(Vec<u8>);
203+
204+
impl Contract {
205+
/// Generate a contract from the contract details.
206+
pub fn from_details(
207+
mut details: ContractDetails,
208+
extra_fields: BTreeMap<String, serde_cbor::Value>,
209+
) -> Result<Contract, Error> {
210+
check_precision(details.precision)?;
211+
check_ticker(&details.ticker)?;
212+
213+
// Add known fields from details.
214+
let mut props = BTreeMap::new();
215+
if let Some(name) = details.name.take() {
216+
props.insert("name".to_owned().into(), name.into());
217+
}
218+
if let Some(mut entity) = details.entity.take() {
219+
let mut ent = BTreeMap::new();
220+
if let Some(domain) = entity.domain.take() {
221+
ent.insert("domain".to_owned().into(), domain.into());
222+
}
223+
props.insert("entity".to_owned().into(), ent.into());
224+
}
225+
if let Some(issuer_pubkey) = details.issuer_pubkey.take() {
226+
props.insert("issuer_pubkey".to_owned().into(), issuer_pubkey.to_bytes().into());
227+
}
228+
229+
// Add extra fields.
230+
for (key, value) in extra_fields.into_iter() {
231+
check_key(&key)?;
232+
if props.insert(key.into(), value).is_some() {
233+
return Err(Error::InvalidContract("extra field reused key from details"));
234+
}
235+
}
236+
237+
let cbor: Vec<serde_cbor::Value> = vec![
238+
details.precision.into(),
239+
details.ticker.into(),
240+
props.into(),
241+
];
242+
243+
let mut buffer = vec![CONTRACT_VERSION_CBOR];
244+
serde_cbor::to_writer(&mut buffer, &cbor).map_err(Error::InvalidCbor)?;
245+
Ok(Contract(buffer))
246+
}
247+
248+
/// Generate a legacy contract from the contract details.
249+
#[deprecated]
250+
pub fn legacy_from_details(
251+
mut details: ContractDetails,
252+
extra_fields: BTreeMap<String, serde_json::Value>,
253+
) -> Result<Contract, Error> {
254+
check_precision(details.precision)?;
255+
check_ticker(&details.ticker)?;
256+
257+
// We will use the extra_fields hashmap to serialize the JSON later.
258+
for key in extra_fields.keys() {
259+
check_key(key)?;
260+
}
261+
let mut props = extra_fields;
262+
263+
// Add known fields from details.
264+
if props.insert("precision".into(), details.precision.into()).is_some() {
265+
return Err(Error::InvalidContract("extra field reused key from details"));
266+
}
267+
if props.insert("ticker".into(), details.ticker.into()).is_some() {
268+
return Err(Error::InvalidContract("extra field reused key from details"));
269+
}
270+
if let Some(name) = details.name.take() {
271+
if props.insert("name".into(), name.into()).is_some() {
272+
return Err(Error::InvalidContract("extra field reused key from details"));
273+
}
274+
}
275+
if let Some(entity) = details.entity.take() {
276+
if props.insert("entity".into(), serde_json::to_value(&entity).unwrap()).is_some() {
277+
return Err(Error::InvalidContract("extra field reused key from details"));
278+
}
279+
}
280+
if let Some(issuer_pubkey) = details.issuer_pubkey.take() {
281+
if props.insert("issuer_pubkey".into(), issuer_pubkey.to_string().into()).is_some() {
282+
return Err(Error::InvalidContract("extra field reused key from details"));
283+
}
284+
}
285+
286+
Ok(Contract(serde_json::to_vec(&props).map_err(Error::InvalidJson)?))
287+
}
288+
289+
/// Parse an asset contract from bytes.
290+
pub fn from_bytes(contract: &[u8]) -> Result<Contract, Error> {
291+
// Check for validity and then store raw contract.
292+
let _ = Content::from_bytes(contract)?;
293+
Ok(Contract(contract.to_vec()))
294+
}
295+
296+
/// Get the binary representation of the asset contract.
297+
pub fn as_bytes(&self) -> &[u8] {
298+
&self.0
299+
}
300+
301+
/// Get the contract hash of this asset contract.
302+
pub fn contract_hash(&self) -> ContractHash {
303+
ContractHash::hash(self.as_bytes())
304+
}
305+
306+
/// Calculate the asset ID of an asset issued with this contract.
307+
pub fn asset_id(&self, prevout: OutPoint) -> AssetId {
308+
AssetId::from_entropy(AssetId::generate_asset_entropy(prevout, self.contract_hash()))
309+
}
310+
311+
/// Get the precision of the asset.
312+
pub fn precision(&self) -> u8 {
313+
match Content::from_bytes(&self.as_bytes()).expect("invariant") {
314+
Content::Legacy(c) => c.precision,
315+
Content::Modern { precision, .. } => precision,
316+
}
317+
}
318+
319+
/// Get the ticker of the asset.
320+
pub fn ticker(&self) -> String {
321+
match Content::from_bytes(&self.as_bytes()).expect("invariant") {
322+
Content::Legacy(c) => c.ticker,
323+
Content::Modern { ticker, .. } => ticker,
324+
}
325+
}
326+
327+
/// Retrieve a property from the contract.
328+
/// For precision and ticker, use the designated methods instead.
329+
pub fn property<T: serde::de::DeserializeOwned>(&self, key: &str) -> Result<Option<T>, Error> {
330+
match Content::from_bytes(&self.as_bytes()).expect("invariant") {
331+
Content::Legacy(c) => {
332+
let value = match c.other.get(key) {
333+
Some(v) => v,
334+
None => return Ok(None),
335+
};
336+
Ok(serde_json::from_value(value.clone()).map_err(Error::InvalidJson)?)
337+
},
338+
Content::Modern { other, .. } => {
339+
let value = match other.get(key) {
340+
Some(v) => v,
341+
None => return Ok(None),
342+
};
343+
//TODO(stevenroose) optimize this when serde_cbor implements from_value
344+
let bytes = serde_cbor::to_vec(&value).map_err(Error::InvalidCbor)?;
345+
Ok(serde_cbor::from_slice(&bytes).map_err(Error::InvalidCbor)?)
346+
},
347+
}
348+
}
349+
}
350+
351+
impl fmt::Display for Contract {
352+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
353+
// We will display legacy contracts as JSON and others as hex.
354+
if self.as_bytes()[0] == CONTRACT_VERSION_JSON {
355+
write!(f, "{}", str::from_utf8(self.as_bytes()).expect("invariant"))
356+
} else {
357+
for b in self.as_bytes() {
358+
write!(f, "{:02x}", b)?;
359+
}
360+
Ok(())
361+
}
362+
}
363+
}
364+
365+
impl fmt::Debug for Contract {
366+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
367+
write!(f, "Contract({:?})", Content::from_bytes(self.as_bytes()).expect("invariant"))
368+
}
369+
}

0 commit comments

Comments
 (0)