diff --git a/Cargo.toml b/Cargo.toml index 063ff1e..537f5d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "pallas-crypto", "pallas-primitives", "pallas-traverse", + "pallas-utxorpc", "pallas", "examples/block-download", "examples/block-decode", diff --git a/pallas-traverse/src/assets.rs b/pallas-traverse/src/assets.rs index 37df086..ecfc097 100644 --- a/pallas-traverse/src/assets.rs +++ b/pallas-traverse/src/assets.rs @@ -81,7 +81,21 @@ impl<'b> MultiEraAsset<'b> { } } - pub fn coin(&self) -> i128 { + pub fn mint_coin(&self) -> Option { + match self { + MultiEraAsset::AlonzoCompatibleMint(_, _, x) => Some(*x), + MultiEraAsset::AlonzoCompatibleOutput(_, _, _) => None, + } + } + + pub fn output_coin(&self) -> Option { + match self { + MultiEraAsset::AlonzoCompatibleOutput(_, _, x) => Some(*x), + MultiEraAsset::AlonzoCompatibleMint(_, _, _) => None, + } + } + + pub fn any_coin(&self) -> i128 { match self { MultiEraAsset::AlonzoCompatibleOutput(_, _, x) => *x as i128, MultiEraAsset::AlonzoCompatibleMint(_, _, x) => *x as i128, diff --git a/pallas-utxorpc/Cargo.toml b/pallas-utxorpc/Cargo.toml new file mode 100644 index 0000000..c5e7b80 --- /dev/null +++ b/pallas-utxorpc/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "pallas-utxorpc" +description = "Pallas interoperability with the UTxORPC spec" +version = "0.19.0-alpha.0" +edition = "2021" +repository = "https://github.com/txpipe/pallas" +homepage = "https://github.com/txpipe/pallas" +documentation = "https://docs.rs/pallas-utxorpc" +license = "Apache-2.0" +readme = "README.md" +authors = ["Santiago Carmuega "] + +[dependencies] +#utxorpc = { path = "../../../utxorpc/rust-sdk" } +utxorpc = { version = "1.0.0-alpha.1" } +pallas-traverse = { version = "0.19.0-alpha.0", path = "../pallas-traverse" } +pallas-primitives = { version = "0.19.0-alpha.0", path = "../pallas-primitives" } +pallas-codec = { version = "0.19.0-alpha.0", path = "../pallas-codec" } diff --git a/pallas-utxorpc/README.md b/pallas-utxorpc/README.md new file mode 100644 index 0000000..7a2ca33 --- /dev/null +++ b/pallas-utxorpc/README.md @@ -0,0 +1,2 @@ +# Pallas UTxO RPC + diff --git a/pallas-utxorpc/src/lib.rs b/pallas-utxorpc/src/lib.rs new file mode 100644 index 0000000..59652b5 --- /dev/null +++ b/pallas-utxorpc/src/lib.rs @@ -0,0 +1,471 @@ +use std::ops::Deref; + +use pallas_codec::utils::KeyValuePairs; +use pallas_primitives::{alonzo, babbage}; +use pallas_traverse as trv; + +use trv::OriginalHash; + +use utxorpc::proto::cardano::v1 as u5c; + +pub fn map_purpose(x: &alonzo::RedeemerTag) -> u5c::RedeemerPurpose { + match x { + babbage::RedeemerTag::Spend => u5c::RedeemerPurpose::Spend, + babbage::RedeemerTag::Mint => u5c::RedeemerPurpose::Mint, + babbage::RedeemerTag::Cert => u5c::RedeemerPurpose::Cert, + babbage::RedeemerTag::Reward => u5c::RedeemerPurpose::Reward, + } +} + +pub fn map_redeemer(x: &alonzo::Redeemer) -> u5c::Redeemer { + u5c::Redeemer { + purpose: map_purpose(&x.tag).into(), + datum: map_plutus_datum(&x.data).into(), + } +} + +pub fn map_tx_input(i: &trv::MultiEraInput, tx: &trv::MultiEraTx) -> u5c::TxInput { + let redeemer = tx + .redeemers() + .iter() + .find(|r| (r.index as u64) == i.index()); + + u5c::TxInput { + tx_hash: i.hash().to_vec().into(), + output_index: i.index() as u32, + redeemer: redeemer.map(map_redeemer), + // TODO: map output data from some context + as_output: None, + } +} + +pub fn map_tx_output(x: &trv::MultiEraOutput) -> u5c::TxOutput { + u5c::TxOutput { + address: x.address().map(|a| a.to_vec()).unwrap_or_default().into(), + coin: x.lovelace_amount(), + // 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.non_ada_assets().iter().map(map_policy_assets).collect(), + datum: match x.datum() { + Some(babbage::PseudoDatumOption::Data(x)) => map_plutus_datum(&x.0).into(), + _ => None, + }, + 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(), + }, + script: match x.script_ref().map(|x| x.deref()) { + Some(babbage::Script::NativeScript(x)) => u5c::Script { + script: u5c::script::Script::Native(map_native_script(&x)).into(), + } + .into(), + Some(babbage::Script::PlutusV1Script(x)) => u5c::Script { + script: u5c::script::Script::PlutusV1(x.0.to_vec().into()).into(), + } + .into(), + Some(babbage::Script::PlutusV2Script(x)) => u5c::Script { + script: u5c::script::Script::PlutusV2(x.0.to_vec().into()).into(), + } + .into(), + _ => None, + }, + } +} + +pub fn map_stake_credential(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(x: &alonzo::Relay) -> u5c::Relay { + match x { + babbage::Relay::SingleHostAddr(port, v4, v6) => u5c::Relay { + ip_v4: v4.as_ref().map(|x| x.to_vec().into()).unwrap_or_default(), + ip_v6: v6.as_ref().map(|x| x.to_vec().into()).unwrap_or_default(), + dns_name: String::default(), + port: 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: 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_cert(x: &trv::MultiEraCert) -> u5c::Certificate { + let inner = match x.as_alonzo().unwrap() { + babbage::Certificate::StakeRegistration(a) => { + u5c::certificate::Certificate::StakeRegistration(map_stake_credential(a)) + } + babbage::Certificate::StakeDeregistration(a) => { + u5c::certificate::Certificate::StakeDeregistration(map_stake_credential(a)) + } + babbage::Certificate::StakeDelegation(a, b) => { + u5c::certificate::Certificate::StakeDelegation(u5c::StakeDelegationCert { + stake_credential: map_stake_credential(a).into(), + pool_keyhash: b.to_vec().into(), + }) + } + babbage::Certificate::PoolRegistration { + operator, + vrf_keyhash, + pledge, + cost, + margin, + reward_account, + pool_owners, + relays, + pool_metadata, + } => u5c::certificate::Certificate::PoolRegistration(u5c::PoolRegistrationCert { + operator: operator.to_vec().into(), + vrf_keyhash: vrf_keyhash.to_vec().into(), + pledge: *pledge, + cost: *cost, + margin: u5c::RationalNumber { + numerator: margin.numerator as i32, + denominator: margin.denominator as u32, + } + .into(), + reward_account: reward_account.to_vec().into(), + pool_owners: pool_owners.iter().map(|x| x.to_vec().into()).collect(), + relays: relays.iter().map(map_relay).collect(), + pool_metadata: pool_metadata + .as_ref() + .map(|x| u5c::PoolMetadata { + url: x.url.clone(), + hash: x.hash.to_vec().into(), + }) + .into(), + }), + babbage::Certificate::PoolRetirement(a, b) => { + u5c::certificate::Certificate::PoolRetirement(u5c::PoolRetirementCert { + pool_keyhash: a.to_vec().into(), + epoch: *b, + }) + } + babbage::Certificate::GenesisKeyDelegation(a, b, c) => { + u5c::certificate::Certificate::GenesisKeyDelegation(u5c::GenesisKeyDelegationCert { + genesis_hash: a.to_vec().into(), + genesis_delegate_hash: b.to_vec().into(), + vrf_keyhash: c.to_vec().into(), + }) + } + babbage::Certificate::MoveInstantaneousRewardsCert(a) => { + u5c::certificate::Certificate::MirCert(u5c::MirCert { + from: match &a.source { + babbage::InstantaneousRewardSource::Reserves => u5c::MirSource::Reserves.into(), + babbage::InstantaneousRewardSource::Treasury => u5c::MirSource::Treasury.into(), + }, + to: match &a.target { + babbage::InstantaneousRewardTarget::StakeCredentials(x) => x + .iter() + .map(|(k, v)| u5c::MirTarget { + stake_credential: map_stake_credential(k).into(), + delta_coin: *v, + }) + .collect(), + _ => Default::default(), + }, + other_pot: match &a.target { + babbage::InstantaneousRewardTarget::OtherAccountingPot(x) => *x, + _ => Default::default(), + }, + }) + } + }; + + u5c::Certificate { + certificate: inner.into(), + } +} + +pub fn map_withdrawals(x: &(&[u8], u64)) -> u5c::Withdrawal { + u5c::Withdrawal { + reward_account: Vec::from(x.0).into(), + coin: x.1, + } +} + +pub fn map_asset(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(x: &trv::MultiEraPolicyAssets) -> u5c::Multiasset { + u5c::Multiasset { + policy_id: x.policy().to_vec().into(), + assets: x.assets().iter().map(map_asset).collect(), + } +} + +pub fn map_vkey_witness(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| map_native_script(x)).collect(), + }) + } + babbage::NativeScript::ScriptAny(x) => { + u5c::native_script::NativeScript::ScriptAll(u5c::NativeScriptList { + items: x.iter().map(|x| map_native_script(x)).collect(), + }) + } + babbage::NativeScript::ScriptNOfK(n, k) => { + u5c::native_script::NativeScript::ScriptNOfK(u5c::ScriptNOfK { + k: *n, + scripts: k.iter().map(|x| 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(tx: &trv::MultiEraTx) -> Vec { + let ns = tx + .native_scripts() + .iter() + .map(map_native_script) + .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(x: &alonzo::Constr) -> u5c::Constr { + u5c::Constr { + tag: x.tag as u32, + any_constructor: x.any_constructor.clone().unwrap_or_default(), + fields: x.fields.iter().map(|f| map_plutus_datum(f)).collect(), + } +} + +pub fn map_plutus_map( + x: &KeyValuePairs, +) -> u5c::PlutusDataMap { + u5c::PlutusDataMap { + pairs: x + .iter() + .map(|(k, v)| u5c::PlutusDataPair { + key: map_plutus_datum(k).into(), + value: map_plutus_datum(v).into(), + }) + .collect(), + } +} + +pub fn map_plutus_array(x: &Vec) -> u5c::PlutusDataArray { + u5c::PlutusDataArray { + items: x.iter().map(map_plutus_datum).collect(), + } +} + +pub fn map_plutus_bigint(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(x: &alonzo::PlutusData) -> u5c::PlutusData { + let inner = match x { + babbage::PlutusData::Constr(x) => { + u5c::plutus_data::PlutusData::Constr(map_plutus_constr(x)) + } + babbage::PlutusData::Map(x) => u5c::plutus_data::PlutusData::Map(map_plutus_map(x)), + babbage::PlutusData::Array(x) => u5c::plutus_data::PlutusData::Array(map_plutus_array(x)), + babbage::PlutusData::BigInt(x) => { + u5c::plutus_data::PlutusData::BigInt(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| map_metadatum(x)).collect(), + }), + babbage::Metadatum::Map(x) => u5c::metadatum::Metadatum::Map(u5c::MetadatumMap { + pairs: x + .iter() + .map(|(k, v)| u5c::MetadatumPair { + key: map_metadatum(k).into(), + value: map_metadatum(v).into(), + }) + .collect(), + }), + }; + + u5c::Metadatum { + metadatum: inner.into(), + } +} + +pub fn map_metadata(label: u64, datum: &alonzo::Metadatum) -> u5c::Metadata { + u5c::Metadata { + label, + value: map_metadatum(&datum).into(), + } +} + +fn collect_all_aux_scripts(tx: &trv::MultiEraTx) -> Vec { + let ns = tx + .aux_native_scripts() + .iter() + .map(map_native_script) + .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() +} + +pub fn map_tx(tx: &trv::MultiEraTx) -> u5c::Tx { + u5c::Tx { + inputs: tx.inputs().iter().map(|i| map_tx_input(i, tx)).collect(), + outputs: tx.outputs().iter().map(|o| map_tx_output(o)).collect(), + certificates: tx.certs().iter().map(|x| map_cert(x)).collect(), + withdrawals: tx + .withdrawals() + .collect::>() + .iter() + .map(|x| map_withdrawals(x)) + .collect(), + mint: tx.mints().iter().map(|x| map_policy_assets(x)).collect(), + reference_inputs: tx + .reference_inputs() + .iter() + .map(|x| map_tx_input(x, tx)) + .collect(), + witnesses: u5c::WitnessSet { + vkeywitness: tx + .vkey_witnesses() + .iter() + .map(|x| map_vkey_witness(x)) + .collect(), + script: collect_all_scripts(tx), + plutus_datums: tx + .plutus_data() + .iter() + .map(|x| map_plutus_datum(x.deref())) + .collect(), + } + .into(), + collateral: u5c::Collateral { + collateral: tx + .collateral() + .iter() + .map(|x| map_tx_input(x, tx)) + .collect(), + collateral_return: tx.collateral_return().map(|x| 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)| map_metadata(l, d)) + .collect(), + scripts: collect_all_aux_scripts(tx), + } + .into(), + } +} diff --git a/pallas/Cargo.toml b/pallas/Cargo.toml index 9eb5d44..d35f61a 100644 --- a/pallas/Cargo.toml +++ b/pallas/Cargo.toml @@ -17,3 +17,4 @@ pallas-traverse = { version = "0.19.0-alpha.0", path = "../pallas-traverse/" } pallas-addresses = { version = "0.19.0-alpha.0", path = "../pallas-addresses/" } pallas-crypto = { version = "0.19.0-alpha.0", path = "../pallas-crypto/" } pallas-codec = { version = "0.19.0-alpha.0", path = "../pallas-codec/" } +pallas-utxorpc = { version = "0.19.0-alpha.0", path = "../pallas-utxorpc/" } diff --git a/pallas/src/lib.rs b/pallas/src/lib.rs index 84bfa16..cd983cc 100644 --- a/pallas/src/lib.rs +++ b/pallas/src/lib.rs @@ -30,3 +30,10 @@ pub use pallas_crypto as crypto; #[doc(inline)] pub use pallas_codec as codec; + +pub mod interop { + //! Interoperability with other protocols, formats & systems + + #[doc(inline)] + pub use pallas_utxorpc as utxorpc; +}