Skip to main content

pallas_utxorpc/
lib.rs

1use std::{collections::HashMap, ops::Deref};
2
3use pallas_codec::utils::KeyValuePairs;
4use pallas_crypto::hash::Hash;
5use pallas_primitives::{alonzo, babbage, conway};
6use pallas_traverse as trv;
7
8use prost_types::FieldMask;
9use trv::OriginalHash;
10
11pub use utxorpc_spec::utxorpc::v1alpha as spec;
12
13use utxorpc_spec::utxorpc::v1alpha::cardano as u5c;
14
15mod certs;
16mod params;
17
18pub type TxHash = Hash<32>;
19pub type TxoIndex = u32;
20pub type TxoRef = (TxHash, TxoIndex);
21pub type Cbor = Vec<u8>;
22pub type EraCbor = (trv::Era, Cbor);
23pub type UtxoMap = HashMap<TxoRef, EraCbor>;
24
25pub trait LedgerContext: Clone {
26    fn get_utxos(&self, refs: &[TxoRef]) -> Option<UtxoMap>;
27}
28
29#[derive(Default, Clone)]
30pub struct Mapper<C: LedgerContext> {
31    ledger: Option<C>,
32    _mask: FieldMask,
33}
34
35impl<C: LedgerContext> Mapper<C> {
36    pub fn new(ledger: C) -> Self {
37        Self {
38            ledger: Some(ledger),
39            _mask: FieldMask { paths: vec![] },
40        }
41    }
42
43    /// Creates a clone of this mapper using a custom field mask
44    pub fn masked(&self, mask: FieldMask) -> Self {
45        Self {
46            ledger: self.ledger.clone(),
47            _mask: mask,
48        }
49    }
50}
51
52impl<C: LedgerContext> Mapper<C> {
53    pub fn map_purpose(&self, x: &conway::RedeemerTag) -> u5c::RedeemerPurpose {
54        match x {
55            conway::RedeemerTag::Spend => u5c::RedeemerPurpose::Spend,
56            conway::RedeemerTag::Mint => u5c::RedeemerPurpose::Mint,
57            conway::RedeemerTag::Cert => u5c::RedeemerPurpose::Cert,
58            conway::RedeemerTag::Reward => u5c::RedeemerPurpose::Reward,
59            conway::RedeemerTag::Vote => u5c::RedeemerPurpose::Vote,
60            conway::RedeemerTag::Propose => u5c::RedeemerPurpose::Propose,
61        }
62    }
63
64    pub fn map_redeemer(&self, x: &trv::MultiEraRedeemer) -> u5c::Redeemer {
65        u5c::Redeemer {
66            purpose: self.map_purpose(&x.tag()).into(),
67            payload: self.map_plutus_datum(x.data()).into(),
68            index: x.index(),
69            ex_units: Some(u5c::ExUnits {
70                steps: x.ex_units().steps,
71                memory: x.ex_units().mem,
72            }),
73            original_cbor: x.encode().into(),
74        }
75    }
76
77    fn decode_resolved_utxo(
78        &self,
79        resolved: &Option<UtxoMap>,
80        input: &trv::MultiEraInput,
81    ) -> Option<u5c::TxOutput> {
82        let as_txref = (*input.hash(), input.index() as u32);
83
84        resolved
85            .as_ref()
86            .and_then(|x| x.get(&as_txref))
87            .and_then(|(era, cbor)| {
88                let o = trv::MultiEraOutput::decode(*era, cbor.as_slice()).ok()?;
89                Some(self.map_tx_output(&o))
90            })
91    }
92
93    pub fn map_tx_input(
94        &self,
95        input: &trv::MultiEraInput,
96        tx: &trv::MultiEraTx,
97        // lexicographical order of the input we're mapping
98        order: u32,
99        resolved: &Option<UtxoMap>,
100    ) -> u5c::TxInput {
101        u5c::TxInput {
102            tx_hash: input.hash().to_vec().into(),
103            output_index: input.index() as u32,
104            as_output: self.decode_resolved_utxo(resolved, input),
105            redeemer: tx.find_spend_redeemer(order).map(|x| self.map_redeemer(&x)),
106        }
107    }
108
109    pub fn map_tx_reference_input(
110        &self,
111        input: &trv::MultiEraInput,
112        resolved: &Option<UtxoMap>,
113    ) -> u5c::TxInput {
114        u5c::TxInput {
115            tx_hash: input.hash().to_vec().into(),
116            output_index: input.index() as u32,
117            as_output: self.decode_resolved_utxo(resolved, input),
118            redeemer: None,
119        }
120    }
121
122    pub fn map_tx_collateral(
123        &self,
124        input: &trv::MultiEraInput,
125        resolved: &Option<UtxoMap>,
126    ) -> u5c::TxInput {
127        u5c::TxInput {
128            tx_hash: input.hash().to_vec().into(),
129            output_index: input.index() as u32,
130            as_output: self.decode_resolved_utxo(resolved, input),
131            redeemer: None,
132        }
133    }
134
135    pub fn map_tx_datum(&self, x: &trv::MultiEraOutput) -> u5c::Datum {
136        u5c::Datum {
137            hash: match x.datum() {
138                Some(babbage::PseudoDatumOption::Data(x)) => x.original_hash().to_vec().into(),
139                Some(babbage::PseudoDatumOption::Hash(x)) => x.to_vec().into(),
140                _ => vec![].into(),
141            },
142            payload: match x.datum() {
143                Some(babbage::PseudoDatumOption::Data(x)) => self.map_plutus_datum(&x.0).into(),
144                _ => None,
145            },
146            original_cbor: match x.datum() {
147                Some(babbage::PseudoDatumOption::Data(x)) => x.raw_cbor().to_vec().into(),
148                _ => vec![].into(),
149            },
150        }
151    }
152
153    pub fn map_tx_output(&self, x: &trv::MultiEraOutput) -> u5c::TxOutput {
154        u5c::TxOutput {
155            address: x.address().map(|a| a.to_vec()).unwrap_or_default().into(),
156            coin: x.value().coin(),
157            // TODO: this is wrong, we're crating a new item for each asset even if they share
158            // the same policy id. We need to adjust Pallas' interface to make this mapping more
159            // ergonomic.
160            assets: x
161                .value()
162                .assets()
163                .iter()
164                .map(|x| self.map_policy_assets(x))
165                .collect(),
166            datum: self.map_tx_datum(x).into(),
167            script: match x.script_ref() {
168                Some(conway::PseudoScript::NativeScript(x)) => u5c::Script {
169                    script: u5c::script::Script::Native(Self::map_native_script(&x)).into(), /*  */
170                }
171                .into(),
172                Some(conway::PseudoScript::PlutusV1Script(x)) => u5c::Script {
173                    script: u5c::script::Script::PlutusV1(x.0.to_vec().into()).into(),
174                }
175                .into(),
176                Some(conway::PseudoScript::PlutusV2Script(x)) => u5c::Script {
177                    script: u5c::script::Script::PlutusV2(x.0.to_vec().into()).into(),
178                }
179                .into(),
180                Some(conway::PseudoScript::PlutusV3Script(x)) => u5c::Script {
181                    script: u5c::script::Script::PlutusV3(x.0.to_vec().into()).into(),
182                }
183                .into(),
184                None => None,
185            },
186        }
187    }
188
189    pub fn map_stake_credential(&self, x: &babbage::StakeCredential) -> u5c::StakeCredential {
190        let inner = match x {
191            babbage::StakeCredential::AddrKeyhash(x) => {
192                u5c::stake_credential::StakeCredential::AddrKeyHash(x.to_vec().into())
193            }
194            babbage::StakeCredential::ScriptHash(x) => {
195                u5c::stake_credential::StakeCredential::ScriptHash(x.to_vec().into())
196            }
197        };
198
199        u5c::StakeCredential {
200            stake_credential: inner.into(),
201        }
202    }
203
204    pub fn map_relay(&self, x: &alonzo::Relay) -> u5c::Relay {
205        match x {
206            babbage::Relay::SingleHostAddr(port, v4, v6) => u5c::Relay {
207                // ip_v4: v4.map(|x| x.to_vec().into()).into().unwrap_or_default(),
208                ip_v4: Option::from(v4.clone().map(|x| x.to_vec().into())).unwrap_or_default(),
209                ip_v6: Option::from(v6.clone().map(|x| x.to_vec().into())).unwrap_or_default(),
210                dns_name: String::default(),
211                port: Option::from(port.clone()).unwrap_or_default(),
212            },
213            babbage::Relay::SingleHostName(port, name) => u5c::Relay {
214                ip_v4: Default::default(),
215                ip_v6: Default::default(),
216                dns_name: name.clone(),
217                port: Option::from(port.clone()).unwrap_or_default(),
218            },
219            babbage::Relay::MultiHostName(name) => u5c::Relay {
220                ip_v4: Default::default(),
221                ip_v6: Default::default(),
222                dns_name: name.clone(),
223                port: Default::default(),
224            },
225        }
226    }
227
228    pub fn map_withdrawals(
229        &self,
230        x: &(&[u8], u64),
231        tx: &trv::MultiEraTx,
232        order: u32,
233    ) -> u5c::Withdrawal {
234        u5c::Withdrawal {
235            reward_account: Vec::from(x.0).into(),
236            coin: x.1,
237            redeemer: tx
238                .find_withdrawal_redeemer(order)
239                .map(|x| self.map_redeemer(&x)),
240        }
241    }
242
243    pub fn map_asset(&self, x: &trv::MultiEraAsset) -> u5c::Asset {
244        u5c::Asset {
245            name: x.name().to_vec().into(),
246            output_coin: x.output_coin().unwrap_or_default(),
247            mint_coin: x.mint_coin().unwrap_or_default(),
248        }
249    }
250
251    pub fn map_policy_assets(&self, x: &trv::MultiEraPolicyAssets) -> u5c::Multiasset {
252        u5c::Multiasset {
253            policy_id: x.policy().to_vec().into(),
254            assets: x.assets().iter().map(|x| self.map_asset(x)).collect(),
255            redeemer: None,
256        }
257    }
258
259    pub fn map_vkey_witness(&self, x: &alonzo::VKeyWitness) -> u5c::VKeyWitness {
260        u5c::VKeyWitness {
261            vkey: x.vkey.to_vec().into(),
262            signature: x.signature.to_vec().into(),
263        }
264    }
265
266    pub fn map_native_script(x: &alonzo::NativeScript) -> u5c::NativeScript {
267        let inner = match x {
268            babbage::NativeScript::ScriptPubkey(x) => {
269                u5c::native_script::NativeScript::ScriptPubkey(x.to_vec().into())
270            }
271            babbage::NativeScript::ScriptAll(x) => {
272                u5c::native_script::NativeScript::ScriptAll(u5c::NativeScriptList {
273                    items: x.iter().map(|x| Self::map_native_script(x)).collect(),
274                })
275            }
276            babbage::NativeScript::ScriptAny(x) => {
277                u5c::native_script::NativeScript::ScriptAll(u5c::NativeScriptList {
278                    items: x.iter().map(|x| Self::map_native_script(x)).collect(),
279                })
280            }
281            babbage::NativeScript::ScriptNOfK(n, k) => {
282                u5c::native_script::NativeScript::ScriptNOfK(u5c::ScriptNOfK {
283                    k: *n,
284                    scripts: k.iter().map(|x| Self::map_native_script(x)).collect(),
285                })
286            }
287            babbage::NativeScript::InvalidBefore(s) => {
288                u5c::native_script::NativeScript::InvalidBefore(*s)
289            }
290            babbage::NativeScript::InvalidHereafter(s) => {
291                u5c::native_script::NativeScript::InvalidHereafter(*s)
292            }
293        };
294
295        u5c::NativeScript {
296            native_script: inner.into(),
297        }
298    }
299
300    fn collect_all_scripts(&self, tx: &trv::MultiEraTx) -> Vec<u5c::Script> {
301        let ns = tx
302            .native_scripts()
303            .iter()
304            .map(|x| Self::map_native_script(x.deref()))
305            .map(|x| u5c::Script {
306                script: u5c::script::Script::Native(x).into(),
307            });
308
309        let p1 = tx
310            .plutus_v1_scripts()
311            .iter()
312            .map(|x| x.0.to_vec().into())
313            .map(|x| u5c::Script {
314                script: u5c::script::Script::PlutusV1(x).into(),
315            });
316
317        let p2 = tx
318            .plutus_v2_scripts()
319            .iter()
320            .map(|x| x.0.to_vec().into())
321            .map(|x| u5c::Script {
322                script: u5c::script::Script::PlutusV2(x).into(),
323            });
324
325        ns.chain(p1).chain(p2).collect()
326    }
327
328    pub fn map_plutus_constr(&self, x: &alonzo::Constr<alonzo::PlutusData>) -> u5c::Constr {
329        u5c::Constr {
330            tag: x.tag as u32,
331            any_constructor: x.any_constructor.unwrap_or_default(),
332            fields: x.fields.iter().map(|x| self.map_plutus_datum(x)).collect(),
333        }
334    }
335
336    pub fn map_plutus_map(
337        &self,
338        x: &KeyValuePairs<alonzo::PlutusData, alonzo::PlutusData>,
339    ) -> u5c::PlutusDataMap {
340        u5c::PlutusDataMap {
341            pairs: x
342                .iter()
343                .map(|(k, v)| u5c::PlutusDataPair {
344                    key: self.map_plutus_datum(k).into(),
345                    value: self.map_plutus_datum(v).into(),
346                })
347                .collect(),
348        }
349    }
350
351    pub fn map_plutus_array(&self, x: &[alonzo::PlutusData]) -> u5c::PlutusDataArray {
352        u5c::PlutusDataArray {
353            items: x.iter().map(|x| self.map_plutus_datum(x)).collect(),
354        }
355    }
356
357    pub fn map_plutus_bigint(&self, x: &alonzo::BigInt) -> u5c::BigInt {
358        let inner = match x {
359            babbage::BigInt::Int(x) => u5c::big_int::BigInt::Int(i128::from(x.0) as i64),
360            babbage::BigInt::BigUInt(x) => {
361                u5c::big_int::BigInt::BigUInt(Vec::<u8>::from(x.clone()).into())
362            }
363            babbage::BigInt::BigNInt(x) => {
364                u5c::big_int::BigInt::BigNInt(Vec::<u8>::from(x.clone()).into())
365            }
366        };
367
368        u5c::BigInt {
369            big_int: inner.into(),
370        }
371    }
372
373    pub fn map_plutus_datum(&self, x: &alonzo::PlutusData) -> u5c::PlutusData {
374        let inner = match x {
375            babbage::PlutusData::Constr(x) => {
376                u5c::plutus_data::PlutusData::Constr(self.map_plutus_constr(x))
377            }
378            babbage::PlutusData::Map(x) => {
379                u5c::plutus_data::PlutusData::Map(self.map_plutus_map(x))
380            }
381            babbage::PlutusData::Array(x) => {
382                u5c::plutus_data::PlutusData::Array(self.map_plutus_array(x))
383            }
384            babbage::PlutusData::BigInt(x) => {
385                u5c::plutus_data::PlutusData::BigInt(self.map_plutus_bigint(x))
386            }
387            babbage::PlutusData::BoundedBytes(x) => {
388                u5c::plutus_data::PlutusData::BoundedBytes(x.to_vec().into())
389            }
390        };
391
392        u5c::PlutusData {
393            plutus_data: inner.into(),
394        }
395    }
396
397    pub fn map_metadatum(x: &alonzo::Metadatum) -> u5c::Metadatum {
398        let inner = match x {
399            babbage::Metadatum::Int(x) => u5c::metadatum::Metadatum::Int(i128::from(x.0) as i64),
400            babbage::Metadatum::Bytes(x) => {
401                u5c::metadatum::Metadatum::Bytes(Vec::<u8>::from(x.clone()).into())
402            }
403            babbage::Metadatum::Text(x) => u5c::metadatum::Metadatum::Text(x.clone()),
404            babbage::Metadatum::Array(x) => u5c::metadatum::Metadatum::Array(u5c::MetadatumArray {
405                items: x.iter().map(|x| Self::map_metadatum(x)).collect(),
406            }),
407            babbage::Metadatum::Map(x) => u5c::metadatum::Metadatum::Map(u5c::MetadatumMap {
408                pairs: x
409                    .iter()
410                    .map(|(k, v)| u5c::MetadatumPair {
411                        key: Self::map_metadatum(k).into(),
412                        value: Self::map_metadatum(v).into(),
413                    })
414                    .collect(),
415            }),
416        };
417
418        u5c::Metadatum {
419            metadatum: inner.into(),
420        }
421    }
422
423    pub fn map_metadata(&self, label: u64, datum: &alonzo::Metadatum) -> u5c::Metadata {
424        u5c::Metadata {
425            label,
426            value: Self::map_metadatum(datum).into(),
427        }
428    }
429
430    fn collect_all_aux_scripts(&self, tx: &trv::MultiEraTx) -> Vec<u5c::Script> {
431        let ns = tx
432            .aux_native_scripts()
433            .iter()
434            .map(|x| Self::map_native_script(x))
435            .map(|x| u5c::Script {
436                script: u5c::script::Script::Native(x).into(),
437            });
438
439        let p1 = tx
440            .aux_plutus_v1_scripts()
441            .iter()
442            .map(|x| x.0.to_vec().into())
443            .map(|x| u5c::Script {
444                script: u5c::script::Script::PlutusV1(x).into(),
445            });
446
447        // TODO: check why we don't have plutus v2 aux script, is that a possibility?
448
449        ns.chain(p1).collect()
450    }
451
452    fn find_related_inputs(&self, tx: &trv::MultiEraTx) -> Vec<TxoRef> {
453        let inputs = tx
454            .inputs()
455            .into_iter()
456            .map(|x| (*x.hash(), x.index() as u32));
457
458        let collateral = tx
459            .collateral()
460            .into_iter()
461            .map(|x| (*x.hash(), x.index() as u32));
462
463        let reference_inputs = tx
464            .reference_inputs()
465            .into_iter()
466            .map(|x| (*x.hash(), x.index() as u32));
467
468        inputs.chain(collateral).chain(reference_inputs).collect()
469    }
470
471    pub fn map_tx(&self, tx: &trv::MultiEraTx) -> u5c::Tx {
472        let resolved = self.ledger.as_ref().and_then(|ctx| {
473            let to_resolve = self.find_related_inputs(tx);
474            ctx.get_utxos(to_resolve.as_slice())
475        });
476
477        u5c::Tx {
478            hash: tx.hash().to_vec().into(),
479            inputs: tx
480                .inputs_sorted_set()
481                .iter()
482                .enumerate()
483                .map(|(order, i)| self.map_tx_input(i, tx, order as u32, &resolved))
484                .collect(),
485            outputs: tx.outputs().iter().map(|x| self.map_tx_output(x)).collect(),
486            certificates: tx
487                .certs()
488                .iter()
489                .enumerate()
490                .filter_map(|(order, x)| self.map_cert(x, tx, order as u32))
491                .collect(),
492            withdrawals: tx
493                .withdrawals_sorted_set()
494                .iter()
495                .enumerate()
496                .map(|(order, x)| self.map_withdrawals(x, tx, order as u32))
497                .collect(),
498            mint: tx
499                .mints_sorted_set()
500                .iter()
501                .enumerate()
502                .map(|(order, x)| {
503                    let mut ma = self.map_policy_assets(x);
504
505                    ma.redeemer = tx
506                        .find_mint_redeemer(order as u32)
507                        .map(|r| self.map_redeemer(&r));
508
509                    ma
510                })
511                .collect(),
512            reference_inputs: tx
513                .reference_inputs()
514                .iter()
515                .map(|x| self.map_tx_reference_input(x, &resolved))
516                .collect(),
517            witnesses: u5c::WitnessSet {
518                vkeywitness: tx
519                    .vkey_witnesses()
520                    .iter()
521                    .map(|x| self.map_vkey_witness(x))
522                    .collect(),
523                script: self.collect_all_scripts(tx),
524                plutus_datums: tx
525                    .plutus_data()
526                    .iter()
527                    .map(|x| self.map_plutus_datum(x.deref()))
528                    .collect(),
529            }
530            .into(),
531            collateral: u5c::Collateral {
532                collateral: tx
533                    .collateral()
534                    .iter()
535                    .map(|x| self.map_tx_collateral(x, &resolved))
536                    .collect(),
537                collateral_return: tx.collateral_return().map(|x| self.map_tx_output(&x)),
538                total_collateral: tx.total_collateral().unwrap_or_default(),
539            }
540            .into(),
541            fee: tx.fee().unwrap_or_default(),
542            validity: u5c::TxValidity {
543                start: tx.validity_start().unwrap_or_default(),
544                ttl: tx.ttl().unwrap_or_default(),
545            }
546            .into(),
547            successful: tx.is_valid(),
548            auxiliary: u5c::AuxData {
549                metadata: tx
550                    .metadata()
551                    .collect::<Vec<_>>()
552                    .into_iter()
553                    .map(|(l, d)| self.map_metadata(l, d))
554                    .collect(),
555                scripts: self.collect_all_aux_scripts(tx),
556            }
557            .into(),
558        }
559    }
560
561    pub fn map_block(&self, block: &trv::MultiEraBlock) -> u5c::Block {
562        u5c::Block {
563            header: u5c::BlockHeader {
564                slot: block.slot(),
565                hash: block.hash().to_vec().into(),
566                height: block.number(),
567            }
568            .into(),
569            body: u5c::BlockBody {
570                tx: block.txs().iter().map(|x| self.map_tx(x)).collect(),
571            }
572            .into(),
573        }
574    }
575
576    pub fn map_block_cbor(&self, raw: &[u8]) -> u5c::Block {
577        let block = trv::MultiEraBlock::decode(raw).unwrap();
578        self.map_block(&block)
579    }
580}
581
582#[cfg(test)]
583mod tests {
584    use super::*;
585    use pretty_assertions::assert_eq;
586
587    #[derive(Clone)]
588    struct NoLedger;
589
590    impl LedgerContext for NoLedger {
591        fn get_utxos(&self, _refs: &[TxoRef]) -> Option<UtxoMap> {
592            None
593        }
594    }
595
596    #[test]
597    fn snapshot() {
598        let test_blocks = [include_str!("../../test_data/u5c1.block")];
599        let test_snapshots = [include_str!("../../test_data/u5c1.json")];
600
601        let mapper = Mapper::new(NoLedger);
602
603        for (block_str, json_str) in test_blocks.iter().zip(test_snapshots) {
604            let cbor = hex::decode(block_str).unwrap();
605            let block = pallas_traverse::MultiEraBlock::decode(&cbor).unwrap();
606            let current = serde_json::json!(mapper.map_block(&block));
607
608            // un-comment the following to generate a new snapshot
609
610            // std::fs::write(
611            //     "new_snapshot.json",
612            //     serde_json::to_string_pretty(&current).unwrap(),
613            // )
614            // .unwrap();
615
616            let expected: serde_json::Value = serde_json::from_str(json_str).unwrap();
617
618            assert_eq!(expected, current)
619        }
620    }
621}