diff --git a/napi-pallas/src/lib.rs b/napi-pallas/src/lib.rs index d96be14..62879f4 100644 --- a/napi-pallas/src/lib.rs +++ b/napi-pallas/src/lib.rs @@ -159,7 +159,7 @@ impl Section { self } - fn build_child(mut self, func: F) -> Self + fn build_child(self, func: F) -> Self where F: FnOnce() -> Section, { @@ -218,9 +218,10 @@ pub struct SectionValidation { pub validations: Validations, } +#[tokio::main] #[napi] -pub fn safe_parse_tx(raw: String, context: ValidationContext) -> SectionValidation { - match tx::parse(raw, context) { +pub async fn safe_parse_tx(raw: String, context: ValidationContext) -> SectionValidation { + match tx::parse(raw, context).await { Ok(x) => { let (section, validations) = x; SectionValidation { diff --git a/napi-pallas/src/tx.rs b/napi-pallas/src/tx.rs index 6f65b2b..fc93e1d 100644 --- a/napi-pallas/src/tx.rs +++ b/napi-pallas/src/tx.rs @@ -3,8 +3,9 @@ use crate::{ProtocolParams, Validations}; use super::Section; use blockfrost::{BlockFrostSettings, BlockfrostAPI}; +use blockfrost_openapi::models::tx_content_utxo_inputs_inner::TxContentUtxoInputsInner; use dotenv::dotenv; -use num_rational::Rational32; +use num_rational::Rational64; use num_traits::FromPrimitive; use pallas::ledger::traverse::Era; use pallas::{ @@ -241,7 +242,10 @@ pub fn create_cbor_structure(tx: &MultiEraTx<'_>) -> Section { out } -pub fn parse(raw: String, context: ValidationContext) -> Result<(Section, Validations), Section> { +pub async fn parse( + raw: String, + context: ValidationContext, +) -> Result<(Section, Validations), Section> { let res_cbor = hex::decode(raw); let mut era_decode = Era::Babbage; match context.era.as_str() { @@ -257,7 +261,7 @@ pub fn parse(raw: String, context: ValidationContext) -> Result<(Section, Valida Ok(cbor) => { let res_mtx = MultiEraTx::decode_for_era(era_decode, &cbor); match res_mtx { - Ok(mtx) => Ok((create_cbor_structure(&mtx), validate(&mtx, context))), + Ok(mtx) => Ok((create_cbor_structure(&mtx), validate(&mtx, context).await)), Err(e) => { let mut err = Section::new(); err.error = Some(e.to_string()); @@ -273,21 +277,21 @@ pub fn parse(raw: String, context: ValidationContext) -> Result<(Section, Valida } } -fn to_fraction(value: f32) -> (i32, i32) { - let rational = Rational32::from_f32(value).unwrap_or_else(|| Rational32::new(0, 1)); +fn to_fraction(value: f32) -> (i64, i64) { + let rational = Rational64::from_f32(value).unwrap_or_else(|| Rational64::new(0, 1)); let (numerator, denominator) = rational.into(); (numerator, denominator) } -fn parse_param_to_i64(value: &str) -> i64 { +fn parse_string_to_i64(value: String) -> i64 { match value.parse::() { Ok(num) => num, Err(_) => 0, } } -fn parse_option_param_to_i64(value: Option) -> i64 { +fn parse_option_string_to_i64(value: Option) -> i64 { match value { Some(value) => match value.parse::() { Ok(num) => num, @@ -297,7 +301,7 @@ fn parse_option_param_to_i64(value: Option) -> i64 { } } -fn parse_option_param_to_u32(value: Option) -> u32 { +fn parse_option_string_to_u32(value: Option) -> u32 { match value { Some(value) => match value.parse::() { Ok(num) => num, @@ -307,6 +311,13 @@ fn parse_option_param_to_u32(value: Option) -> u32 { } } +fn parse_option_i32_to_u32(value: Option) -> u32 { + match value { + Some(value) => value as u32, + None => 0, + } +} + pub async fn get_epochs_latest_parameters( network: String, ) -> Result { @@ -324,31 +335,34 @@ pub async fn get_epochs_latest_parameters( match epochs_latest_parameters { Ok(params) => { let mut out = ProtocolParams::new(); - let parsed_key_deposit: i64 = parse_param_to_i64(¶ms.key_deposit); - let parsed_pool_deposit: i64 = parse_param_to_i64(¶ms.pool_deposit); - let parsed_extra_entropy: f32 = match params.extra_entropy { + let parsed_key_deposit = parse_string_to_i64(params.key_deposit); + let parsed_pool_deposit = parse_string_to_i64(params.pool_deposit); + let parsed_extra_entropy = match params.extra_entropy { Some(value) => match value.parse::() { Ok(num) => num, Err(_) => 0.0, }, None => 0.0, }; - let parsed_max_tx_ex_mem: u32 = parse_option_param_to_u32(params.max_tx_ex_mem); - let parsed_max_tx_ex_steps: i64 = parse_option_param_to_i64(params.max_tx_ex_steps); - let parsed_max_block_ex_mem: u32 = parse_option_param_to_u32(params.max_block_ex_mem); - let parsed_max_block_ex_steps: i64 = parse_option_param_to_i64(params.max_block_ex_steps); - - let parsed_max_val_size: u32 = parse_option_param_to_u32(params.max_val_size); - let parsed_collateral_percent = match params.collateral_percent { - Some(value) => value as u32, - None => 0, + let parsed_price_mem = match params.price_mem { + Some(value) => value, + None => 0.0, }; - let parsed_max_collateral_inputs: u32 = match params.max_collateral_inputs { - Some(value) => value as u32, - None => 0, + let parsed_price_step = match params.price_step { + Some(value) => value, + None => 0.0, }; - let parsed_coins_per_utxo_size: i64 = parse_option_param_to_i64(params.coins_per_utxo_size); - let parsed_coins_per_utxo_word: i64 = parse_option_param_to_i64(params.coins_per_utxo_word); + let parsed_min_utxo = parse_string_to_i64(params.min_utxo); + let parsed_min_pool_cost = parse_string_to_i64(params.min_pool_cost); + let parsed_max_tx_ex_mem = parse_option_string_to_u32(params.max_tx_ex_mem); + let parsed_max_tx_ex_steps = parse_option_string_to_i64(params.max_tx_ex_steps); + let parsed_max_block_ex_mem = parse_option_string_to_u32(params.max_block_ex_mem); + let parsed_max_block_ex_steps = parse_option_string_to_i64(params.max_block_ex_steps); + let parsed_max_val_size = parse_option_string_to_u32(params.max_val_size); + let parsed_collateral_percent = parse_option_i32_to_u32(params.collateral_percent); + let parsed_max_collateral_inputs = parse_option_i32_to_u32(params.max_collateral_inputs); + let parsed_coins_per_utxo_size = parse_option_string_to_i64(params.coins_per_utxo_size); + let parsed_coins_per_utxo_word = parse_option_string_to_i64(params.coins_per_utxo_word); let (a0_numerator, a0_denominator) = to_fraction(params.a0); let (rho_numerator, rho_denominator) = to_fraction(params.rho); @@ -356,6 +370,8 @@ pub async fn get_epochs_latest_parameters( let (decentralisation_param_numerator, decentralisation_param_denominator) = to_fraction(params.decentralisation_param); let (extra_entropy_numerator, extra_entropy_denominator) = to_fraction(parsed_extra_entropy); + let (price_mem_numerator, price_mem_denominator) = to_fraction(parsed_price_mem); + let (price_step_numerator, price_step_denominator) = to_fraction(parsed_price_step); out.epoch = params.epoch as u32; out.min_fee_a = params.min_fee_a as u32; @@ -367,16 +383,24 @@ pub async fn get_epochs_latest_parameters( out.pool_deposit = parsed_pool_deposit; out.e_max = params.e_max as i64; out.n_opt = params.n_opt as u32; - out.a0_numerator = a0_numerator as i64; - out.a0_denominator = a0_denominator as i64; - out.rho_numerator = rho_numerator as i64; - out.rho_denominator = rho_denominator as i64; - out.tau_numerator = tau_numerator as i64; - out.tau_denominator = tau_denominator as i64; - out.decentralisation_param_numerator = decentralisation_param_numerator as i64; - out.decentralisation_param_denominator = decentralisation_param_denominator as i64; + out.a0_numerator = a0_numerator; + out.a0_denominator = a0_denominator; + out.rho_numerator = rho_numerator; + out.rho_denominator = rho_denominator; + out.tau_numerator = tau_numerator; + out.tau_denominator = tau_denominator; + out.decentralisation_param_numerator = decentralisation_param_numerator; + out.decentralisation_param_denominator = decentralisation_param_denominator; out.extra_entropy_numerator = extra_entropy_numerator as u32; out.extra_entropy_denominator = extra_entropy_denominator as u32; + out.protocol_major_ver = params.protocol_major_ver as i64; + out.protocol_minor_ver = params.protocol_minor_ver as i64; + out.min_utxo = parsed_min_utxo; + out.min_pool_cost = parsed_min_pool_cost; + out.price_mem_numerator = price_mem_numerator; + out.price_mem_denominator = price_mem_denominator; + out.price_step_numerator = price_step_numerator; + out.price_step_denominator = price_step_denominator; out.max_tx_ex_mem = parsed_max_tx_ex_mem; out.max_tx_ex_steps = parsed_max_tx_ex_steps; out.max_block_ex_mem = parsed_max_block_ex_mem; @@ -396,3 +420,21 @@ pub async fn get_epochs_latest_parameters( } } } + +pub async fn get_inputs(hash: String, network: String) -> Vec { + let settings = BlockFrostSettings::new(); + dotenv().ok(); + let mut project_id = env::var("MAINNET_PROJECT_ID").expect("MAINNET_PROJECT_ID must be set."); + if network == "Preprod" { + project_id = env::var("PREPROD_PROJECT_ID").expect("PREPROD_PROJECT_ID must be set."); + } else if network == "Preview" { + project_id = env::var("PREVIEW_PROJECT_ID").expect("PREVIEW_PROJECT_ID must be set."); + } + + let api = BlockfrostAPI::new(&project_id, settings); + let tx_content = api.transactions_utxos(&hash).await; + match tx_content { + Ok(tx_) => tx_.inputs, + Err(_) => Vec::new(), + } +} diff --git a/napi-pallas/src/validations/alonzo.rs b/napi-pallas/src/validations/alonzo.rs index 71994b4..7f76ae2 100644 --- a/napi-pallas/src/validations/alonzo.rs +++ b/napi-pallas/src/validations/alonzo.rs @@ -1,3 +1,5 @@ +use std::{borrow::Cow, iter::zip, str::FromStr}; + use pallas::{ applying::{ alonzo::{ @@ -9,13 +11,18 @@ use pallas::{ utils::{get_alonzo_comp_tx_size, AlonzoProtParams}, Environment, MultiEraProtocolParameters, UTxOs, }, - ledger::primitives::{ - alonzo::{ExUnitPrices, Language, MintedTx, TransactionBody}, - conway::{ExUnits, Nonce, NonceVariant, RationalNumber}, + codec::utils::Bytes, + crypto::hash::Hash, + ledger::{ + primitives::{ + alonzo::{ExUnitPrices, Language, MintedTx, TransactionBody, TransactionOutput, Value}, + conway::{ExUnits, Nonce, NonceVariant, RationalNumber}, + }, + traverse::{MultiEraInput, MultiEraOutput, OriginalHash}, }, }; -use crate::{Validation, ValidationContext, Validations}; +use crate::{tx::get_inputs, Validation, ValidationContext, Validations}; use super::validate::set_description; use pallas::codec::utils::KeyValuePairs; @@ -234,7 +241,33 @@ fn validate_alonzo_fee(mtx_a: &MintedTx, utxos: &UTxOs, prot_pps: &AlonzoProtPar } } -pub fn validate_alonzo(mtx_a: &MintedTx, context: ValidationContext) -> Validations { +pub fn mk_utxo_for_alonzo_compatible_tx<'a>( + tx_body: &TransactionBody, + tx_outs_info: &Vec<( + String, // address in string format + Value, + Option>, + )>, +) -> UTxOs<'a> { + let mut utxos: UTxOs = UTxOs::new(); + for (tx_in, (address, amount, datum_hash)) in zip(tx_body.inputs.clone(), tx_outs_info) { + let multi_era_in = MultiEraInput::AlonzoCompatible(Box::new(Cow::Owned(tx_in))); + let address_bytes = match hex::decode(hex::encode(address)) { + Ok(bytes_vec) => Bytes::from(bytes_vec), + _ => return UTxOs::new(), + }; + let tx_out = TransactionOutput { + address: address_bytes, + amount: amount.clone(), + datum_hash: *datum_hash, + }; + let multi_era_out = MultiEraOutput::AlonzoCompatible(Box::new(Cow::Owned(tx_out))); + utxos.insert(multi_era_in, multi_era_out); + } + utxos +} + +pub async fn validate_alonzo(mtx_a: &MintedTx<'_>, context: ValidationContext) -> Validations { let tx_body: &TransactionBody = &mtx_a.transaction_body; let ppt_params = context.protocol_params; let size: &Option = &get_alonzo_comp_tx_size(tx_body); @@ -316,6 +349,73 @@ pub fn validate_alonzo(mtx_a: &MintedTx, context: ValidationContext) -> Validati block_slot: context.block_slot as u64, network_id: net_id, }; + + let inputs = get_inputs( + mtx_a.transaction_body.original_hash().to_string(), + context.network.clone(), + ) + .await; + let mut tx_outs_info = vec![]; + inputs.iter().for_each(|tx_in| { + let address = &tx_in.address; + let mut lovelace_am = 0; + let mut assets: Vec<(Hash<28>, Vec<(Bytes, u64)>)> = vec![]; + for amt in &tx_in.amount { + match amt.quantity.parse::() { + Ok(a) => { + if amt.unit == "lovelace" { + lovelace_am += a + } else { + let policy: Hash<28> = match Hash::<28>::from_str(&amt.unit[..56]) { + Ok(hash) => hash, + Err(_) => Hash::new([0; 28]), + }; + let asset_name = Bytes::from(hex::decode(amt.unit[56..].to_string()).unwrap()); + if let Some((_, policies)) = assets.iter_mut().find(|(hash, _)| hash == &policy) { + // If found, append (asset name, amount) to assets + policies.push((asset_name, amt.quantity.parse::().unwrap())); + } else { + // If not found, add a new tuple (policy, (asset name, amount)) to assets + assets.push(( + policy, + vec![(asset_name, amt.quantity.parse::().unwrap())], + )); + } + } + } + Err(_) => { + // TODO: Handle error appropriately + continue; // Skip this iteration if parsing fails + } + } + } + + let datum_opt = match &tx_in.data_hash { + Some(data_hash) => Some(hex::decode(data_hash).unwrap().as_slice().into()), + _ => None, + }; + if assets.len() > 0 { + let transformed_assets: Vec<(Hash<28>, KeyValuePairs)> = assets + .into_iter() + .map(|(hash, vec)| { + let kv_pairs = KeyValuePairs::from(Vec::from(vec)); + (hash, kv_pairs) + }) + .collect(); + tx_outs_info.push(( + address.clone(), + Value::Multiasset( + lovelace_am, + KeyValuePairs::from(Vec::from(transformed_assets)), + ), + datum_opt, + )); + } else { + tx_outs_info.push((address.clone(), Value::Coin(lovelace_am), datum_opt)); + } + }); + let utxos = mk_utxo_for_alonzo_compatible_tx(&mtx_a.transaction_body, &tx_outs_info); + let out = Validations::new() .with_era("Alonzo".to_string()) .add_new_validation(validate_alonzo_tx_size(size, &prot_params)) @@ -328,6 +428,10 @@ pub fn validate_alonzo(mtx_a: &MintedTx, context: ValidationContext) -> Validati .add_new_validation(validate_alonzo_tx_ex_units(mtx_a, &prot_params)) .add_new_validation(validate_alonzo_languages(mtx_a, &prot_params)) .add_new_validation(validate_alonzo_network_id(mtx_a, &env.network_id)) - .add_new_validation(validate_alonzo_tx_validity_interval(mtx_a, &env.block_slot)); + .add_new_validation(validate_alonzo_tx_validity_interval(mtx_a, &env.block_slot)) + .add_new_validation(validate_alonzo_ins_and_collateral_in_utxos(mtx_a, &utxos)) + .add_new_validation(validate_alonzo_preservation_of_value(mtx_a, &utxos)) + .add_new_validation(validate_alonzo_witness_set(mtx_a, &utxos)) + .add_new_validation(validate_alonzo_fee(mtx_a, &utxos, &prot_params)); out } diff --git a/napi-pallas/src/validations/babbage.rs b/napi-pallas/src/validations/babbage.rs index 147823e..e5139c2 100644 --- a/napi-pallas/src/validations/babbage.rs +++ b/napi-pallas/src/validations/babbage.rs @@ -1,7 +1,4 @@ -use crate::{Validation, ValidationContext, Validations}; -use blockfrost::{BlockFrostSettings, BlockfrostAPI}; -use blockfrost_openapi::models::tx_content_utxo_outputs_inner::TxContentUtxoOutputsInner; -use dotenv::dotenv; +use crate::{tx::get_inputs, Validation, ValidationContext, Validations}; use pallas::{ applying::{ babbage::{ @@ -13,15 +10,25 @@ use pallas::{ utils::{get_babbage_tx_size, BabbageProtParams}, Environment, MultiEraProtocolParameters, UTxOs, }, + codec::{ + minicbor::{Decode, Decoder}, + utils::{Bytes, CborWrap, KeepRaw, KeyValuePairs}, + }, + crypto::hash::Hash, ledger::{ primitives::{ alonzo::ExUnitPrices, - babbage::{CostMdls, MintedTransactionBody, MintedTx as BabbageMintedTx}, - conway::{Nonce, NonceVariant, RationalNumber}, + babbage::{ + CostMdls, MintedDatumOption, MintedPostAlonzoTransactionOutput, MintedScriptRef, + MintedTransactionBody, MintedTransactionOutput, MintedTx as BabbageMintedTx, + PseudoTransactionOutput, Value, + }, + conway::{Nonce, NonceVariant, PlutusV2Script, RationalNumber}, }, - traverse::update::ExUnits, + traverse::{update::ExUnits, MultiEraInput, MultiEraOutput, OriginalHash}, }, }; +use std::{borrow::Cow, iter::zip, str::FromStr}; use super::validate::set_description; @@ -290,41 +297,40 @@ fn validate_babbage_network_id(mtx: &BabbageMintedTx, network_id: u8) -> Validat .with_description(description); } -use std::env; - -pub async fn get_input_from_index( - hash: String, - network: String, - index: i32, -) -> Option { - let settings = BlockFrostSettings::new(); - dotenv().ok(); - let mut project_id = env::var("MAINNET_PROJECT_ID").expect("MAINNET_PROJECT_ID must be set."); - if network == "Preprod" { - project_id = env::var("PREPROD_PROJECT_ID").expect("PREPROD_PROJECT_ID must be set."); - } else if network == "Preview" { - project_id = env::var("PREVIEW_PROJECT_ID").expect("PREVIEW_PROJECT_ID must be set."); - } +pub fn mk_utxo_for_babbage_tx<'a>( + tx_body: &MintedTransactionBody, + tx_outs_info: &'a Vec<( + String, // address in string format + Value, + Option, + Option>, + )>, +) -> UTxOs<'a> { + let mut utxos: UTxOs = UTxOs::new(); - let api = BlockfrostAPI::new(&project_id, settings); - let tx = api.transactions_utxos(&hash).await; - print!("{:?}", tx); - match tx { - Ok(tx_) => { - let outputs = tx_.outputs; - outputs.iter().find_map(|output| { - if output.output_index == index { - Some(output.clone()) - } else { - None - } - }) - } - Err(_) => None, + for (tx_in, (addr, val, datum_opt, script_ref)) in zip(tx_body.inputs.clone(), tx_outs_info) { + let multi_era_in = MultiEraInput::AlonzoCompatible(Box::new(Cow::Owned(tx_in))); + let address_bytes = match hex::decode(hex::encode(addr)) { + Ok(bytes_vec) => Bytes::from(bytes_vec), + _ => return UTxOs::new(), + }; + let tx_out: MintedTransactionOutput = + PseudoTransactionOutput::PostAlonzo(MintedPostAlonzoTransactionOutput { + address: address_bytes, + value: val.clone(), + datum_option: datum_opt.clone(), + script_ref: script_ref.clone(), + }); + let multi_era_out: MultiEraOutput = MultiEraOutput::Babbage(Box::new(Cow::Owned(tx_out))); + utxos.insert(multi_era_in, multi_era_out); } + utxos } -pub fn validate_babbage(mtx_b: &BabbageMintedTx, context: ValidationContext) -> Validations { +pub async fn validate_babbage( + mtx_b: &BabbageMintedTx<'_>, + context: ValidationContext, +) -> Validations { let tx_body: &MintedTransactionBody = &mtx_b.transaction_body.clone(); let ppt_params = context.protocol_params; let size: &Option = &get_babbage_tx_size(tx_body); @@ -410,8 +416,91 @@ pub fn validate_babbage(mtx_b: &BabbageMintedTx, context: ValidationContext) -> network_id: net_id, }; - let inputs = mtx_b.transaction_body.inputs.clone(); - let mut utxos: UTxOs = UTxOs::new(); + let inputs = get_inputs( + mtx_b.transaction_body.original_hash().to_string(), + context.network.clone(), + ) + .await; + let mut tx_outs_info = vec![]; + + inputs.iter().for_each(|tx_in| { + let address = &tx_in.address; + let mut lovelace_am = 0; + let mut assets: Vec<(Hash<28>, Vec<(Bytes, u64)>)> = vec![]; + for amt in &tx_in.amount { + match amt.quantity.parse::() { + Ok(a) => { + if amt.unit == "lovelace" { + lovelace_am += a + } else { + let policy: Hash<28> = match Hash::<28>::from_str(&amt.unit[..56]) { + Ok(hash) => hash, + Err(_) => Hash::new([0; 28]), + }; + let asset_name = Bytes::from(hex::decode(amt.unit[56..].to_string()).unwrap()); + if let Some((_, policies)) = assets.iter_mut().find(|(hash, _)| hash == &policy) { + // If found, append (asset name, amount) to assets + policies.push((asset_name, amt.quantity.parse::().unwrap())); + } else { + // If not found, add a new tuple (policy, (asset name, amount)) to assets + assets.push(( + policy, + vec![(asset_name, amt.quantity.parse::().unwrap())], + )); + } + } + } + Err(_) => { + // TODO: Handle error appropriately + continue; // Skip this iteration if parsing fails + } + } + } + + let datum_opt = match &tx_in.data_hash { + Some(data_hash) => Some(MintedDatumOption::Hash( + hex::decode(data_hash).unwrap().as_slice().into(), + )), + _ => match &tx_in.inline_datum { + // TODO: Fix so that the inline datum is properly parsed + Some(_) => None::, + _ => None::, + }, + }; + let script_ref = match &tx_in.reference_script_hash { + Some(script) => Some(CborWrap(MintedScriptRef::PlutusV2Script(PlutusV2Script( + Bytes::from(hex::decode(script).unwrap()), + )))), + _ => None::>, + }; + if assets.len() > 0 { + let transformed_assets: Vec<(Hash<28>, KeyValuePairs)> = assets + .into_iter() + .map(|(hash, vec)| { + let kv_pairs = KeyValuePairs::from(Vec::from(vec)); + (hash, kv_pairs) + }) + .collect(); + tx_outs_info.push(( + address.clone(), + Value::Multiasset( + lovelace_am, + KeyValuePairs::from(Vec::from(transformed_assets)), + ), + datum_opt, + script_ref, + )); + } else { + tx_outs_info.push(( + address.clone(), + Value::Coin(lovelace_am), + datum_opt, + script_ref, + )); + } + }); + + let utxos = mk_utxo_for_babbage_tx(&mtx_b.transaction_body, &tx_outs_info); let out = Validations::new() .with_era(context.era.to_string()) @@ -423,10 +512,29 @@ pub fn validate_babbage(mtx_b: &BabbageMintedTx, context: ValidationContext) -> .add_new_validation(validate_babbage_output_val_size(&mtx_b, &prot_params)) .add_new_validation(validate_babbage_tx_ex_units(&mtx_b, &prot_params)) .add_new_validation(validate_babbage_tx_size(&size, &prot_params)) + .add_new_validation(validate_babbage_fee(&mtx_b, &size, &utxos, &prot_params)) + .add_new_validation(validate_babbage_witness_set(&mtx_b, &utxos)) + .add_new_validation(validate_babbage_all_ins_in_utxos(&mtx_b, &utxos)) + .add_new_validation(validate_babbage_preservation_of_value(&mtx_b, &utxos)) + .add_new_validation(validate_babbage_languages( + &mtx_b, + &utxos, + &magic, + &net_id, + env.block_slot, + )) + .add_new_validation(validate_babbage_script_data_hash( + &mtx_b, + &utxos, + &magic, + &net_id, + env.block_slot, + )) .add_new_validation(validate_babbage_tx_validity_interval( &mtx_b, env.block_slot, )) .add_new_validation(validate_babbage_network_id(&mtx_b, env.network_id)); + out } diff --git a/napi-pallas/src/validations/byron.rs b/napi-pallas/src/validations/byron.rs index bee436d..a659035 100644 --- a/napi-pallas/src/validations/byron.rs +++ b/napi-pallas/src/validations/byron.rs @@ -1,4 +1,6 @@ -use crate::{Validation, Validations}; +use std::{borrow::Cow, iter::zip}; + +use crate::{tx::get_inputs, Validation, ValidationContext, Validations}; use pallas::{ applying::{ byron::{ @@ -8,8 +10,14 @@ use pallas::{ utils::ByronProtParams, UTxOs, }, - codec::minicbor::encode, - ledger::primitives::byron::{MintedTxPayload, Tx}, + codec::{ + minicbor::{bytes::ByteVec, encode}, + utils::TagWrap, + }, + ledger::{ + primitives::byron::{Address, MintedTxPayload, Tx, TxOut}, + traverse::{MultiEraInput, MultiEraOutput, OriginalHash}, + }, }; use super::validate::set_description; @@ -120,7 +128,28 @@ fn validate_byron_fees( .with_description(description); } -pub fn validate_byron(mtxp: &MintedTxPayload) -> Validations { +pub fn mk_utxo_for_byron_tx<'a>(tx: &Tx, tx_outs_info: &[(String, u64)]) -> UTxOs<'a> { + let mut utxos: UTxOs = UTxOs::new(); + for (tx_in, (address_payload, amount)) in zip(tx.inputs.clone().to_vec(), tx_outs_info) { + let input_tx_out_addr: Address = match hex::decode(hex::encode(address_payload)) { + Ok(addr_bytes) => Address { + payload: TagWrap(ByteVec::from(addr_bytes)), + crc: 3430631884, + }, + _ => return UTxOs::new(), + }; + let tx_out: TxOut = TxOut { + address: input_tx_out_addr, + amount: *amount, + }; + let multi_era_in: MultiEraInput = MultiEraInput::Byron(Box::new(Cow::Owned(tx_in))); + let multi_era_out: MultiEraOutput = MultiEraOutput::Byron(Box::new(Cow::Owned(tx_out))); + utxos.insert(multi_era_in, multi_era_out); + } + utxos +} + +pub async fn validate_byron(mtxp: &MintedTxPayload<'_>, context: ValidationContext) -> Validations { let tx: &Tx = &mtxp.transaction; let size: &u64 = &get_tx_size(&tx); let prot_pps: ByronProtParams = ByronProtParams { @@ -141,11 +170,44 @@ pub fn validate_byron(mtxp: &MintedTxPayload) -> Validations { unlock_stake_epoch: 18446744073709551615, }; + let inputs = get_inputs( + mtxp.transaction.original_hash().to_string(), + context.network.clone(), + ) + .await; + let mut tx_outs_info = vec![]; + inputs.iter().for_each(|tx_in| { + let address = &tx_in.address; + let mut lovelace_am = 0; + for amt in &tx_in.amount { + match amt.quantity.parse::() { + Ok(a) => lovelace_am += a, + Err(_) => { + // TODO: Handle error appropriately + continue; // Skip this iteration if parsing fails + } + } + } + + tx_outs_info.push((address.clone(), lovelace_am)); + }); + let utxos = mk_utxo_for_byron_tx(&mtxp.transaction, &tx_outs_info); + let mut magic = 764824073; // For mainnet + if context.network == "Preprod" { + magic = 1; + } else if context.network == "Preview" { + magic = 2; + } + let out = Validations::new() .with_era("Byron".to_string()) .add_new_validation(validate_byron_size(&size, &prot_pps)) .add_new_validation(validate_byron_ins_not_empty(&tx)) .add_new_validation(validate_byron_outs_not_empty(&tx)) - .add_new_validation(validate_byron_outs_have_lovelace(&tx)); + .add_new_validation(validate_byron_outs_have_lovelace(&tx)) + .add_new_validation(validate_byron_ins_in_utxos(&tx, &utxos)) + .add_new_validation(validate_byron_witnesses(&mtxp, &utxos, magic)) + .add_new_validation(validate_byron_fees(&tx, &size, &utxos, &prot_pps)); + out } diff --git a/napi-pallas/src/validations/shelley_ma.rs b/napi-pallas/src/validations/shelley_ma.rs index 4676d92..0e6dbf1 100644 --- a/napi-pallas/src/validations/shelley_ma.rs +++ b/napi-pallas/src/validations/shelley_ma.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + use pallas::{ applying::{ shelley_ma::{ @@ -8,18 +10,20 @@ use pallas::{ utils::{get_alonzo_comp_tx_size, ShelleyProtParams}, Environment, MultiEraProtocolParameters, UTxOs, }, + codec::utils::{Bytes, KeyValuePairs}, + crypto::hash::Hash, ledger::{ primitives::{ - alonzo::{MintedTx, MintedWitnessSet, TransactionBody}, + alonzo::{MintedTx, MintedWitnessSet, TransactionBody, Value}, conway::{NonceVariant, RationalNumber}, }, - traverse::{update::Nonce, Era}, + traverse::{update::Nonce, Era, OriginalHash}, }, }; -use crate::{Validation, ValidationContext, Validations}; +use crate::{tx::get_inputs, Validation, ValidationContext, Validations}; -use super::validate::set_description; +use super::{alonzo::mk_utxo_for_alonzo_compatible_tx, validate::set_description}; // & The following validation requires the size and the protocol parameters fn validate_shelley_ma_tx_size(size: &Option, prot_pps: &ShelleyProtParams) -> Validation { @@ -203,8 +207,8 @@ fn validate_shelley_ma_network_id(mtx_sma: &MintedTx, network_id: &u8) -> Valida .with_description(description); } -pub fn validate_shelley_ma( - mtx_sma: &MintedTx, +pub async fn validate_shelley_ma( + mtx_sma: &MintedTx<'_>, era: &Era, context: ValidationContext, ) -> Validations { @@ -267,6 +271,73 @@ pub fn validate_shelley_ma( block_slot: context.block_slot as u64, network_id: net_id, }; + + let inputs = get_inputs( + mtx_sma.transaction_body.original_hash().to_string(), + context.network.clone(), + ) + .await; + let mut tx_outs_info = vec![]; + inputs.iter().for_each(|tx_in| { + let address = &tx_in.address; + let mut lovelace_am = 0; + let mut assets: Vec<(Hash<28>, Vec<(Bytes, u64)>)> = vec![]; + for amt in &tx_in.amount { + match amt.quantity.parse::() { + Ok(a) => { + if amt.unit == "lovelace" { + lovelace_am += a + } else { + let policy: Hash<28> = match Hash::<28>::from_str(&amt.unit[..56]) { + Ok(hash) => hash, + Err(_) => Hash::new([0; 28]), + }; + let asset_name = Bytes::from(hex::decode(amt.unit[56..].to_string()).unwrap()); + if let Some((_, policies)) = assets.iter_mut().find(|(hash, _)| hash == &policy) { + // If found, append (asset name, amount) to assets + policies.push((asset_name, amt.quantity.parse::().unwrap())); + } else { + // If not found, add a new tuple (policy, (asset name, amount)) to assets + assets.push(( + policy, + vec![(asset_name, amt.quantity.parse::().unwrap())], + )); + } + } + } + Err(_) => { + // TODO: Handle error appropriately + continue; // Skip this iteration if parsing fails + } + } + } + + let datum_opt = match &tx_in.data_hash { + Some(data_hash) => Some(hex::decode(data_hash).unwrap().as_slice().into()), + _ => None, + }; + if assets.len() > 0 { + let transformed_assets: Vec<(Hash<28>, KeyValuePairs)> = assets + .into_iter() + .map(|(hash, vec)| { + let kv_pairs = KeyValuePairs::from(Vec::from(vec)); + (hash, kv_pairs) + }) + .collect(); + tx_outs_info.push(( + address.clone(), + Value::Multiasset( + lovelace_am, + KeyValuePairs::from(Vec::from(transformed_assets)), + ), + datum_opt, + )); + } else { + tx_outs_info.push((address.clone(), Value::Coin(lovelace_am), datum_opt)); + } + }); + let utxos = mk_utxo_for_alonzo_compatible_tx(&mtx_sma.transaction_body, &tx_outs_info); + let out = Validations::new() .with_era("Shelley Mary Allegra".to_string()) .add_new_validation(validate_shelley_ma_tx_size(size, &prot_params)) @@ -276,6 +347,12 @@ pub fn validate_shelley_ma( .add_new_validation(validate_shelley_ma_min_lovelace(&mtx_sma, &prot_params)) .add_new_validation(validate_shelley_ma_fees(&mtx_sma, &size, &prot_params)) .add_new_validation(validate_shelley_ma_ttl(&mtx_sma, &env.block_slot)) - .add_new_validation(validate_shelley_ma_network_id(&mtx_sma, &env.network_id)); + .add_new_validation(validate_shelley_ma_network_id(&mtx_sma, &env.network_id)) + .add_new_validation(validate_shelley_ma_ins_in_utxos(&mtx_sma, &utxos)) + .add_new_validation(validate_shelley_ma_preservation_of_value( + &mtx_sma, &utxos, era, + )) + .add_new_validation(validate_shelley_ma_witnesses(&mtx_sma, &tx_wits, &utxos)); + out } diff --git a/napi-pallas/src/validations/validate.rs b/napi-pallas/src/validations/validate.rs index b1b2c1b..7e3e8ea 100644 --- a/napi-pallas/src/validations/validate.rs +++ b/napi-pallas/src/validations/validate.rs @@ -16,20 +16,20 @@ pub fn set_description(res: &Result<(), ValidationError>, success: String) -> St } } -pub fn validate(mtx: &MultiEraTx<'_>, context: ValidationContext) -> Validations { +pub async fn validate(mtx: &MultiEraTx<'_>, context: ValidationContext) -> Validations { match &mtx { - MultiEraTx::Byron(mtxp) => validate_byron(&mtxp), + MultiEraTx::Byron(mtxp) => validate_byron(&mtxp, context).await, MultiEraTx::AlonzoCompatible(mtx_sma, Era::Shelley) => { - validate_shelley_ma(&mtx_sma, &Era::Shelley, context) + validate_shelley_ma(&mtx_sma, &Era::Shelley, context).await } MultiEraTx::AlonzoCompatible(mtx_sma, Era::Allegra) => { - validate_shelley_ma(&mtx_sma, &Era::Allegra, context) + validate_shelley_ma(&mtx_sma, &Era::Allegra, context).await } MultiEraTx::AlonzoCompatible(mtx_sma, Era::Mary) => { - validate_shelley_ma(&mtx_sma, &Era::Mary, context) + validate_shelley_ma(&mtx_sma, &Era::Mary, context).await } - MultiEraTx::AlonzoCompatible(mtx_a, Era::Alonzo) => validate_alonzo(&mtx_a, context), - MultiEraTx::Babbage(mtx_b) => validate_babbage(&mtx_b, context), + MultiEraTx::AlonzoCompatible(mtx_a, Era::Alonzo) => validate_alonzo(&mtx_a, context).await, + MultiEraTx::Babbage(mtx_b) => validate_babbage(&mtx_b, context).await, MultiEraTx::Conway(_) => validate_conway(), // This case is impossible. TODO: Handle error _ => Validations::new(), diff --git a/web/app/components/Validations/Configurations/ContextTab.tsx b/web/app/components/Validations/Configurations/ContextTab.tsx index 82776c0..fdb1d4a 100644 --- a/web/app/components/Validations/Configurations/ContextTab.tsx +++ b/web/app/components/Validations/Configurations/ContextTab.tsx @@ -156,12 +156,13 @@ export function ContextTab({ disabled={isByron} value={Number(param.value).toString() ?? 0} onChange={changeParam(index)} - className={`block w-full px-4 py-2 mt-1 border-2 bg-white border-black h-16 shadow shadow-black rounded-lg rounded-b-xl border-b-8 appearance-none text-black placeholder-gray-400 text-2xl outline-none - ${ - isByron - ? "bg-slate-300 cursor-not-allowed" - : "focus:bg-pink-200 hover:bg-pink-200" - }`} + className={`block w-full px-4 py-2 mt-1 border-2 bg-white border-black h-16 shadow shadow-black + rounded-lg rounded-b-xl border-b-8 appearance-none text-black placeholder-gray-400 text-2xl outline-none + ${ + isByron + ? "bg-slate-300 cursor-not-allowed" + : "focus:bg-pink-200 hover:bg-pink-200" + }`} inputSize="small" /> diff --git a/web/app/routes/tx.tsx b/web/app/routes/tx.tsx index 756cbfa..6353119 100644 --- a/web/app/routes/tx.tsx +++ b/web/app/routes/tx.tsx @@ -157,7 +157,7 @@ export default function Index() {
setModalOpen(false)} uiConfigs={uiConfigs} setUiConfigs={setUiConfigs} /> diff --git a/web/app/utils.ts b/web/app/utils.ts index c36a4f1..da0f6a8 100644 --- a/web/app/utils.ts +++ b/web/app/utils.ts @@ -201,13 +201,13 @@ export enum UIOptions { } const reducibleParams = [ - "A0", - "Rho", - "Tau", - "DecentralisationParam", - "ExtraEntropy", - "PriceMem", - "PriceStep", + "a0", + "rho", + "tau", + "decentralisationParam", + "extraEntropy", + "priceMem", + "priceStep", ]; export const paramsParser = ( @@ -223,8 +223,8 @@ export const paramsParser = ( .split(/(?=[A-Z])/) .join(" ") .replace("Transaction", "Tx") - .replace("Numerator", ""); - + .replace("Numerator", "") + .trim(); if (reducibleParams.includes(parsedKey)) { let denominator = Number( newParams[`${parsedKey}Denominator` as keyof ProtocolParams] @@ -253,6 +253,9 @@ export const ByronValidations = [ "Transaction size", "Non empty outputs", "Outputs have lovelace", + "Inputs in UTXOs", + "Witnesses", + "Fees", ]; export const ShelleyMAValidations = [ "Transaction size", @@ -263,6 +266,9 @@ export const ShelleyMAValidations = [ "Fees", "TTL", "Network id", + "Inputs in UTxOs", + "Preservation of value", + "Witnesses", ]; export const AlonzoValidations = [ "Non empty inputs", @@ -276,6 +282,10 @@ export const AlonzoValidations = [ "Outputs value size", "Execution units", "Languages", + "Inputs and collateral in UTxOs", + "Preservation of value", + "Witness set", + "Fee", ]; export const BabbageValidations = [ "Non empty inputs", @@ -286,6 +296,12 @@ export const BabbageValidations = [ "Output value size", "Transaction execution units", "Transaction size", + "Fee", + "Witness set", + "All inputs in UTxOs", + "Preservation of value", + "Languages", + "Script data hash", "Validity interval", "Network id", ];