tezos_smart_rollup_encoding/michelson/
ticket.rs1use crate::{
10 contract::Contract,
11 michelson::{
12 Michelson, MichelsonBytes, MichelsonContract, MichelsonInt, MichelsonOption,
13 MichelsonPair, MichelsonString, MichelsonUnit,
14 },
15};
16use core::{
17 cmp::Ordering,
18 fmt::{Display, Formatter, Result as FmtResult},
19};
20use crypto::blake2b::{digest_256, Blake2bError};
21use hex::FromHexError;
22use nom::combinator::map;
23use num_bigint::BigInt;
24use num_traits::Signed;
25use std::fmt::Debug;
26use tezos_data_encoding::{
27 enc::{BinError, BinResult, BinWriter},
28 encoding::HasEncoding,
29 nom::{NomReader, NomResult},
30 types::{SizedBytes, Zarith},
31};
32use thiserror::Error;
33
34#[cfg(feature = "testing")]
35pub mod testing;
36
37pub const TICKET_HASH_SIZE: usize = 32;
39
40#[derive(Clone, PartialEq, Eq, NomReader, BinWriter, HasEncoding)]
42pub struct TicketHash {
43 inner: SizedBytes<TICKET_HASH_SIZE>,
44}
45
46impl PartialOrd for TicketHash {
47 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
48 self.inner.as_ref().partial_cmp(other.inner.as_ref())
49 }
50}
51
52impl Ord for TicketHash {
53 fn cmp(&self, other: &Self) -> Ordering {
54 self.inner.as_ref().cmp(other.inner.as_ref())
55 }
56}
57
58impl Debug for TicketHash {
59 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60 write!(f, "TicketId(")?;
61 for &byte in self.inner.as_ref() {
62 write!(f, "{:02x?}", byte)?;
63 }
64 write!(f, ")")
65 }
66}
67
68impl Display for TicketHash {
69 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
70 write!(f, "{}", hex::encode(&self.inner))
71 }
72}
73
74#[allow(clippy::from_over_into)]
75impl Into<String> for TicketHash {
76 fn into(self) -> String {
77 hex::encode(self.inner)
78 }
79}
80
81impl TryFrom<String> for TicketHash {
82 type Error = FromHexError;
83
84 fn try_from(value: String) -> Result<Self, Self::Error> {
85 let mut result = Self {
86 inner: SizedBytes([0; TICKET_HASH_SIZE]),
87 };
88 hex::decode_to_slice(value, result.inner.as_mut())?;
89 Ok(result)
90 }
91}
92
93#[derive(Error, Debug)]
95pub enum TicketHashError {
96 #[error("Unable to serialize ticket for hashing: {0}")]
98 Serialization(#[from] BinError),
99 #[error("Unable to hash ticket bytes: {0}")]
101 Hashing(#[from] Blake2bError),
102}
103
104#[derive(Error, Debug, Clone)]
106pub enum TicketError {
107 #[error("ticket amount out of range")]
109 InvalidAmount(BigInt),
110}
111
112type TicketRepr<Expr> =
115 MichelsonPair<MichelsonContract, MichelsonPair<Expr, MichelsonInt>>;
116
117#[derive(Debug, PartialEq, Eq)]
119pub struct Ticket<Expr: Michelson>(pub TicketRepr<Expr>);
120
121impl<Expr: Michelson> Michelson for Ticket<Expr> {}
122
123impl<Expr: Michelson> NomReader for Ticket<Expr> {
124 fn nom_read(bytes: &[u8]) -> NomResult<Self> {
125 map(<TicketRepr<Expr>>::nom_read, Ticket)(bytes)
126 }
127}
128
129impl<Expr: Michelson> BinWriter for Ticket<Expr> {
130 fn bin_write(&self, output: &mut Vec<u8>) -> BinResult {
131 self.0.bin_write(output)
132 }
133}
134
135impl<Expr: Michelson> HasEncoding for Ticket<Expr> {
136 fn encoding() -> tezos_data_encoding::encoding::Encoding {
137 <TicketRepr<Expr>>::encoding()
138 }
139}
140
141impl<Expr: Michelson> Ticket<Expr> {
142 pub fn new<Val: Into<Expr>, Amount: Into<BigInt>>(
144 creator: Contract,
145 contents: Val,
146 amount: Amount,
147 ) -> Result<Self, TicketError> {
148 let amount: BigInt = amount.into();
149 if amount.is_positive() {
150 Ok(Ticket(MichelsonPair(
151 MichelsonContract(creator),
152 MichelsonPair(contents.into(), MichelsonInt(Zarith(amount))),
153 )))
154 } else {
155 Err(TicketError::InvalidAmount(amount))
156 }
157 }
158
159 pub fn hash(&self) -> Result<TicketHash, TicketHashError> {
165 let mut bytes = Vec::new();
166 self.creator().bin_write(&mut bytes)?;
167 self.contents().bin_write(&mut bytes)?;
168
169 let digest = digest_256(bytes.as_slice())?;
170 let digest: [u8; TICKET_HASH_SIZE] = digest.try_into().unwrap();
171
172 Ok(TicketHash {
173 inner: SizedBytes(digest),
174 })
175 }
176
177 pub fn creator(&self) -> &MichelsonContract {
179 &self.0 .0
180 }
181 pub fn contents(&self) -> &Expr {
183 &self.0 .1 .0
184 }
185 pub fn amount(&self) -> &BigInt {
187 &self.0 .1 .1 .0 .0
188 }
189
190 pub fn amount_as<T: TryFrom<BigInt, Error = E>, E>(&self) -> Result<T, E> {
192 self.amount().to_owned().try_into()
193 }
194}
195
196pub type IntTicket = Ticket<MichelsonInt>;
198
199pub type StringTicket = Ticket<MichelsonString>;
201
202impl Ticket<MichelsonString> {
203 #[cfg(feature = "testing")]
205 pub fn testing_clone(&self) -> Self {
206 Ticket(MichelsonPair(
207 MichelsonContract(self.creator().0.clone()),
208 MichelsonPair(
209 MichelsonString(self.contents().0.clone()),
210 MichelsonInt(Zarith(self.amount().clone())),
211 ),
212 ))
213 }
214}
215
216pub type BytesTicket = Ticket<MichelsonBytes>;
218
219pub type UnitTicket = Ticket<MichelsonUnit>;
221
222pub type FA2_1Ticket =
224 Ticket<MichelsonPair<MichelsonInt, MichelsonOption<MichelsonBytes>>>;
225
226#[cfg(test)]
227mod test {
228 use super::*;
229 use tezos_data_encoding::enc::BinWriter;
230 use tezos_data_encoding::nom::NomReader;
231
232 #[test]
233 fn content_bytes() {
234 let ticket = BytesTicket::new(
235 Contract::from_b58check("KT1NgXQ6Mwu3XKFDcKdYFS6dkkY3iNKdBKEc").unwrap(),
236 MichelsonBytes(vec![1, 2, 3, 4, 5]),
237 500,
238 )
239 .unwrap();
240
241 assert_encode_decode(ticket);
242 }
243
244 #[test]
245 fn content_string() {
246 let ticket = StringTicket::new(
247 Contract::from_b58check("KT1NgXQ6Mwu3XKFDcKdYFS6dkkY3iNKdBKEc").unwrap(),
248 MichelsonString("Hello, Ticket".to_string()),
249 900,
250 )
251 .unwrap();
252
253 assert_encode_decode(ticket);
254 }
255
256 #[test]
257 fn content_unit() {
258 let ticket = UnitTicket::new(
259 Contract::from_b58check("KT1NgXQ6Mwu3XKFDcKdYFS6dkkY3iNKdBKEc").unwrap(),
260 MichelsonUnit,
261 900,
262 )
263 .unwrap();
264
265 assert_encode_decode(ticket);
266 }
267
268 #[test]
269 fn content_int() {
270 let ticket = IntTicket::new::<i32, i32>(
271 Contract::from_b58check("KT1NgXQ6Mwu3XKFDcKdYFS6dkkY3iNKdBKEc").unwrap(),
272 -25,
273 900,
274 )
275 .unwrap();
276
277 assert_encode_decode(ticket);
278 }
279
280 #[test]
281 fn content_pair() {
282 type NestedPair = MichelsonPair<
283 MichelsonUnit,
284 MichelsonPair<MichelsonPair<MichelsonString, MichelsonBytes>, MichelsonInt>,
285 >;
286 let ticket: Ticket<NestedPair> = Ticket::new::<_, i32>(
287 Contract::from_b58check("KT1NgXQ6Mwu3XKFDcKdYFS6dkkY3iNKdBKEc").unwrap(),
288 MichelsonPair(
289 MichelsonUnit,
290 MichelsonPair(
291 MichelsonPair(
292 MichelsonString("hello".to_string()),
293 MichelsonBytes(b"a series of bytes".to_vec()),
294 ),
295 MichelsonInt::from(19),
296 ),
297 ),
298 17,
299 )
300 .unwrap();
301
302 assert_encode_decode(ticket);
303 }
304
305 fn assert_encode_decode<T: Michelson>(ticket: Ticket<T>) {
306 let mut bin = Vec::new();
307 ticket.bin_write(&mut bin).unwrap();
308
309 let (remaining, parsed) = Ticket::nom_read(&bin).unwrap();
310
311 assert_eq!(ticket, parsed);
312 assert!(remaining.is_empty());
313 }
314}