use crate::{
contract::Contract,
michelson::{
Michelson, MichelsonBytes, MichelsonContract, MichelsonInt, MichelsonPair,
MichelsonString, MichelsonUnit,
},
};
use core::{
cmp::Ordering,
fmt::{Display, Formatter, Result as FmtResult},
};
use crypto::blake2b::{digest_256, Blake2bError};
use hex::FromHexError;
use nom::combinator::map;
use num_bigint::BigInt;
use num_traits::Signed;
use std::fmt::Debug;
use tezos_data_encoding::{
enc::{BinError, BinResult, BinWriter},
encoding::HasEncoding,
nom::{NomReader, NomResult},
types::{SizedBytes, Zarith},
};
use thiserror::Error;
#[cfg(feature = "testing")]
pub mod testing;
pub const TICKET_HASH_SIZE: usize = 32;
#[derive(Clone, PartialEq, Eq, NomReader, BinWriter, HasEncoding)]
pub struct TicketHash {
inner: SizedBytes<TICKET_HASH_SIZE>,
}
impl PartialOrd for TicketHash {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.inner.as_ref().partial_cmp(other.inner.as_ref())
}
}
impl Ord for TicketHash {
fn cmp(&self, other: &Self) -> Ordering {
self.inner.as_ref().cmp(other.inner.as_ref())
}
}
impl Debug for TicketHash {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "TicketId(")?;
for &byte in self.inner.as_ref() {
write!(f, "{:02x?}", byte)?;
}
write!(f, ")")
}
}
impl Display for TicketHash {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
write!(f, "{}", hex::encode(&self.inner))
}
}
#[allow(clippy::from_over_into)]
impl Into<String> for TicketHash {
fn into(self) -> String {
hex::encode(self.inner)
}
}
impl TryFrom<String> for TicketHash {
type Error = FromHexError;
fn try_from(value: String) -> Result<Self, Self::Error> {
let mut result = Self {
inner: SizedBytes([0; TICKET_HASH_SIZE]),
};
hex::decode_to_slice(value, result.inner.as_mut())?;
Ok(result)
}
}
#[derive(Error, Debug)]
pub enum TicketHashError {
#[error("Unable to serialize ticket for hashing: {0}")]
Serialization(#[from] BinError),
#[error("Unable to hash ticket bytes: {0}")]
Hashing(#[from] Blake2bError),
}
#[derive(Error, Debug, Clone)]
pub enum TicketError {
#[error("ticket amount out of range")]
InvalidAmount(BigInt),
}
type TicketRepr<Expr> =
MichelsonPair<MichelsonContract, MichelsonPair<Expr, MichelsonInt>>;
#[derive(Debug, PartialEq, Eq)]
pub struct Ticket<Expr: Michelson>(pub TicketRepr<Expr>);
impl<Expr: Michelson> Michelson for Ticket<Expr> {}
impl<Expr: Michelson> NomReader for Ticket<Expr> {
fn nom_read(bytes: &[u8]) -> NomResult<Self> {
map(<TicketRepr<Expr>>::nom_read, Ticket)(bytes)
}
}
impl<Expr: Michelson> BinWriter for Ticket<Expr> {
fn bin_write(&self, output: &mut Vec<u8>) -> BinResult {
self.0.bin_write(output)
}
}
impl<Expr: Michelson> HasEncoding for Ticket<Expr> {
fn encoding() -> tezos_data_encoding::encoding::Encoding {
<TicketRepr<Expr>>::encoding()
}
}
impl<Expr: Michelson> Ticket<Expr> {
pub fn new<Val: Into<Expr>, Amount: Into<BigInt>>(
creator: Contract,
contents: Val,
amount: Amount,
) -> Result<Self, TicketError> {
let amount: BigInt = amount.into();
if amount.is_positive() {
Ok(Ticket(MichelsonPair(
MichelsonContract(creator),
MichelsonPair(contents.into(), MichelsonInt(Zarith(amount))),
)))
} else {
Err(TicketError::InvalidAmount(amount))
}
}
pub fn hash(&self) -> Result<TicketHash, TicketHashError> {
let mut bytes = Vec::new();
self.creator().bin_write(&mut bytes)?;
self.contents().bin_write(&mut bytes)?;
let digest = digest_256(bytes.as_slice())?;
let digest: [u8; TICKET_HASH_SIZE] = digest.try_into().unwrap();
Ok(TicketHash {
inner: SizedBytes(digest),
})
}
pub fn creator(&self) -> &MichelsonContract {
&self.0 .0
}
pub fn contents(&self) -> &Expr {
&self.0 .1 .0
}
pub fn amount(&self) -> &BigInt {
&self.0 .1 .1 .0 .0
}
pub fn amount_as<T: TryFrom<BigInt, Error = E>, E>(&self) -> Result<T, E> {
self.amount().to_owned().try_into()
}
}
pub type IntTicket = Ticket<MichelsonInt>;
pub type StringTicket = Ticket<MichelsonString>;
impl Ticket<MichelsonString> {
#[cfg(feature = "testing")]
pub fn testing_clone(&self) -> Self {
Ticket(MichelsonPair(
MichelsonContract(self.creator().0.clone()),
MichelsonPair(
MichelsonString(self.contents().0.clone()),
MichelsonInt(Zarith(self.amount().clone())),
),
))
}
}
pub type BytesTicket = Ticket<MichelsonBytes>;
pub type UnitTicket = Ticket<MichelsonUnit>;
#[cfg(test)]
mod test {
use super::*;
use tezos_data_encoding::enc::BinWriter;
use tezos_data_encoding::nom::NomReader;
#[test]
fn content_bytes() {
let ticket = BytesTicket::new(
Contract::from_b58check("KT1NgXQ6Mwu3XKFDcKdYFS6dkkY3iNKdBKEc").unwrap(),
MichelsonBytes(vec![1, 2, 3, 4, 5]),
500,
)
.unwrap();
assert_encode_decode(ticket);
}
#[test]
fn content_string() {
let ticket = StringTicket::new(
Contract::from_b58check("KT1NgXQ6Mwu3XKFDcKdYFS6dkkY3iNKdBKEc").unwrap(),
MichelsonString("Hello, Ticket".to_string()),
900,
)
.unwrap();
assert_encode_decode(ticket);
}
#[test]
fn content_unit() {
let ticket = UnitTicket::new(
Contract::from_b58check("KT1NgXQ6Mwu3XKFDcKdYFS6dkkY3iNKdBKEc").unwrap(),
MichelsonUnit,
900,
)
.unwrap();
assert_encode_decode(ticket);
}
#[test]
fn content_int() {
let ticket = IntTicket::new::<i32, i32>(
Contract::from_b58check("KT1NgXQ6Mwu3XKFDcKdYFS6dkkY3iNKdBKEc").unwrap(),
-25,
900,
)
.unwrap();
assert_encode_decode(ticket);
}
#[test]
fn content_pair() {
type NestedPair = MichelsonPair<
MichelsonUnit,
MichelsonPair<MichelsonPair<MichelsonString, MichelsonBytes>, MichelsonInt>,
>;
let ticket: Ticket<NestedPair> = Ticket::new::<_, i32>(
Contract::from_b58check("KT1NgXQ6Mwu3XKFDcKdYFS6dkkY3iNKdBKEc").unwrap(),
MichelsonPair(
MichelsonUnit,
MichelsonPair(
MichelsonPair(
MichelsonString("hello".to_string()),
MichelsonBytes(b"a series of bytes".to_vec()),
),
MichelsonInt::from(19),
),
),
17,
)
.unwrap();
assert_encode_decode(ticket);
}
fn assert_encode_decode<T: Michelson>(ticket: Ticket<T>) {
let mut bin = Vec::new();
ticket.bin_write(&mut bin).unwrap();
let (remaining, parsed) = Ticket::nom_read(&bin).unwrap();
assert_eq!(ticket, parsed);
assert!(remaining.is_empty());
}
}