tezos_smart_rollup_encoding/michelson/
ticket.rs

1// SPDX-FileCopyrightText: 2022-2023 TriliTech <contact@trili.tech>
2// SPDX-FileCopyrightText: 2023 Nomadic Labs <contact@nomadic-labs.com>
3// SPDX-FileCopyrightText: 2023 Marigold <contact@marigold.dev>
4//
5// SPDX-License-Identifier: MIT
6
7//! Michelson-ticket encoding.
8
9use 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
37/// The length of a Tezos ticket ID
38pub const TICKET_HASH_SIZE: usize = 32;
39
40/// The hash of a string ticket - identifying a ticket by creator and contents.
41#[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/// Errors occurring when identifying tickets.
94#[derive(Error, Debug)]
95pub enum TicketHashError {
96    /// Unable to serialize ticket creator and contents.
97    #[error("Unable to serialize ticket for hashing: {0}")]
98    Serialization(#[from] BinError),
99    /// Unable to hash serialized ticket.
100    #[error("Unable to hash ticket bytes: {0}")]
101    Hashing(#[from] Blake2bError),
102}
103
104/// Errors occurring when identifying tickets.
105#[derive(Error, Debug, Clone)]
106pub enum TicketError {
107    /// Invalid amount in ticket repr.
108    #[error("ticket amount out of range")]
109    InvalidAmount(BigInt),
110}
111
112// Expr is guarantee by construction to implement `Michelson` even though
113// rust does not enforce it in type aliases `type TicketRepr<Expr: Michelson>`.
114type TicketRepr<Expr> =
115    MichelsonPair<MichelsonContract, MichelsonPair<Expr, MichelsonInt>>;
116
117/// Michelson ticket representative.
118#[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    /// creates a new ticket with `creator`, `contents` and `amount`.
143    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    /// Return an identifying hash of the ticket creator and contents.
160    ///
161    /// Calculated as the `blake2b` hash of a tezos-encoded `obj2`:
162    /// - creator contract
163    /// - string contents
164    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    /// The L1 ticketer's address.
178    pub fn creator(&self) -> &MichelsonContract {
179        &self.0 .0
180    }
181    /// The ticket's content
182    pub fn contents(&self) -> &Expr {
183        &self.0 .1 .0
184    }
185    /// The ticket's amount
186    pub fn amount(&self) -> &BigInt {
187        &self.0 .1 .1 .0 .0
188    }
189
190    /// same as `amount()` but returns it as a `T`
191    pub fn amount_as<T: TryFrom<BigInt, Error = E>, E>(&self) -> Result<T, E> {
192        self.amount().to_owned().try_into()
193    }
194}
195
196/// Specialized version of ticket where the content must be an int
197pub type IntTicket = Ticket<MichelsonInt>;
198
199/// Specialized version of ticket where the content must be a string
200pub type StringTicket = Ticket<MichelsonString>;
201
202impl Ticket<MichelsonString> {
203    /// clone used in testing
204    #[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
216/// Specialized version of ticket where the content must be byte
217pub type BytesTicket = Ticket<MichelsonBytes>;
218
219/// Specialized version of ticket where the content must be unit
220pub type UnitTicket = Ticket<MichelsonUnit>;
221
222/// Specialized version of ticket for FA2.1 tokens
223pub 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}