use std::{collections::HashMap, ops::Deref}; use pallas_codec::utils::KeyValuePairs; use pallas_crypto::hash::Hash; use pallas_primitives::{alonzo, babbage, conway}; use pallas_traverse as trv; use prost_types::FieldMask; use trv::OriginalHash; pub use utxorpc_spec::utxorpc::v1alpha as spec; use utxorpc_spec::utxorpc::v1alpha::cardano as u5c; mod certs; mod params; pub type TxHash = Hash<32>; pub type TxoIndex = u32; pub type TxoRef = (TxHash, TxoIndex); pub type Cbor = Vec; pub type EraCbor = (trv::Era, Cbor); pub type UtxoMap = HashMap; pub trait LedgerContext: Clone { fn get_utxos(&self, refs: &[TxoRef]) -> Option; } #[derive(Default, Clone)] pub struct Mapper { ledger: Option, _mask: FieldMask, } impl Mapper { pub fn new(ledger: C) -> Self { Self { ledger: Some(ledger), _mask: FieldMask { paths: vec![] }, } } /// Creates a clone of this mapper using a custom field mask pub fn masked(&self, mask: FieldMask) -> Self { Self { ledger: self.ledger.clone(), _mask: mask, } } } impl Mapper { pub fn map_purpose(&self, x: &conway::RedeemerTag) -> u5c::RedeemerPurpose { match x { conway::RedeemerTag::Spend => u5c::RedeemerPurpose::Spend, conway::RedeemerTag::Mint => u5c::RedeemerPurpose::Mint, conway::RedeemerTag::Cert => u5c::RedeemerPurpose::Cert, conway::RedeemerTag::Reward => u5c::RedeemerPurpose::Reward, conway::RedeemerTag::Vote => u5c::RedeemerPurpose::Vote, conway::RedeemerTag::Propose => u5c::RedeemerPurpose::Propose, } } pub fn map_redeemer(&self, x: &trv::MultiEraRedeemer) -> u5c::Redeemer { u5c::Redeemer { purpose: self.map_purpose(&x.tag()).into(), payload: self.map_plutus_datum(x.data()).into(), } } fn decode_resolved_utxo( &self, resolved: &Option, input: &trv::MultiEraInput, ) -> Option { let as_txref = (*input.hash(), input.index() as u32); resolved .as_ref() .and_then(|x| x.get(&as_txref)) .and_then(|(era, cbor)| { let o = trv::MultiEraOutput::decode(*era, cbor.as_slice()).ok()?; Some(self.map_tx_output(&o)) }) } pub fn map_tx_input( &self, input: &trv::MultiEraInput, tx: &trv::MultiEraTx, // lexicographical order of the input we're mapping order: u32, resolved: &Option, ) -> u5c::TxInput { u5c::TxInput { tx_hash: input.hash().to_vec().into(), output_index: input.index() as u32, as_output: self.decode_resolved_utxo(resolved, input), redeemer: tx.find_spend_redeemer(order).map(|x| self.map_redeemer(&x)), } } pub fn map_tx_reference_input( &self, input: &trv::MultiEraInput, resolved: &Option, ) -> u5c::TxInput { u5c::TxInput { tx_hash: input.hash().to_vec().into(), output_index: input.index() as u32, as_output: self.decode_resolved_utxo(resolved, input), redeemer: None, } } pub fn map_tx_collateral( &self, input: &trv::MultiEraInput, resolved: &Option, ) -> u5c::TxInput { u5c::TxInput { tx_hash: input.hash().to_vec().into(), output_index: input.index() as u32, as_output: self.decode_resolved_utxo(resolved, input), redeemer: None, } } pub fn map_tx_datum(&self, x: &trv::MultiEraOutput) -> u5c::Datum { u5c::Datum { hash: match x.datum() { Some(babbage::PseudoDatumOption::Data(x)) => x.original_hash().to_vec().into(), Some(babbage::PseudoDatumOption::Hash(x)) => x.to_vec().into(), _ => vec![].into(), }, payload: match x.datum() { Some(babbage::PseudoDatumOption::Data(x)) => self.map_plutus_datum(&x.0).into(), _ => None, }, original_cbor: match x.datum() { Some(babbage::PseudoDatumOption::Data(x)) => x.raw_cbor().to_vec().into(), _ => vec![].into(), }, } } pub fn map_tx_output(&self, x: &trv::MultiEraOutput) -> u5c::TxOutput { u5c::TxOutput { address: x.address().map(|a| a.to_vec()).unwrap_or_default().into(), coin: x.value().coin(), // TODO: this is wrong, we're crating a new item for each asset even if they share // the same policy id. We need to adjust Pallas' interface to make this mapping more // ergonomic. assets: x .value() .assets() .iter() .map(|x| self.map_policy_assets(x)) .collect(), datum: self.map_tx_datum(x).into(), script: match x.script_ref() { Some(conway::PseudoScript::NativeScript(x)) => u5c::Script { script: u5c::script::Script::Native(Self::map_native_script(&x)).into(), /* */ } .into(), Some(conway::PseudoScript::PlutusV1Script(x)) => u5c::Script { script: u5c::script::Script::PlutusV1(x.0.to_vec().into()).into(), } .into(), Some(conway::PseudoScript::PlutusV2Script(x)) => u5c::Script { script: u5c::script::Script::PlutusV2(x.0.to_vec().into()).into(), } .into(), Some(conway::PseudoScript::PlutusV3Script(x)) => u5c::Script { script: u5c::script::Script::PlutusV3(x.0.to_vec().into()).into(), } .into(), None => None, }, } } pub fn map_stake_credential(&self, x: &babbage::StakeCredential) -> u5c::StakeCredential { let inner = match x { babbage::StakeCredential::AddrKeyhash(x) => { u5c::stake_credential::StakeCredential::AddrKeyHash(x.to_vec().into()) } babbage::StakeCredential::ScriptHash(x) => { u5c::stake_credential::StakeCredential::ScriptHash(x.to_vec().into()) } }; u5c::StakeCredential { stake_credential: inner.into(), } } pub fn map_relay(&self, x: &alonzo::Relay) -> u5c::Relay { match x { babbage::Relay::SingleHostAddr(port, v4, v6) => u5c::Relay { // ip_v4: v4.map(|x| x.to_vec().into()).into().unwrap_or_default(), ip_v4: Option::from(v4.clone().map(|x| x.to_vec().into())).unwrap_or_default(), ip_v6: Option::from(v6.clone().map(|x| x.to_vec().into())).unwrap_or_default(), dns_name: String::default(), port: Option::from(port.clone()).unwrap_or_default(), }, babbage::Relay::SingleHostName(port, name) => u5c::Relay { ip_v4: Default::default(), ip_v6: Default::default(), dns_name: name.clone(), port: Option::from(port.clone()).unwrap_or_default(), }, babbage::Relay::MultiHostName(name) => u5c::Relay { ip_v4: Default::default(), ip_v6: Default::default(), dns_name: name.clone(), port: Default::default(), }, } } pub fn map_withdrawals( &self, x: &(&[u8], u64), tx: &trv::MultiEraTx, order: u32, ) -> u5c::Withdrawal { u5c::Withdrawal { reward_account: Vec::from(x.0).into(), coin: x.1, redeemer: tx .find_withdrawal_redeemer(order) .map(|x| self.map_redeemer(&x)), } } pub fn map_asset(&self, x: &trv::MultiEraAsset) -> u5c::Asset { u5c::Asset { name: x.name().to_vec().into(), output_coin: x.output_coin().unwrap_or_default(), mint_coin: x.mint_coin().unwrap_or_default(), } } pub fn map_policy_assets(&self, x: &trv::MultiEraPolicyAssets) -> u5c::Multiasset { u5c::Multiasset { policy_id: x.policy().to_vec().into(), assets: x.assets().iter().map(|x| self.map_asset(x)).collect(), redeemer: None, } } pub fn map_vkey_witness(&self, x: &alonzo::VKeyWitness) -> u5c::VKeyWitness { u5c::VKeyWitness { vkey: x.vkey.to_vec().into(), signature: x.signature.to_vec().into(), } } pub fn map_native_script(x: &alonzo::NativeScript) -> u5c::NativeScript { let inner = match x { babbage::NativeScript::ScriptPubkey(x) => { u5c::native_script::NativeScript::ScriptPubkey(x.to_vec().into()) } babbage::NativeScript::ScriptAll(x) => { u5c::native_script::NativeScript::ScriptAll(u5c::NativeScriptList { items: x.iter().map(|x| Self::map_native_script(x)).collect(), }) } babbage::NativeScript::ScriptAny(x) => { u5c::native_script::NativeScript::ScriptAll(u5c::NativeScriptList { items: x.iter().map(|x| Self::map_native_script(x)).collect(), }) } babbage::NativeScript::ScriptNOfK(n, k) => { u5c::native_script::NativeScript::ScriptNOfK(u5c::ScriptNOfK { k: *n, scripts: k.iter().map(|x| Self::map_native_script(x)).collect(), }) } babbage::NativeScript::InvalidBefore(s) => { u5c::native_script::NativeScript::InvalidBefore(*s) } babbage::NativeScript::InvalidHereafter(s) => { u5c::native_script::NativeScript::InvalidHereafter(*s) } }; u5c::NativeScript { native_script: inner.into(), } } fn collect_all_scripts(&self, tx: &trv::MultiEraTx) -> Vec { let ns = tx .native_scripts() .iter() .map(|x| Self::map_native_script(x.deref())) .map(|x| u5c::Script { script: u5c::script::Script::Native(x).into(), }); let p1 = tx .plutus_v1_scripts() .iter() .map(|x| x.0.to_vec().into()) .map(|x| u5c::Script { script: u5c::script::Script::PlutusV1(x).into(), }); let p2 = tx .plutus_v2_scripts() .iter() .map(|x| x.0.to_vec().into()) .map(|x| u5c::Script { script: u5c::script::Script::PlutusV2(x).into(), }); ns.chain(p1).chain(p2).collect() } pub fn map_plutus_constr(&self, x: &alonzo::Constr) -> u5c::Constr { u5c::Constr { tag: x.tag as u32, any_constructor: x.any_constructor.unwrap_or_default(), fields: x.fields.iter().map(|x| self.map_plutus_datum(x)).collect(), } } pub fn map_plutus_map( &self, x: &KeyValuePairs, ) -> u5c::PlutusDataMap { u5c::PlutusDataMap { pairs: x .iter() .map(|(k, v)| u5c::PlutusDataPair { key: self.map_plutus_datum(k).into(), value: self.map_plutus_datum(v).into(), }) .collect(), } } pub fn map_plutus_array(&self, x: &[alonzo::PlutusData]) -> u5c::PlutusDataArray { u5c::PlutusDataArray { items: x.iter().map(|x| self.map_plutus_datum(x)).collect(), } } pub fn map_plutus_bigint(&self, x: &alonzo::BigInt) -> u5c::BigInt { let inner = match x { babbage::BigInt::Int(x) => u5c::big_int::BigInt::Int(i128::from(x.0) as i64), babbage::BigInt::BigUInt(x) => { u5c::big_int::BigInt::BigUInt(Vec::::from(x.clone()).into()) } babbage::BigInt::BigNInt(x) => { u5c::big_int::BigInt::BigNInt(Vec::::from(x.clone()).into()) } }; u5c::BigInt { big_int: inner.into(), } } pub fn map_plutus_datum(&self, x: &alonzo::PlutusData) -> u5c::PlutusData { let inner = match x { babbage::PlutusData::Constr(x) => { u5c::plutus_data::PlutusData::Constr(self.map_plutus_constr(x)) } babbage::PlutusData::Map(x) => { u5c::plutus_data::PlutusData::Map(self.map_plutus_map(x)) } babbage::PlutusData::Array(x) => { u5c::plutus_data::PlutusData::Array(self.map_plutus_array(x)) } babbage::PlutusData::BigInt(x) => { u5c::plutus_data::PlutusData::BigInt(self.map_plutus_bigint(x)) } babbage::PlutusData::BoundedBytes(x) => { u5c::plutus_data::PlutusData::BoundedBytes(x.to_vec().into()) } }; u5c::PlutusData { plutus_data: inner.into(), } } pub fn map_metadatum(x: &alonzo::Metadatum) -> u5c::Metadatum { let inner = match x { babbage::Metadatum::Int(x) => u5c::metadatum::Metadatum::Int(i128::from(x.0) as i64), babbage::Metadatum::Bytes(x) => { u5c::metadatum::Metadatum::Bytes(Vec::::from(x.clone()).into()) } babbage::Metadatum::Text(x) => u5c::metadatum::Metadatum::Text(x.clone()), babbage::Metadatum::Array(x) => u5c::metadatum::Metadatum::Array(u5c::MetadatumArray { items: x.iter().map(|x| Self::map_metadatum(x)).collect(), }), babbage::Metadatum::Map(x) => u5c::metadatum::Metadatum::Map(u5c::MetadatumMap { pairs: x .iter() .map(|(k, v)| u5c::MetadatumPair { key: Self::map_metadatum(k).into(), value: Self::map_metadatum(v).into(), }) .collect(), }), }; u5c::Metadatum { metadatum: inner.into(), } } pub fn map_metadata(&self, label: u64, datum: &alonzo::Metadatum) -> u5c::Metadata { u5c::Metadata { label, value: Self::map_metadatum(datum).into(), } } fn collect_all_aux_scripts(&self, tx: &trv::MultiEraTx) -> Vec { let ns = tx .aux_native_scripts() .iter() .map(|x| Self::map_native_script(x)) .map(|x| u5c::Script { script: u5c::script::Script::Native(x).into(), }); let p1 = tx .aux_plutus_v1_scripts() .iter() .map(|x| x.0.to_vec().into()) .map(|x| u5c::Script { script: u5c::script::Script::PlutusV1(x).into(), }); // TODO: check why we don't have plutus v2 aux script, is that a possibility? ns.chain(p1).collect() } fn find_related_inputs(&self, tx: &trv::MultiEraTx) -> Vec { let inputs = tx .inputs() .into_iter() .map(|x| (*x.hash(), x.index() as u32)); let collateral = tx .collateral() .into_iter() .map(|x| (*x.hash(), x.index() as u32)); let reference_inputs = tx .reference_inputs() .into_iter() .map(|x| (*x.hash(), x.index() as u32)); inputs.chain(collateral).chain(reference_inputs).collect() } pub fn map_tx(&self, tx: &trv::MultiEraTx) -> u5c::Tx { let resolved = self.ledger.as_ref().and_then(|ctx| { let to_resolve = self.find_related_inputs(tx); ctx.get_utxos(to_resolve.as_slice()) }); u5c::Tx { hash: tx.hash().to_vec().into(), inputs: tx .inputs_sorted_set() .iter() .enumerate() .map(|(order, i)| self.map_tx_input(i, tx, order as u32, &resolved)) .collect(), outputs: tx.outputs().iter().map(|x| self.map_tx_output(x)).collect(), certificates: tx .certs() .iter() .enumerate() .filter_map(|(order, x)| self.map_cert(x, tx, order as u32)) .collect(), withdrawals: tx .withdrawals_sorted_set() .iter() .enumerate() .map(|(order, x)| self.map_withdrawals(x, tx, order as u32)) .collect(), mint: tx .mints_sorted_set() .iter() .enumerate() .map(|(order, x)| { let mut ma = self.map_policy_assets(x); ma.redeemer = tx .find_mint_redeemer(order as u32) .map(|r| self.map_redeemer(&r)); ma }) .collect(), reference_inputs: tx .reference_inputs() .iter() .map(|x| self.map_tx_reference_input(x, &resolved)) .collect(), witnesses: u5c::WitnessSet { vkeywitness: tx .vkey_witnesses() .iter() .map(|x| self.map_vkey_witness(x)) .collect(), script: self.collect_all_scripts(tx), plutus_datums: tx .plutus_data() .iter() .map(|x| self.map_plutus_datum(x.deref())) .collect(), } .into(), collateral: u5c::Collateral { collateral: tx .collateral() .iter() .map(|x| self.map_tx_collateral(x, &resolved)) .collect(), collateral_return: tx.collateral_return().map(|x| self.map_tx_output(&x)), total_collateral: tx.total_collateral().unwrap_or_default(), } .into(), fee: tx.fee().unwrap_or_default(), validity: u5c::TxValidity { start: tx.validity_start().unwrap_or_default(), ttl: tx.ttl().unwrap_or_default(), } .into(), successful: tx.is_valid(), auxiliary: u5c::AuxData { metadata: tx .metadata() .collect::>() .into_iter() .map(|(l, d)| self.map_metadata(l, d)) .collect(), scripts: self.collect_all_aux_scripts(tx), } .into(), } } pub fn map_block(&self, block: &trv::MultiEraBlock) -> u5c::Block { u5c::Block { header: u5c::BlockHeader { slot: block.slot(), hash: block.hash().to_vec().into(), height: block.number(), } .into(), body: u5c::BlockBody { tx: block.txs().iter().map(|x| self.map_tx(x)).collect(), } .into(), } } pub fn map_block_cbor(&self, raw: &[u8]) -> u5c::Block { let block = trv::MultiEraBlock::decode(raw).unwrap(); self.map_block(&block) } } #[cfg(test)] mod tests { use super::*; use pretty_assertions::assert_eq; #[derive(Clone)] struct NoLedger; impl LedgerContext for NoLedger { fn get_utxos(&self, _refs: &[TxoRef]) -> Option { None } } #[test] fn snapshot() { let test_blocks = [include_str!("../../test_data/u5c1.block")]; let test_snapshots = [include_str!("../../test_data/u5c1.json")]; let mapper = Mapper::new(NoLedger); for (block_str, json_str) in test_blocks.iter().zip(test_snapshots) { let cbor = hex::decode(block_str).unwrap(); let block = pallas_traverse::MultiEraBlock::decode(&cbor).unwrap(); let current = serde_json::json!(mapper.map_block(&block)); // un-comment the following to generate a new snapshot // std::fs::write( // "new_snapshot.json", // serde_json::to_string_pretty(¤t).unwrap(), // ) // .unwrap(); let expected: serde_json::Value = serde_json::from_str(&json_str).unwrap(); assert_eq!(expected, current) } } }