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 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 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 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(), }
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: 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 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 let expected: serde_json::Value = serde_json::from_str(json_str).unwrap();
617
618 assert_eq!(expected, current)
619 }
620 }
621}