diff --git a/Cargo.lock b/Cargo.lock index b370ac7c..1a3474b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -150,6 +150,12 @@ version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.4.10" @@ -200,6 +206,7 @@ version = "0.1.0" dependencies = [ "data-encoding", "lbr-prelude", + "linked-hash-map", "num-bigint", "proptest", "serde", diff --git a/Cargo.toml b/Cargo.toml index e81d5f15..d211d72d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ serde = { version = "1.0.189", features = ["derive"], optional = true } true = { version = "0.1.0", optional = true } data-encoding = { version = "2.4.0", optional = true } thiserror = "1.0.50" +linked-hash-map = "0.5.6" [features] serde = ["dep:serde", "num-bigint/serde"] diff --git a/src/generators/correct/v1.rs b/src/generators/correct/v1.rs index 1256e4ba..0e4f8b6c 100644 --- a/src/generators/correct/v1.rs +++ b/src/generators/correct/v1.rs @@ -7,13 +7,15 @@ use crate::plutus_data::PlutusData; use crate::v1::address::{ Address, CertificateIndex, ChainPointer, Credential, Slot, StakingCredential, TransactionIndex, }; -use crate::v1::crypto::{Ed25519PubKeyHash, LedgerBytes}; +use crate::v1::assoc_map::AssocMap; +use crate::v1::crypto::{Ed25519PubKeyHash, LedgerBytes, PaymentPubKeyHash}; use crate::v1::datum::{Datum, DatumHash}; use crate::v1::interval::{Extended, LowerBound, PlutusInterval, UpperBound}; use crate::v1::redeemer::{Redeemer, RedeemerHash}; use crate::v1::script::{MintingPolicyHash, ScriptHash, ValidatorHash}; use crate::v1::transaction::{ - POSIXTime, TransactionHash, TransactionInput, TransactionOutput, TxInInfo, + DCert, POSIXTime, ScriptContext, ScriptPurpose, TransactionHash, TransactionInfo, + TransactionInput, TransactionOutput, TxInInfo, }; use crate::v2::value::{AssetClass, CurrencySymbol, TokenName, Value}; use num_bigint::BigInt; @@ -243,7 +245,7 @@ pub fn arb_transaction_index() -> impl Strategy { arb_integer().prop_map(TransactionIndex) } -/// Strategy to generate a certificate index +/// Strategy to generate a certificate index. pub fn arb_certificate_index() -> impl Strategy { arb_integer().prop_map(CertificateIndex) } @@ -293,3 +295,80 @@ pub fn arb_tx_in_info() -> impl Strategy { (arb_transaction_input(), arb_transaction_output()) .prop_map(|(reference, output)| TxInInfo { reference, output }) } + +/// Strategy to generate an AssocMap, given the strategies to generate keys and values +pub fn arb_assoc_map( + arb_k: impl Strategy, + arb_v: impl Strategy, +) -> impl Strategy> { + vec((arb_k, arb_v), 10).prop_map(AssocMap) +} + +/// Strategy to generate a PaymentPubKeyHash +pub fn arb_payment_pub_key_hash() -> impl Strategy { + arb_ed25519_pub_key_hash().prop_map(PaymentPubKeyHash) +} + +/// Strategy to generate a DCert +pub fn arb_d_cert() -> impl Strategy { + prop_oneof![ + arb_staking_credential().prop_map(DCert::DelegKey), + arb_staking_credential().prop_map(DCert::DelegDeregKey), + (arb_staking_credential(), arb_payment_pub_key_hash()) + .prop_map(|(sc, pkh)| DCert::DelegDelegate(sc, pkh)), + (arb_payment_pub_key_hash(), arb_payment_pub_key_hash()) + .prop_map(|(p1, p2)| DCert::PoolRegister(p1, p2)), + (arb_payment_pub_key_hash(), arb_integer()).prop_map(|(pkh, i)| DCert::PoolRetire(pkh, i)), + Just(DCert::Genesis), + Just(DCert::Mir) + ] +} + +/// Strategy to generate a ScriptPurpose +pub fn arb_script_purpose() -> impl Strategy { + prop_oneof![ + arb_currency_symbol().prop_map(ScriptPurpose::Minting), + arb_transaction_input().prop_map(ScriptPurpose::Spending), + arb_staking_credential().prop_map(ScriptPurpose::Rewarding), + arb_d_cert().prop_map(ScriptPurpose::Certifying) + ] +} + +/// Strategy to generate a TransactionInfo. Note that its inputs, outputs, d_cert, +/// signatories and datums field will each have a length of 0 to 5 +pub fn arb_transaction_info() -> impl Strategy { + ( + vec(arb_tx_in_info(), 5), + vec(arb_transaction_output(), 5), + arb_value(), + arb_value(), + vec(arb_d_cert(), 5), + vec((arb_staking_credential(), arb_integer()), 5), + arb_plutus_interval_posix_time(), + vec(arb_payment_pub_key_hash(), 5), + vec((arb_datum_hash(), arb_datum()), 5), + arb_transaction_hash(), + ) + .prop_map( + |(inputs, outputs, fee, mint, d_cert, wdrl, valid_range, signatories, datums, id)| { + TransactionInfo { + inputs, + outputs, + fee, + mint, + d_cert, + wdrl, + valid_range, + signatories, + datums, + id, + } + }, + ) +} + +/// Strategy to generate a ScriptContext +pub fn arb_script_context() -> impl Strategy { + (arb_script_purpose(), arb_transaction_info()) + .prop_map(|(purpose, tx_info)| ScriptContext { purpose, tx_info }) +} diff --git a/src/generators/correct/v2.rs b/src/generators/correct/v2.rs index 8f3f4176..6d5bda4f 100644 --- a/src/generators/correct/v2.rs +++ b/src/generators/correct/v2.rs @@ -5,11 +5,18 @@ use crate::generators::correct::v1::{ arb_address, arb_datum, arb_datum_hash, arb_script_hash, arb_transaction_input, arb_value, }; use crate::v2::datum::OutputDatum; -use crate::v2::transaction::{TransactionOutput, TxInInfo}; +use crate::v2::transaction::{ScriptContext, TransactionInfo, TransactionOutput, TxInInfo}; +use proptest::collection::vec; use proptest::option; use proptest::prelude::{prop_oneof, Just}; use proptest::strategy::Strategy; +use super::primitive::arb_integer; +use super::v1::{ + arb_assoc_map, arb_d_cert, arb_payment_pub_key_hash, arb_plutus_interval_posix_time, + arb_redeemer, arb_script_purpose, arb_staking_credential, arb_transaction_hash, +}; + /// Strategy to generate transaction output pub fn arb_transaction_output() -> impl Strategy { ( @@ -42,3 +49,56 @@ pub fn arb_tx_in_info() -> impl Strategy { (arb_transaction_input(), arb_transaction_output()) .prop_map(|(reference, output)| TxInInfo { reference, output }) } + +pub fn arb_transaction_info() -> impl Strategy { + ( + vec(arb_tx_in_info(), 5), + vec(arb_tx_in_info(), 5), + vec(arb_transaction_output(), 5), + arb_value(), + arb_value(), + vec(arb_d_cert(), 5), + arb_assoc_map(arb_staking_credential(), arb_integer()), + arb_plutus_interval_posix_time(), + vec(arb_payment_pub_key_hash(), 5), + arb_assoc_map(arb_script_purpose(), arb_redeemer()), + arb_assoc_map(arb_datum_hash(), arb_datum()), + arb_transaction_hash(), + ) + .prop_map( + |( + inputs, + reference_inputs, + outputs, + fee, + mint, + d_cert, + wdrl, + valid_range, + signatories, + redeemers, + datums, + id, + )| { + TransactionInfo { + inputs, + reference_inputs, + outputs, + fee, + mint, + d_cert, + wdrl, + valid_range, + signatories, + redeemers, + datums, + id, + } + }, + ) +} + +pub fn arb_script_context() -> impl Strategy { + (arb_script_purpose(), arb_transaction_info()) + .prop_map(|(purpose, tx_info)| ScriptContext { purpose, tx_info }) +} diff --git a/src/lib.rs b/src/lib.rs index cac4a3b8..72d9425e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,3 +7,4 @@ pub mod v1; pub mod v2; #[cfg(feature = "lbf")] pub use lbr_prelude::json; +pub(crate) mod utils; diff --git a/src/plutus_data.rs b/src/plutus_data.rs index 90513ca0..4607217a 100644 --- a/src/plutus_data.rs +++ b/src/plutus_data.rs @@ -568,3 +568,51 @@ pub fn verify_constr_fields( Ok(()) } } + +/// Given a vector of PlutusData, parse it as an array whose length is known at +/// compile time. +pub fn parse_fixed_len_constr_fields<'a, const LEN: usize>( + v: &'a [PlutusData], +) -> Result<&'a [PlutusData; LEN], PlutusDataError> { + v.try_into() + .map_err(|_| PlutusDataError::UnexpectedListLength { + got: v.len(), + wanted: LEN, + }) +} + +/// Given a PlutusData, parse it as PlutusData::Constr and its tag as u32. Return +/// the u32 tag and fields. +pub fn parse_constr<'a>( + data: &'a PlutusData, +) -> Result<(u32, &'a Vec), PlutusDataError> { + match data { + PlutusData::Constr(tag, fields) => u32::try_from(tag) + .map_err(|err| PlutusDataError::UnexpectedPlutusInvariant { + got: err.to_string(), + wanted: "Constr bigint tag within u32 range".into(), + }) + .map(|tag| (tag, fields)), + _ => Err(PlutusDataError::UnexpectedPlutusType { + wanted: PlutusType::Constr, + got: PlutusType::from(data), + }), + } +} + +/// Given a PlutusData, parse it as PlutusData::Constr and verify its tag. +pub fn parse_constr_with_tag<'a>( + data: &'a PlutusData, + expected_tag: u32, +) -> Result<&'a Vec, PlutusDataError> { + let (tag, fields) = parse_constr(data)?; + + if tag != expected_tag { + Err(PlutusDataError::UnexpectedPlutusInvariant { + got: tag.to_string(), + wanted: format!("Constr tag to be: {}", expected_tag), + }) + } else { + Ok(fields) + } +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 00000000..2edb5543 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,17 @@ +use std::iter::{empty, once}; + +/// Create a container C from one element. +pub fn singleton(value: T) -> C +where + C: FromIterator, +{ + once(value).collect() +} + +/// Create an empty container. +pub fn none() -> C +where + C: FromIterator, +{ + empty::().collect() +} diff --git a/src/v1/assoc_map.rs b/src/v1/assoc_map.rs new file mode 100644 index 00000000..63ae0fb5 --- /dev/null +++ b/src/v1/assoc_map.rs @@ -0,0 +1,98 @@ +use std::hash::Hash; + +#[cfg(feature = "lbf")] +use lbr_prelude::json::{json_array, Json}; +use linked_hash_map::LinkedHashMap; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::plutus_data::{IsPlutusData, PlutusData, PlutusDataError, PlutusType}; + +#[derive(Debug, PartialEq, Eq, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct AssocMap(pub Vec<(K, V)>); + +impl IsPlutusData for AssocMap { + fn to_plutus_data(&self) -> PlutusData { + PlutusData::Map( + (&self.0) + .into_iter() + .map(|(k, v)| (k.to_plutus_data(), v.to_plutus_data())) + .collect(), + ) + } + + fn from_plutus_data(plutus_data: &PlutusData) -> Result { + match plutus_data { + PlutusData::Map(pairs) => pairs + .into_iter() + .map(|(k, v)| Ok((K::from_plutus_data(k)?, V::from_plutus_data(v)?))) + .collect::, PlutusDataError>>() + .map(Self), + _ => Err(PlutusDataError::UnexpectedPlutusType { + got: From::from(plutus_data), + wanted: PlutusType::Map, + }), + } + } +} + +impl From> for AssocMap { + fn from(vec: Vec<(K, V)>) -> Self { + AssocMap(vec) + } +} + +impl From> for Vec<(K, V)> { + fn from(m: AssocMap) -> Self { + m.0 + } +} + +impl From> for LinkedHashMap { + fn from(m: AssocMap) -> Self { + m.0.into_iter().collect() + } +} + +impl From> for AssocMap { + fn from(value: LinkedHashMap) -> Self { + AssocMap(value.into_iter().collect()) + } +} + +#[cfg(feature = "lbf")] +impl Json for AssocMap { + fn to_json(&self) -> serde_json::Value { + json_array( + (&self.0) + .into_iter() + .map(|(k, v)| json_array(vec![k.to_json(), v.to_json()])) + .collect(), + ) + } + + fn from_json(value: &serde_json::Value) -> Result { + let vec_of_vectors: Vec> = Json::from_json(value)?; + let vec_of_pairs = vec_of_vectors + .into_iter() + .map(|vec| { + let [k, v]: [serde_json::Value; 2] = + TryFrom::try_from(vec).map_err(|vec: Vec<_>| { + lbr_prelude::json::Error::UnexpectedArrayLength { + got: vec.len(), + wanted: 2, + parser: "v1::assoc_map::AssocMap".into(), + } + })?; + + let k = K::from_json(&k)?; + let v = V::from_json(&v)?; + + Ok((k, v)) + }) + .collect::, _>>()?; + + Ok(Self(vec_of_pairs)) + } +} diff --git a/src/v1/mod.rs b/src/v1/mod.rs index 0b41aa5d..1e070d28 100644 --- a/src/v1/mod.rs +++ b/src/v1/mod.rs @@ -1,5 +1,6 @@ //! Plutus types and utilities for Plutus V1 pub mod address; +pub mod assoc_map; pub mod crypto; pub mod datum; pub mod interval; diff --git a/src/v1/transaction.rs b/src/v1/transaction.rs index 6e800b17..0bd46189 100644 --- a/src/v1/transaction.rs +++ b/src/v1/transaction.rs @@ -1,16 +1,19 @@ //! Types related to Cardano transactions. +use super::{ + address::{Address, StakingCredential}, + crypto::{LedgerBytes, PaymentPubKeyHash}, + datum::{Datum, DatumHash}, + interval::PlutusInterval, + value::{CurrencySymbol, Value}, +}; use crate::plutus_data::{ - verify_constr_fields, IsPlutusData, PlutusData, PlutusDataError, PlutusType, + parse_constr, parse_constr_with_tag, parse_fixed_len_constr_fields, verify_constr_fields, + IsPlutusData, PlutusData, PlutusDataError, PlutusType, }; -use crate::v1::address::Address; -use crate::v1::crypto::LedgerBytes; -use crate::v1::datum::DatumHash; -use crate::v1::interval::PlutusInterval; -use crate::v1::value::Value; +use crate::utils::{none, singleton}; #[cfg(feature = "lbf")] use lbr_prelude::json::Json; use num_bigint::BigInt; - #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -207,3 +210,222 @@ impl IsPlutusData for TxInInfo { } } } + +/// Partial representation of digests of certificates on the ledger. +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "lbf", derive(Json))] +pub enum DCert { + DelegKey(StakingCredential), + DelegDeregKey(StakingCredential), + DelegDelegate( + /// Delegator + StakingCredential, + /// Delegatee + PaymentPubKeyHash, + ), + /// A digest of the PoolParam + PoolRegister( + /// Pool id + PaymentPubKeyHash, + /// Pool VFR + PaymentPubKeyHash, + ), + PoolRetire( + PaymentPubKeyHash, + /// Epoch + BigInt, + ), + Genesis, + Mir, +} + +impl IsPlutusData for DCert { + fn to_plutus_data(&self) -> PlutusData { + let (tag, fields) = match self { + DCert::DelegKey(c) => (0u32, singleton(c.to_plutus_data())), + DCert::DelegDeregKey(c) => (1, singleton(c.to_plutus_data())), + DCert::DelegDelegate(c, pkh) => (2, vec![c.to_plutus_data(), pkh.to_plutus_data()]), + DCert::PoolRegister(pkh, pkh1) => { + (3, vec![pkh.to_plutus_data(), pkh1.to_plutus_data()]) + } + DCert::PoolRetire(pkh, i) => (4, vec![pkh.to_plutus_data(), i.to_plutus_data()]), + DCert::Genesis => (5, none()), + DCert::Mir => (6, none()), + }; + + PlutusData::Constr(BigInt::from(tag), fields) + } + + fn from_plutus_data(data: &PlutusData) -> Result { + let (tag, fields) = parse_constr(data)?; + + match tag { + 0 => { + let [field] = parse_fixed_len_constr_fields::<1>(fields)?; + IsPlutusData::from_plutus_data(field).map(Self::DelegKey) + } + 1 => { + let [field] = parse_fixed_len_constr_fields::<1>(fields)?; + IsPlutusData::from_plutus_data(field).map(Self::DelegDeregKey) + } + 2 => { + let [field1, field2] = parse_fixed_len_constr_fields::<2>(fields)?; + Ok(Self::DelegDelegate( + IsPlutusData::from_plutus_data(field1)?, + IsPlutusData::from_plutus_data(field2)?, + )) + } + 3 => { + let [field1, field2] = parse_fixed_len_constr_fields::<2>(fields)?; + Ok(Self::PoolRegister( + IsPlutusData::from_plutus_data(field1)?, + IsPlutusData::from_plutus_data(field2)?, + )) + } + 4 => { + let [field1, field2] = parse_fixed_len_constr_fields::<2>(fields)?; + Ok(Self::PoolRetire( + IsPlutusData::from_plutus_data(field1)?, + IsPlutusData::from_plutus_data(field2)?, + )) + } + 5 => { + let [] = parse_fixed_len_constr_fields::<0>(fields)?; + Ok(Self::Genesis) + } + 6 => { + let [] = parse_fixed_len_constr_fields::<0>(fields)?; + Ok(Self::Mir) + } + bad_tag => Err(PlutusDataError::UnexpectedPlutusInvariant { + wanted: "Constr tag to be 0, 1, 2, 3, 4, 5 or 6".to_owned(), + got: bad_tag.to_string(), + }), + } + } +} + +/// The purpose of the script that's currently running. +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "lbf", derive(Json))] +pub enum ScriptPurpose { + Minting(CurrencySymbol), + Spending(TransactionInput), + Rewarding(StakingCredential), + Certifying(DCert), +} + +impl IsPlutusData for ScriptPurpose { + fn to_plutus_data(&self) -> PlutusData { + let (tag, field) = match self { + ScriptPurpose::Minting(cs) => (0u32, cs.to_plutus_data()), + ScriptPurpose::Spending(i) => (1, i.to_plutus_data()), + ScriptPurpose::Rewarding(c) => (2, c.to_plutus_data()), + ScriptPurpose::Certifying(c) => (3, c.to_plutus_data()), + }; + + PlutusData::Constr(BigInt::from(tag), singleton(field)) + } + + fn from_plutus_data(plutus_data: &PlutusData) -> Result { + let (tag, fields) = parse_constr(plutus_data)?; + let [field] = parse_fixed_len_constr_fields(fields)?; + + match tag { + 0 => IsPlutusData::from_plutus_data(field).map(Self::Minting), + 1 => IsPlutusData::from_plutus_data(field).map(Self::Spending), + 2 => IsPlutusData::from_plutus_data(field).map(Self::Rewarding), + 3 => IsPlutusData::from_plutus_data(field).map(Self::Certifying), + bad_tag => Err(PlutusDataError::UnexpectedPlutusInvariant { + got: bad_tag.to_string(), + wanted: format!("Constr tag to be 0, 1, 2 or 3"), + }), + } + } +} + +/// A pending transaction as seen by validator scripts, also known as TxInfo in Plutus +#[derive(Debug, PartialEq, Eq, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "lbf", derive(Json))] +pub struct TransactionInfo { + pub inputs: Vec, + pub outputs: Vec, + pub fee: Value, + pub mint: Value, + pub d_cert: Vec, + pub wdrl: Vec<(StakingCredential, BigInt)>, + pub valid_range: POSIXTimeRange, + pub signatories: Vec, + pub datums: Vec<(DatumHash, Datum)>, + pub id: TransactionHash, +} + +impl IsPlutusData for TransactionInfo { + fn to_plutus_data(&self) -> PlutusData { + PlutusData::Constr( + BigInt::from(0), + vec![ + self.inputs.to_plutus_data(), + self.outputs.to_plutus_data(), + self.fee.to_plutus_data(), + self.mint.to_plutus_data(), + self.d_cert.to_plutus_data(), + self.wdrl.to_plutus_data(), + self.valid_range.to_plutus_data(), + self.signatories.to_plutus_data(), + self.datums.to_plutus_data(), + self.id.to_plutus_data(), + ], + ) + } + + fn from_plutus_data(data: &PlutusData) -> Result { + let fields = parse_constr_with_tag(data, 0)?; + let [inputs, outputs, fee, mint, d_cert, wdrl, valid_range, signatories, datums, id] = + parse_fixed_len_constr_fields(fields)?; + + Ok(Self { + inputs: IsPlutusData::from_plutus_data(inputs)?, + outputs: IsPlutusData::from_plutus_data(outputs)?, + fee: IsPlutusData::from_plutus_data(fee)?, + mint: IsPlutusData::from_plutus_data(mint)?, + d_cert: IsPlutusData::from_plutus_data(d_cert)?, + wdrl: IsPlutusData::from_plutus_data(wdrl)?, + valid_range: IsPlutusData::from_plutus_data(valid_range)?, + signatories: IsPlutusData::from_plutus_data(signatories)?, + datums: IsPlutusData::from_plutus_data(datums)?, + id: IsPlutusData::from_plutus_data(id)?, + }) + } +} + +/// The context that is presented to the currently-executing script. +#[derive(Debug, PartialEq, Eq, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "lbf", derive(Json))] +pub struct ScriptContext { + pub purpose: ScriptPurpose, + pub tx_info: TransactionInfo, +} + +impl IsPlutusData for ScriptContext { + fn to_plutus_data(&self) -> PlutusData { + PlutusData::Constr( + BigInt::from(0), + vec![self.purpose.to_plutus_data(), self.tx_info.to_plutus_data()], + ) + } + + fn from_plutus_data(data: &PlutusData) -> Result { + let fields = parse_constr_with_tag(data, 0)?; + let [purpose, tx_info] = parse_fixed_len_constr_fields(fields)?; + + Ok(Self { + purpose: IsPlutusData::from_plutus_data(purpose)?, + tx_info: IsPlutusData::from_plutus_data(tx_info)?, + }) + } +} diff --git a/src/v2/mod.rs b/src/v2/mod.rs index ac7c2e3f..a2d84855 100644 --- a/src/v2/mod.rs +++ b/src/v2/mod.rs @@ -6,6 +6,7 @@ pub mod transaction; // Inherited from v1 pub use crate::v1::address; +pub use crate::v1::assoc_map; pub use crate::v1::crypto; pub use crate::v1::interval; pub use crate::v1::redeemer; diff --git a/src/v2/transaction.rs b/src/v2/transaction.rs index 27a9d09a..91242d29 100644 --- a/src/v2/transaction.rs +++ b/src/v2/transaction.rs @@ -1,12 +1,11 @@ //! Types related to Cardano transactions. +use crate::plutus_data::{parse_constr_with_tag, parse_fixed_len_constr_fields}; use crate::plutus_data::{ verify_constr_fields, IsPlutusData, PlutusData, PlutusDataError, PlutusType, }; -pub use crate::v1::transaction::{POSIXTime, POSIXTimeRange, TransactionHash, TransactionInput}; -use crate::v2::address::Address; -use crate::v2::datum::OutputDatum; -use crate::v2::script::ScriptHash; -use crate::v2::value::Value; +pub use crate::v1::transaction::{ + DCert, POSIXTime, POSIXTimeRange, ScriptPurpose, TransactionHash, TransactionInput, +}; #[cfg(feature = "lbf")] use lbr_prelude::json::Json; use num_bigint::BigInt; @@ -14,6 +13,16 @@ use num_bigint::BigInt; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +use super::{ + address::{Address, StakingCredential}, + assoc_map::AssocMap, + crypto::PaymentPubKeyHash, + datum::{Datum, DatumHash, OutputDatum}, + redeemer::Redeemer, + script::ScriptHash, + value::Value, +}; + /// An output of a transaction /// /// This must include the target address, an optional datum, an optional reference script, and the @@ -110,3 +119,93 @@ impl IsPlutusData for TxInInfo { } } } + +/// A pending transaction as seen by validator scripts, also known as TxInfo in Plutus +#[derive(Debug, PartialEq, Eq, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "lbf", derive(Json))] +pub struct TransactionInfo { + pub inputs: Vec, + pub reference_inputs: Vec, + pub outputs: Vec, + pub fee: Value, + pub mint: Value, + pub d_cert: Vec, + pub wdrl: AssocMap, + pub valid_range: POSIXTimeRange, + pub signatories: Vec, + pub redeemers: AssocMap, + pub datums: AssocMap, + pub id: TransactionHash, +} + +impl IsPlutusData for TransactionInfo { + fn to_plutus_data(&self) -> PlutusData { + PlutusData::Constr( + BigInt::from(0), + vec![ + self.inputs.to_plutus_data(), + self.reference_inputs.to_plutus_data(), + self.outputs.to_plutus_data(), + self.fee.to_plutus_data(), + self.mint.to_plutus_data(), + self.d_cert.to_plutus_data(), + self.wdrl.to_plutus_data(), + self.valid_range.to_plutus_data(), + self.signatories.to_plutus_data(), + self.redeemers.to_plutus_data(), + self.datums.to_plutus_data(), + self.id.to_plutus_data(), + ], + ) + } + + fn from_plutus_data(data: &PlutusData) -> Result { + let fields = parse_constr_with_tag(data, 0)?; + let [inputs, reference_inputs, outputs, fee, mint, d_cert, wdrl, valid_range, signatories, redeemers, datums, id] = + parse_fixed_len_constr_fields(fields)?; + + Ok(Self { + inputs: IsPlutusData::from_plutus_data(inputs)?, + reference_inputs: IsPlutusData::from_plutus_data(reference_inputs)?, + outputs: IsPlutusData::from_plutus_data(outputs)?, + fee: IsPlutusData::from_plutus_data(fee)?, + mint: IsPlutusData::from_plutus_data(mint)?, + d_cert: IsPlutusData::from_plutus_data(d_cert)?, + wdrl: IsPlutusData::from_plutus_data(wdrl)?, + valid_range: IsPlutusData::from_plutus_data(valid_range)?, + signatories: IsPlutusData::from_plutus_data(signatories)?, + redeemers: IsPlutusData::from_plutus_data(redeemers)?, + datums: IsPlutusData::from_plutus_data(datums)?, + id: IsPlutusData::from_plutus_data(id)?, + }) + } +} + +/// The context that is presented to the currently-executing script. +#[derive(Debug, PartialEq, Eq, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "lbf", derive(Json))] +pub struct ScriptContext { + pub purpose: ScriptPurpose, + pub tx_info: TransactionInfo, +} + +impl IsPlutusData for ScriptContext { + fn to_plutus_data(&self) -> PlutusData { + PlutusData::Constr( + BigInt::from(0), + vec![self.purpose.to_plutus_data(), self.tx_info.to_plutus_data()], + ) + } + + fn from_plutus_data(data: &PlutusData) -> Result { + let fields = parse_constr_with_tag(data, 0)?; + let [purpose, tx_info] = parse_fixed_len_constr_fields(fields)?; + + Ok(Self { + purpose: IsPlutusData::from_plutus_data(purpose)?, + tx_info: IsPlutusData::from_plutus_data(tx_info)?, + }) + } +} diff --git a/tests/json.rs b/tests/json.rs index 2dbcf1bb..c918724e 100644 --- a/tests/json.rs +++ b/tests/json.rs @@ -11,7 +11,7 @@ mod json_roundtrip_tests { mod v1 { use super::from_to_json; - use plutus_ledger_api::generators::correct::v1::*; + use plutus_ledger_api::generators::correct::{primitive::arb_integer, v1::*}; use proptest::prelude::*; proptest! { @@ -83,6 +83,41 @@ mod json_roundtrip_tests { assert_eq!(val, from_to_json(&val)?); } } + + proptest! { + #[test] + fn test_bigint_assoc_map(val in arb_assoc_map(arb_integer(), arb_integer())) { + assert_eq!(val, from_to_json(&val)?); + } + } + + proptest! { + #[test] + fn test_d_cert(val in arb_d_cert()) { + assert_eq!(val, from_to_json(&val)?) + } + } + + proptest! { + #[test] + fn test_script_purpose(val in arb_script_purpose()) { + assert_eq!(val, from_to_json(&val)?) + } + } + + proptest! { + #[test] + fn test_transaction_info(val in arb_transaction_info()) { + assert_eq!(val, from_to_json(&val)?) + } + } + + proptest! { + #[test] + fn test_script_context(val in arb_script_context()) { + assert_eq!(val, from_to_json(&val)?) + } + } } mod v2 { use super::from_to_json; @@ -109,5 +144,19 @@ mod json_roundtrip_tests { assert_eq!(val, from_to_json(&val)?); } } + + proptest! { + #[test] + fn test_transaction_info(val in arb_transaction_info()) { + assert_eq!(val, from_to_json(&val)?) + } + } + + proptest! { + #[test] + fn test_script_context(val in arb_script_context()) { + assert_eq!(val, from_to_json(&val)?) + } + } } } diff --git a/tests/plutus_data.rs b/tests/plutus_data.rs index 886d234d..3344765a 100644 --- a/tests/plutus_data.rs +++ b/tests/plutus_data.rs @@ -95,7 +95,7 @@ mod plutusdata_roundtrip_tests { } mod v1 { use super::from_to_plutus_data; - use plutus_ledger_api::generators::correct::v1::*; + use plutus_ledger_api::generators::correct::{primitive::arb_integer, v1::*}; use proptest::prelude::*; proptest! { @@ -167,6 +167,48 @@ mod plutusdata_roundtrip_tests { assert_eq!(val, from_to_plutus_data(&val)?); } } + + proptest! { + #[test] + fn test_bigint_assoc_map(val in arb_assoc_map(arb_integer(), arb_integer())) { + assert_eq!(val, from_to_plutus_data(&val)?); + } + } + + proptest! { + #[test] + fn test_arb_payment_pub_key_hash(val in arb_payment_pub_key_hash()) { + assert_eq!(val, from_to_plutus_data(&val)?) + } + } + + proptest! { + #[test] + fn test_d_cert(val in arb_d_cert()) { + assert_eq!(val, from_to_plutus_data(&val)?) + } + } + + proptest! { + #[test] + fn test_script_purpose(val in arb_script_purpose()) { + assert_eq!(val, from_to_plutus_data(&val)?) + } + } + + proptest! { + #[test] + fn test_transaction_info(val in arb_transaction_info()) { + assert_eq!(val, from_to_plutus_data(&val)?) + } + } + + proptest! { + #[test] + fn test_script_context(val in arb_script_context()) { + assert_eq!(val, from_to_plutus_data(&val)?) + } + } } mod v2 { use super::from_to_plutus_data; @@ -193,5 +235,19 @@ mod plutusdata_roundtrip_tests { assert_eq!(val, from_to_plutus_data(&val)?); } } + + proptest! { + #[test] + fn test_transaction_info(val in arb_transaction_info()) { + assert_eq!(val, from_to_plutus_data(&val)?) + } + } + + proptest! { + #[test] + fn test_script_context(val in arb_script_context()) { + assert_eq!(val, from_to_plutus_data(&val)?) + } + } } }