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