cml_chain/builders/
redeemer_builder.rs

1use super::{
2    certificate_builder::CertificateBuilderResult, input_builder::InputBuilderResult,
3    mint_builder::MintBuilderResult, proposal_builder::ProposalBuilderResult,
4    vote_builder::VoteBuilderResult, withdrawal_builder::WithdrawalBuilderResult,
5};
6use crate::{
7    address::RewardAddress,
8    plutus::{ExUnits, LegacyRedeemer, PlutusData, RedeemerTag, Redeemers},
9    transaction::TransactionInput,
10    PolicyId,
11};
12use std::{collections::BTreeMap, fmt::Debug};
13
14#[derive(Clone, Copy, PartialOrd, Ord, Debug, PartialEq, Eq, Hash)]
15pub struct RedeemerWitnessKey {
16    tag: RedeemerTag,
17    index: u64,
18}
19
20impl RedeemerWitnessKey {
21    pub fn new(tag: RedeemerTag, index: u64) -> Self {
22        Self { tag, index }
23    }
24}
25
26impl From<&LegacyRedeemer> for RedeemerWitnessKey {
27    fn from(redeemer: &LegacyRedeemer) -> Self {
28        Self {
29            tag: redeemer.tag,
30            index: redeemer.index,
31        }
32    }
33}
34
35/// LegacyRedeemer without the tag of index
36/// This allows builder code to return partial redeemers
37/// and then later have them placed in the right context
38#[derive(Clone, Debug)]
39pub struct UntaggedRedeemer {
40    pub data: PlutusData,
41    pub ex_units: ExUnits,
42}
43
44impl UntaggedRedeemer {
45    pub fn new(data: PlutusData, ex_units: ExUnits) -> Self {
46        Self { data, ex_units }
47    }
48}
49
50#[derive(Clone, Debug)]
51enum UntaggedRedeemerPlaceholder {
52    JustData(PlutusData),
53    Full(UntaggedRedeemer),
54}
55
56impl UntaggedRedeemerPlaceholder {
57    fn data(&self) -> &PlutusData {
58        match self {
59            Self::JustData(data) => data,
60            Self::Full(untagged_redeemer) => &untagged_redeemer.data,
61        }
62    }
63}
64
65/// Possible errors during conversion from bytes
66#[derive(Debug, thiserror::Error)]
67pub enum MissingExunitError {
68    #[error("Missing exunit for {0:?} with <key, index> values of <{1:?}, {2}>")]
69    Key(RedeemerTag, usize, String),
70}
71
72#[derive(Debug, thiserror::Error)]
73pub enum RedeemerBuilderError {
74    #[error("Missing ExUnit: {0}")]
75    MissingExUnit(#[from] MissingExunitError),
76}
77
78/// In order to calculate the index from the sorted set, "add_*" methods in this builder
79/// must be called along with the "add_*" methods in transaction builder.
80#[derive(Clone, Default, Debug)]
81pub struct RedeemerSetBuilder {
82    // the set of inputs is an ordered set (according to the order defined on the type TxIn) -
83    // this also is the order in which the elements of the set are indexed (lex order on the pair of TxId and Ix).
84    // All inputs of a transaction are included in the set being indexed (not just the ones that point to a Plutus script UTxO)
85    spend: BTreeMap<TransactionInput, Option<UntaggedRedeemerPlaceholder>>,
86
87    // the set of policy IDs is ordered according to the order defined on PolicyId (lex).
88    // The index of a PolicyId in this set of policy IDs is computed according to this order.
89    // Note that at the use site, the set of policy IDs passed to indexof is the (unfiltered)
90    // domain of the Value map in the mint field of the transaction.
91    mint: BTreeMap<PolicyId, Option<UntaggedRedeemerPlaceholder>>,
92
93    // the index of a reward account "ract" in the reward withdrawals map is the index of "ract" as a key in the (unfiltered) map.
94    // The keys of the Wdrl map are arranged in the order defined on the RewardAcnt type, which is a lexicographical (abbrv. lex)
95    // order on the pair of the Network and the Credential.
96    reward: BTreeMap<RewardAddress, Option<UntaggedRedeemerPlaceholder>>,
97
98    // certificates in the DCert list are indexed in the order in which they arranged in the (full, unfiltered)
99    // list of certificates inside the transaction
100    cert: Vec<Option<UntaggedRedeemerPlaceholder>>,
101
102    proposals: Vec<Option<UntaggedRedeemerPlaceholder>>,
103
104    votes: Vec<Option<UntaggedRedeemerPlaceholder>>,
105}
106
107impl RedeemerSetBuilder {
108    pub fn new() -> Self {
109        Self::default()
110    }
111
112    pub fn is_empty(&self) -> bool {
113        self.spend.is_empty()
114            && self.mint.is_empty()
115            && self.reward.is_empty()
116            && self.cert.is_empty()
117    }
118
119    /// note: will override existing value if called twice with the same key
120    pub fn update_ex_units(&mut self, key: RedeemerWitnessKey, ex_units: ExUnits) {
121        match key.tag {
122            RedeemerTag::Spend => {
123                let entry = self.spend.iter_mut().nth(key.index as usize).unwrap().1;
124                *entry = Some(UntaggedRedeemerPlaceholder::Full(UntaggedRedeemer::new(
125                    entry.as_ref().unwrap().data().clone(),
126                    ex_units,
127                )));
128            }
129            RedeemerTag::Mint => {
130                let entry = self.mint.iter_mut().nth(key.index as usize).unwrap().1;
131                *entry = Some(UntaggedRedeemerPlaceholder::Full(UntaggedRedeemer::new(
132                    entry.as_ref().unwrap().data().clone(),
133                    ex_units,
134                )));
135            }
136            RedeemerTag::Cert => {
137                let entry = self.cert.get_mut(key.index as usize).unwrap();
138                *entry = Some(UntaggedRedeemerPlaceholder::Full(UntaggedRedeemer::new(
139                    entry.as_ref().unwrap().data().clone(),
140                    ex_units,
141                )));
142            }
143            RedeemerTag::Reward => {
144                let entry = self.reward.iter_mut().nth(key.index as usize).unwrap().1;
145                *entry = Some(UntaggedRedeemerPlaceholder::Full(UntaggedRedeemer::new(
146                    entry.as_ref().unwrap().data().clone(),
147                    ex_units,
148                )));
149            }
150            RedeemerTag::Proposing => {
151                let entry = self.proposals.get_mut(key.index as usize).unwrap();
152                *entry = Some(UntaggedRedeemerPlaceholder::Full(UntaggedRedeemer::new(
153                    entry.as_ref().unwrap().data().clone(),
154                    ex_units,
155                )));
156            }
157            RedeemerTag::Voting => {
158                let entry = self.votes.get_mut(key.index as usize).unwrap();
159                *entry = Some(UntaggedRedeemerPlaceholder::Full(UntaggedRedeemer::new(
160                    entry.as_ref().unwrap().data().clone(),
161                    ex_units,
162                )));
163            }
164        }
165    }
166
167    pub fn add_spend(&mut self, result: &InputBuilderResult) {
168        let redeemer_data = {
169            result
170                .aggregate_witness
171                .as_ref()
172                .and_then(|data| data.redeemer_plutus_data())
173        };
174        if let Some(data) = redeemer_data {
175            self.spend.insert(
176                result.input.clone(),
177                Some(UntaggedRedeemerPlaceholder::JustData(data.clone())),
178            );
179        } else {
180            self.spend.insert(result.input.clone(), None);
181        }
182    }
183
184    pub fn add_mint(&mut self, result: &MintBuilderResult) {
185        let redeemer_data = {
186            result
187                .aggregate_witness
188                .as_ref()
189                .and_then(|data| data.redeemer_plutus_data())
190        };
191        if let Some(data) = redeemer_data {
192            self.mint.insert(
193                result.policy_id,
194                Some(UntaggedRedeemerPlaceholder::JustData(data.clone())),
195            );
196        } else {
197            self.mint.insert(result.policy_id, None);
198        }
199    }
200
201    pub fn add_reward(&mut self, result: &WithdrawalBuilderResult) {
202        let redeemer_data = {
203            result
204                .aggregate_witness
205                .as_ref()
206                .and_then(|data| data.redeemer_plutus_data())
207        };
208        if let Some(data) = redeemer_data {
209            self.reward.insert(
210                result.address.clone(),
211                Some(UntaggedRedeemerPlaceholder::JustData(data.clone())),
212            );
213        } else {
214            self.reward.insert(result.address.clone(), None);
215        }
216    }
217
218    pub fn add_cert(&mut self, result: &CertificateBuilderResult) {
219        let redeemer_data = {
220            result
221                .aggregate_witness
222                .as_ref()
223                .and_then(|data| data.redeemer_plutus_data())
224        };
225        if let Some(data) = redeemer_data {
226            self.cert
227                .push(Some(UntaggedRedeemerPlaceholder::JustData(data.clone())));
228        } else {
229            self.cert.push(None);
230        }
231    }
232
233    pub fn add_proposal(&mut self, result: &ProposalBuilderResult) {
234        for aggregate_witness in &result.aggregate_witnesses {
235            if let Some(data) = aggregate_witness.redeemer_plutus_data() {
236                self.proposals
237                    .push(Some(UntaggedRedeemerPlaceholder::JustData(data.clone())));
238            } else {
239                self.proposals.push(None);
240            }
241        }
242    }
243
244    pub fn add_vote(&mut self, result: &VoteBuilderResult) {
245        for aggregate_witness in &result.aggregate_witnesses {
246            if let Some(data) = aggregate_witness.redeemer_plutus_data() {
247                self.votes
248                    .push(Some(UntaggedRedeemerPlaceholder::JustData(data.clone())));
249            } else {
250                self.votes.push(None);
251            }
252        }
253    }
254
255    pub fn build(&self, default_to_dummy_exunits: bool) -> Result<Redeemers, RedeemerBuilderError> {
256        let mut redeemers = Vec::new();
257        // Calling iter on a BTreeMap returns a list of sorted keys
258        self.remove_placeholders_and_tag(
259            &mut redeemers,
260            RedeemerTag::Spend,
261            &mut self.spend.iter(),
262            default_to_dummy_exunits,
263        )?;
264        self.remove_placeholders_and_tag(
265            &mut redeemers,
266            RedeemerTag::Mint,
267            &mut self.mint.iter(),
268            default_to_dummy_exunits,
269        )?;
270        self.remove_placeholders_and_tag(
271            &mut redeemers,
272            RedeemerTag::Reward,
273            &mut self.reward.iter(),
274            default_to_dummy_exunits,
275        )?;
276        self.remove_placeholders_and_tag(
277            &mut redeemers,
278            RedeemerTag::Cert,
279            &mut self.cert.iter().map(|entry| (&(), entry)),
280            default_to_dummy_exunits,
281        )?;
282        self.remove_placeholders_and_tag(
283            &mut redeemers,
284            RedeemerTag::Proposing,
285            &mut self.proposals.iter().map(|entry| (&(), entry)),
286            default_to_dummy_exunits,
287        )?;
288        self.remove_placeholders_and_tag(
289            &mut redeemers,
290            RedeemerTag::Voting,
291            &mut self.votes.iter().map(|entry| (&(), entry)),
292            default_to_dummy_exunits,
293        )?;
294
295        Ok(Redeemers::new_arr_legacy_redeemer(redeemers))
296    }
297
298    fn remove_placeholders_and_tag<'a, K: Debug + Clone>(
299        &self,
300        redeemers: &mut Vec<LegacyRedeemer>,
301        tag: RedeemerTag,
302        entries: &mut dyn Iterator<Item = (&'a K, &'a Option<UntaggedRedeemerPlaceholder>)>,
303        default_to_dummy_exunits: bool,
304    ) -> Result<(), RedeemerBuilderError> {
305        let mut result = vec![];
306        for (i, entry) in entries.enumerate() {
307            let key = (tag, i, entry.0);
308
309            let redeemer = match entry.1 {
310                Some(UntaggedRedeemerPlaceholder::JustData(data)) => {
311                    if !default_to_dummy_exunits {
312                        Err(RedeemerBuilderError::MissingExUnit(
313                            MissingExunitError::Key(key.0, key.1, format!("{:?}", key.2)),
314                        ))
315                    } else {
316                        Ok(Some(UntaggedRedeemer::new(data.clone(), ExUnits::dummy())))
317                    }
318                }
319                Some(UntaggedRedeemerPlaceholder::Full(untagged_redeemer)) => {
320                    Ok(Some(untagged_redeemer.clone()))
321                }
322                None => Ok(None),
323            }?;
324            result.push(redeemer);
325        }
326        redeemers.append(&mut Self::tag_redeemer(tag, &result));
327        Ok(())
328    }
329
330    fn tag_redeemer(
331        tag: RedeemerTag,
332        untagged_redeemers: &[Option<UntaggedRedeemer>],
333    ) -> Vec<LegacyRedeemer> {
334        let mut result = Vec::new();
335
336        for (index, untagged_redeemer) in untagged_redeemers.iter().enumerate() {
337            if let Some(untagged_redeemer) = untagged_redeemer {
338                result.push(LegacyRedeemer::new(
339                    tag,
340                    index as u64,
341                    untagged_redeemer.data.clone(),
342                    untagged_redeemer.ex_units.clone(),
343                ));
344            }
345        }
346        result
347    }
348}
349
350#[cfg(test)]
351mod tests {
352    use crate::{
353        address::Address,
354        builders::witness_builder::{
355            InputAggregateWitnessData, PartialPlutusWitness, PlutusScriptWitness,
356            RequiredWitnessSet,
357        },
358        plutus::{PlutusScript, PlutusV1Script},
359        transaction::AlonzoFormatTxOut,
360        Value,
361    };
362    use cml_crypto::{PublicKey, RawBytesEncoding, TransactionHash};
363
364    use super::*;
365
366    fn fake_raw_key_public(id: u8) -> PublicKey {
367        PublicKey::from_raw_bytes(&[
368            id, 118, 57, 154, 33, 13, 232, 114, 14, 159, 168, 148, 228, 94, 65, 226, 154, 181, 37,
369            227, 11, 196, 2, 128, 28, 7, 98, 80, 209, 88, 91, 205,
370        ])
371        .unwrap()
372    }
373
374    #[test]
375    fn test_redeemer_set_builder() {
376        let mut builder = RedeemerSetBuilder::new();
377
378        let data = {
379            let witness = {
380                let script = PlutusScript::PlutusV1(PlutusV1Script::new(vec![0]));
381                PartialPlutusWitness {
382                    script: PlutusScriptWitness::Script(script),
383                    redeemer: PlutusData::new_integer(0u64.into()),
384                }
385            };
386            let missing_signers = vec![fake_raw_key_public(0).hash()];
387            InputAggregateWitnessData::PlutusScript(witness, missing_signers.into(), None)
388        };
389
390        let address = Address::from_bech32("addr1qxeqxcja25k8q05evyngf4f88xn89asl54x2zg3ephgj26ndyt5qk02xmmras5pe9jz2c7tc93wu4c96rqwvg6e2v50qlpmx70").unwrap();
391
392        let input_result = InputBuilderResult {
393            input: TransactionInput::new(TransactionHash::from([1; 32]), 1),
394            utxo_info: AlonzoFormatTxOut::new(address.clone(), Value::zero()).into(),
395            aggregate_witness: None,
396            required_wits: RequiredWitnessSet::new(),
397        };
398
399        builder.add_spend(&input_result);
400
401        let input_result = InputBuilderResult {
402            input: TransactionInput::new(TransactionHash::from([1; 32]), 0),
403            utxo_info: AlonzoFormatTxOut::new(address.clone(), Value::zero()).into(),
404            aggregate_witness: None,
405            required_wits: RequiredWitnessSet::new(),
406        };
407
408        builder.add_spend(&input_result);
409
410        let input_result = InputBuilderResult {
411            input: TransactionInput::new(TransactionHash::from([0; 32]), 0),
412            utxo_info: AlonzoFormatTxOut::new(address, Value::zero()).into(),
413            aggregate_witness: Some(data),
414            required_wits: RequiredWitnessSet::new(),
415        };
416
417        builder.add_spend(&input_result);
418
419        builder.update_ex_units(
420            RedeemerWitnessKey::new(RedeemerTag::Spend, 0),
421            ExUnits::new(10, 10),
422        );
423
424        let redeemers = builder.build(false).unwrap().to_flat_format();
425
426        assert_eq!(redeemers.len(), 1);
427
428        let spend_redeemer = &redeemers[0];
429
430        assert_eq!(spend_redeemer.tag, RedeemerTag::Spend);
431        assert_eq!(spend_redeemer.index, 0);
432    }
433}