plutus_ledger_api/v3/
transaction.rs

1//! Types related to Cardano transactions.
2
3use std::{fmt, str::FromStr};
4
5use anyhow::anyhow;
6use cardano_serialization_lib as csl;
7#[cfg(feature = "lbf")]
8use lbr_prelude::json::Json;
9use nom::{
10    character::complete::char,
11    combinator::{all_consuming, map, map_res},
12    error::{context, VerboseError},
13    sequence::{preceded, tuple},
14    Finish, IResult,
15};
16use num_bigint::BigInt;
17#[cfg(feature = "serde")]
18use serde::{Deserialize, Serialize};
19#[cfg(feature = "serde")]
20use serde_with::{DeserializeFromStr, SerializeDisplay};
21
22#[cfg(feature = "chrono")]
23pub use crate::v1::transaction::POSIXTimeConversionError;
24pub use crate::v2::transaction::{
25    DCert, POSIXTime, POSIXTimeRange, TransactionOutput, TransactionOutputWithExtraInfo,
26    WithdrawalsWithExtraInfo,
27};
28use crate::{
29    self as plutus_ledger_api,
30    aux::{big_int, guard_bytes},
31    csl::{
32        csl_to_pla::FromCSL,
33        pla_to_csl::{TryFromPLA, TryFromPLAError, TryToCSL},
34    },
35    error::ConversionError,
36    plutus_data::{IsPlutusData, PlutusData},
37    v2::{
38        address::Credential,
39        assoc_map::AssocMap,
40        crypto::{PaymentPubKeyHash, StakePubKeyHash},
41        datum::{Datum, DatumHash},
42        redeemer::Redeemer,
43        script::ScriptHash,
44        value::{CurrencySymbol, Lovelace, Value},
45    },
46};
47
48use super::{
49    crypto::{ledger_bytes, Ed25519PubKeyHash, LedgerBytes},
50    ratio::Rational,
51};
52
53/////////////////////
54// TransactionHash //
55/////////////////////
56
57/// 32-bytes blake2b256 hash of a transaction body.
58///
59/// Also known as Transaction ID or `TxID`.
60/// Note: Plutus docs might incorrectly state that it uses SHA256.
61/// V3 TransactionHash uses a more efficient Plutus Data encoding
62#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, IsPlutusData)]
63#[is_plutus_data_derive_strategy = "Newtype"]
64#[cfg_attr(feature = "serde", derive(SerializeDisplay, DeserializeFromStr))]
65#[cfg_attr(feature = "lbf", derive(Json))]
66pub struct TransactionHash(pub LedgerBytes);
67
68impl fmt::Display for TransactionHash {
69    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70        write!(f, "{}", self.0)
71    }
72}
73
74impl TransactionHash {
75    pub fn from_bytes(bytes: Vec<u8>) -> Result<Self, ConversionError> {
76        Ok(TransactionHash(LedgerBytes(guard_bytes(
77            "ScriptHash",
78            bytes,
79            32,
80        )?)))
81    }
82}
83
84impl FromCSL<csl::TransactionHash> for TransactionHash {
85    fn from_csl(value: &csl::TransactionHash) -> Self {
86        TransactionHash(LedgerBytes(value.to_bytes()))
87    }
88}
89
90impl TryFromPLA<TransactionHash> for csl::TransactionHash {
91    fn try_from_pla(val: &TransactionHash) -> Result<Self, TryFromPLAError> {
92        csl::TransactionHash::from_bytes(val.0 .0.to_owned())
93            .map_err(TryFromPLAError::CSLDeserializeError)
94    }
95}
96
97/// Nom parser for TransactionHash
98/// Expects a hexadecimal string representation of 32 bytes
99/// E.g.: 1122334455667788990011223344556677889900112233445566778899001122
100pub(crate) fn transaction_hash(input: &str) -> IResult<&str, TransactionHash, VerboseError<&str>> {
101    context(
102        "transaction_hash",
103        map_res(ledger_bytes, |LedgerBytes(bytes)| {
104            TransactionHash::from_bytes(bytes)
105        }),
106    )(input)
107}
108
109impl FromStr for TransactionHash {
110    type Err = ConversionError;
111
112    fn from_str(s: &str) -> Result<Self, Self::Err> {
113        all_consuming(transaction_hash)(s)
114            .finish()
115            .map_err(|err| {
116                ConversionError::ParseError(anyhow!(
117                    "Error while parsing TransactionHash '{}': {}",
118                    s,
119                    err
120                ))
121            })
122            .map(|(_, cs)| cs)
123    }
124}
125
126//////////////////////
127// TransactionInput //
128//////////////////////
129
130/// An input of a transaction
131///
132/// Also know as `TxOutRef` from Plutus, this identifies a UTxO by its transacton hash and index
133/// inside the transaction
134#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, IsPlutusData)]
135#[is_plutus_data_derive_strategy = "Constr"]
136#[cfg_attr(feature = "serde", derive(SerializeDisplay, DeserializeFromStr))]
137#[cfg_attr(feature = "lbf", derive(Json))]
138pub struct TransactionInput {
139    pub transaction_id: TransactionHash,
140    pub index: BigInt,
141}
142
143/// Serializing into a hexadecimal tx hash, followed by an tx id after a # (e.g. aabbcc#1)
144impl fmt::Display for TransactionInput {
145    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146        write!(f, "{}#{}", self.transaction_id.0, self.index)
147    }
148}
149
150impl FromCSL<csl::TransactionInput> for TransactionInput {
151    fn from_csl(value: &csl::TransactionInput) -> Self {
152        TransactionInput {
153            transaction_id: TransactionHash::from_csl(&value.transaction_id()),
154            index: BigInt::from_csl(&value.index()),
155        }
156    }
157}
158
159impl TryFromPLA<TransactionInput> for csl::TransactionInput {
160    fn try_from_pla(val: &TransactionInput) -> Result<Self, TryFromPLAError> {
161        Ok(csl::TransactionInput::new(
162            &val.transaction_id.try_to_csl()?,
163            val.index.try_to_csl()?,
164        ))
165    }
166}
167
168impl FromCSL<csl::TransactionInputs> for Vec<TransactionInput> {
169    fn from_csl(value: &csl::TransactionInputs) -> Self {
170        (0..value.len())
171            .map(|idx| TransactionInput::from_csl(&value.get(idx)))
172            .collect()
173    }
174}
175
176impl TryFromPLA<Vec<TransactionInput>> for csl::TransactionInputs {
177    fn try_from_pla(val: &Vec<TransactionInput>) -> Result<Self, TryFromPLAError> {
178        val.iter()
179            .try_fold(csl::TransactionInputs::new(), |mut acc, input| {
180                acc.add(&input.try_to_csl()?);
181                Ok(acc)
182            })
183    }
184}
185
186/// Nom parser for TransactionInput
187/// Expects a transaction hash of 32 bytes in hexadecimal followed by a # and an integer index
188/// E.g.: 1122334455667788990011223344556677889900112233445566778899001122#1
189pub(crate) fn transaction_input(
190    input: &str,
191) -> IResult<&str, TransactionInput, VerboseError<&str>> {
192    map(
193        tuple((transaction_hash, preceded(char('#'), big_int))),
194        |(transaction_id, index)| TransactionInput {
195            transaction_id,
196            index,
197        },
198    )(input)
199}
200
201impl FromStr for TransactionInput {
202    type Err = ConversionError;
203
204    fn from_str(s: &str) -> Result<Self, Self::Err> {
205        all_consuming(transaction_input)(s)
206            .finish()
207            .map_err(|err| {
208                ConversionError::ParseError(anyhow!(
209                    "Error while parsing TransactionInput '{}': {}",
210                    s,
211                    err
212                ))
213            })
214            .map(|(_, cs)| cs)
215    }
216}
217
218/////////////////////////////
219// ColdCommitteeCredential //
220/////////////////////////////
221
222#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, IsPlutusData)]
223#[is_plutus_data_derive_strategy = "Newtype"]
224#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
225#[cfg_attr(feature = "lbf", derive(Json))]
226pub struct ColdCommitteeCredential(pub Credential);
227
228////////////////////////////
229// HotCommitteeCredential //
230////////////////////////////
231
232#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, IsPlutusData)]
233#[is_plutus_data_derive_strategy = "Newtype"]
234#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
235#[cfg_attr(feature = "lbf", derive(Json))]
236pub struct HotCommitteeCredential(pub Credential);
237
238////////////////////
239// DrepCredential //
240////////////////////
241
242#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, IsPlutusData)]
243#[is_plutus_data_derive_strategy = "Newtype"]
244#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
245#[cfg_attr(feature = "lbf", derive(Json))]
246pub struct DRepCredential(pub Credential);
247
248//////////
249// DRep //
250//////////
251
252#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, IsPlutusData)]
253#[is_plutus_data_derive_strategy = "Constr"]
254#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
255#[cfg_attr(feature = "lbf", derive(Json))]
256pub enum DRep {
257    DRep(DRepCredential),
258    AlwaysAbstain,
259    AlwaysNoConfidence,
260}
261
262///////////////
263// Delegatee //
264///////////////
265
266#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, IsPlutusData)]
267#[is_plutus_data_derive_strategy = "Constr"]
268#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
269#[cfg_attr(feature = "lbf", derive(Json))]
270pub enum Delegatee {
271    Stake(StakePubKeyHash),
272    Vote(DRep),
273    StakeVote(StakePubKeyHash, DRep),
274}
275
276////////////
277// TxCert //
278////////////
279
280#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, IsPlutusData)]
281#[is_plutus_data_derive_strategy = "Constr"]
282#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
283#[cfg_attr(feature = "lbf", derive(Json))]
284pub enum TxCert {
285    /// Register staking credential with an optional deposit amount
286    RegStaking(Credential, Option<Lovelace>),
287    /// Un-Register staking credential with an optional refund amount
288    UnRegStaking(Credential, Option<Lovelace>),
289    /// Delegate staking credential to a Delegatee
290    DelegStaking(Credential, Delegatee),
291    /// Register and delegate staking credential to a Delegatee in one certificate. Note that deposit is mandatory.
292    RegDeleg(Credential, Delegatee, Lovelace),
293    /// Register a DRep with a deposit value. The optional anchor is omitted.
294    RegDRep(DRepCredential, Lovelace),
295    /// Update a DRep. The optional anchor is omitted.
296    UpdateDRep(DRepCredential),
297    /// UnRegister a DRep with mandatory refund value
298    UnRegDRep(DRepCredential, Lovelace),
299    /// A digest of the PoolParams
300    PoolRegister(
301        /// pool id
302        Ed25519PubKeyHash,
303        // pool vrf
304        Ed25519PubKeyHash,
305    ),
306    /// The retirement certificate and the Epoch in which the retirement will take place
307    PoolRetire(Ed25519PubKeyHash, BigInt),
308    /// Authorize a Hot credential for a specific Committee member's cold credential
309    AuthHotCommittee(ColdCommitteeCredential, HotCommitteeCredential),
310    ResignColdCommittee(ColdCommitteeCredential),
311}
312
313//////////
314// Vote //
315//////////
316
317#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, IsPlutusData)]
318#[is_plutus_data_derive_strategy = "Constr"]
319#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
320#[cfg_attr(feature = "lbf", derive(Json))]
321pub enum Voter {
322    CommitteeVoter(HotCommitteeCredential),
323    DRepVoter(DRepCredential),
324    StakePoolVoter(Ed25519PubKeyHash),
325}
326
327///////////
328// Voter //
329///////////
330
331#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, IsPlutusData)]
332#[is_plutus_data_derive_strategy = "Constr"]
333#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
334#[cfg_attr(feature = "lbf", derive(Json))]
335pub enum Vote {
336    VoteNo,
337    VoteYes,
338    Abstain,
339}
340
341////////////////////////
342// GovernanceActionId //
343////////////////////////
344
345/// Similar to TransactionInput, but for GovernanceAction.
346#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, IsPlutusData)]
347#[is_plutus_data_derive_strategy = "Constr"]
348#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
349#[cfg_attr(feature = "lbf", derive(Json))]
350pub struct GovernanceActionId {
351    pub tx_id: TransactionHash,
352    pub gov_action_id: BigInt,
353}
354
355///////////////
356// Committee //
357///////////////
358
359#[derive(Clone, Debug, PartialEq, Eq, IsPlutusData)]
360#[is_plutus_data_derive_strategy = "Constr"]
361#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
362#[cfg_attr(feature = "lbf", derive(Json))]
363pub struct Committee {
364    /// Committee members with epoch number when each of them expires
365    pub members: AssocMap<ColdCommitteeCredential, BigInt>,
366    /// Quorum of the committee that is necessary for a successful vote
367    pub quorum: Rational,
368}
369
370//////////////////
371// Constitution //
372//////////////////
373
374#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, IsPlutusData)]
375#[is_plutus_data_derive_strategy = "Constr"]
376#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
377#[cfg_attr(feature = "lbf", derive(Json))]
378pub struct Constitution {
379    /// Optional guardrail script
380    pub constitution_script: Option<ScriptHash>,
381}
382
383/////////////////////
384// ProtocolVersion //
385/////////////////////
386
387#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, IsPlutusData)]
388#[is_plutus_data_derive_strategy = "Constr"]
389#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
390#[cfg_attr(feature = "lbf", derive(Json))]
391pub struct ProtocolVersion {
392    pub major: BigInt,
393    pub minor: BigInt,
394}
395
396///////////////////////
397// ChangedParameters //
398///////////////////////
399
400// TODO(chfanghr): check invariant according to https://github.com/IntersectMBO/plutus/blob/bb33f082d26f8b6576d3f0d423be53eddfb6abd8/plutus-ledger-api/src/PlutusLedgerApi/V3/Contexts.hs#L338-L364
401/// A Plutus Data object containing proposed parameter changes.
402#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, IsPlutusData)]
403#[is_plutus_data_derive_strategy = "Newtype"]
404#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
405#[cfg_attr(feature = "lbf", derive(Json))]
406pub struct ChangedParameters(pub PlutusData);
407
408//////////////////////
409// GovernanceAction //
410//////////////////////
411
412#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, IsPlutusData)]
413#[is_plutus_data_derive_strategy = "Constr"]
414#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
415#[cfg_attr(feature = "lbf", derive(Json))]
416pub enum GovernanceAction {
417    /// Propose to change the protocol parameters
418    ParameterChange(
419        Option<GovernanceActionId>,
420        ChangedParameters,
421        // The hash of the constitution script
422        Option<ScriptHash>,
423    ),
424    /// Propose to update protocol version
425    HardForkInitiation(Option<GovernanceActionId>, ProtocolVersion),
426    /// Propose to withdraw from the cardano treasury
427    TreasuryWithdrawals(
428        AssocMap<Credential, Lovelace>,
429        // The hash of the constitution script
430        Option<ScriptHash>,
431    ),
432    /// Propose to create a state of no-confidence in the current constitutional committee
433    NoConfidence(Option<GovernanceActionId>),
434    /// Propose to update the members of the constitutional committee and/or its signature threshold and/or terms
435    UpdateCommittee(
436        Option<GovernanceActionId>,
437        /// Committee members to be removed
438        Vec<ColdCommitteeCredential>,
439        /// Committee members to be added
440        AssocMap<ColdCommitteeCredential, BigInt>,
441        /// New quorum
442        Rational,
443    ),
444    /// Propose to modify the constitution or its guardrail script
445    NewConstitution(Option<GovernanceActionId>, Constitution),
446    InfoAction,
447}
448
449///////////////////////
450// ProposalProcedure //
451///////////////////////
452
453#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Hash, IsPlutusData)]
454#[is_plutus_data_derive_strategy = "Constr"]
455#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
456#[cfg_attr(feature = "lbf", derive(Json))]
457pub struct ProposalProcedure {
458    pub deposit: Lovelace,
459    pub return_addr: Credential,
460    pub governance_action: GovernanceAction,
461}
462
463///////////////////
464// ScriptPurpose //
465///////////////////
466
467#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Hash, IsPlutusData)]
468#[is_plutus_data_derive_strategy = "Constr"]
469#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
470#[cfg_attr(feature = "lbf", derive(Json))]
471pub enum ScriptPurpose {
472    Minting(CurrencySymbol),
473    Spending(TransactionInput),
474    Rewarding(Credential),
475    Certifying(
476        /// 0-based index of the given `TxCert` in `the `tx_certs` field of the `TransactionInfo`
477        BigInt,
478        TxCert,
479    ),
480    Voting(Voter),
481    Proposing(
482        /// 0-based index of the given `ProposalProcedure` in `proposal_procedures` field of the `TransactionInfo`
483        BigInt,
484        ProposalProcedure,
485    ),
486}
487
488////////////////
489// ScriptInfo //
490////////////////
491
492#[derive(Clone, Debug, PartialEq, Eq, IsPlutusData)]
493#[is_plutus_data_derive_strategy = "Constr"]
494#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
495#[cfg_attr(feature = "lbf", derive(Json))]
496pub enum ScriptInfo {
497    Minting(CurrencySymbol),
498    Spending(TransactionInput, Option<Datum>),
499    Rewarding(Credential),
500    Certifying(BigInt, TxCert),
501    Voting(Voter),
502    Proposing(BigInt, ProposalProcedure),
503}
504
505/////////////////////
506// TransactionInfo //
507/////////////////////
508
509#[derive(Clone, Debug, PartialEq, Eq, IsPlutusData)]
510#[is_plutus_data_derive_strategy = "Constr"]
511#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
512#[cfg_attr(feature = "lbf", derive(Json))]
513pub struct TransactionInfo {
514    pub inputs: Vec<TxInInfo>,
515    pub reference_inputs: Vec<TxInInfo>,
516    pub outputs: Vec<TransactionOutput>,
517    pub fee: Lovelace,
518    pub mint: Value,
519    pub tx_certs: Vec<TxCert>,
520    pub wdrl: AssocMap<Credential, Lovelace>,
521    pub valid_range: POSIXTimeRange,
522    pub signatories: Vec<PaymentPubKeyHash>,
523    pub redeemers: AssocMap<ScriptPurpose, Redeemer>,
524    pub datums: AssocMap<DatumHash, Datum>,
525    pub id: TransactionHash,
526    pub votes: AssocMap<Voter, AssocMap<GovernanceActionId, Vote>>,
527    pub proposal_procedures: Vec<ProposalProcedure>,
528    pub current_treasury_amount: Option<Lovelace>,
529    pub treasury_donation: Option<Lovelace>,
530}
531
532//////////////
533// TxInInfo //
534//////////////
535
536/// An input of a pending transaction.
537#[derive(Clone, Debug, PartialEq, Eq, IsPlutusData)]
538#[is_plutus_data_derive_strategy = "Constr"]
539#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
540#[cfg_attr(feature = "lbf", derive(Json))]
541pub struct TxInInfo {
542    pub reference: TransactionInput,
543    pub output: TransactionOutput,
544}
545
546impl From<(TransactionInput, TransactionOutput)> for TxInInfo {
547    fn from((reference, output): (TransactionInput, TransactionOutput)) -> TxInInfo {
548        TxInInfo { reference, output }
549    }
550}
551
552///////////////////
553// ScriptContext //
554///////////////////
555
556#[derive(Clone, Debug, PartialEq, Eq, IsPlutusData)]
557#[is_plutus_data_derive_strategy = "Constr"]
558#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
559#[cfg_attr(feature = "lbf", derive(Json))]
560pub struct ScriptContext {
561    pub tx_info: TransactionInfo,
562    pub redeemer: Redeemer,
563    pub script_info: ScriptInfo,
564}