From 70583680646eea2d5bd018a568470ecba55eba91 Mon Sep 17 00:00:00 2001 From: Santiago Carmuega Date: Wed, 14 May 2025 21:31:23 -0300 Subject: [PATCH] chore: remove legacy tx-builder --- balius-sdk/src/lib.rs | 3 - balius-sdk/src/qol.rs | 43 +- balius-sdk/src/txbuilder/asset_math.rs | 322 --------- balius-sdk/src/txbuilder/build.rs | 201 ------ balius-sdk/src/txbuilder/dsl.rs | 893 ------------------------- balius-sdk/src/txbuilder/mod.rs | 87 --- balius-sdk/src/txbuilder/plutus.rs | 51 -- 7 files changed, 10 insertions(+), 1590 deletions(-) delete mode 100644 balius-sdk/src/txbuilder/asset_math.rs delete mode 100644 balius-sdk/src/txbuilder/build.rs delete mode 100644 balius-sdk/src/txbuilder/dsl.rs delete mode 100644 balius-sdk/src/txbuilder/mod.rs delete mode 100644 balius-sdk/src/txbuilder/plutus.rs diff --git a/balius-sdk/src/lib.rs b/balius-sdk/src/lib.rs index 8498dd1..1aeef75 100644 --- a/balius-sdk/src/lib.rs +++ b/balius-sdk/src/lib.rs @@ -13,9 +13,6 @@ pub use balius_macros as macros; /// Macro to mark the main function for the worker pub use balius_macros::main; -/// Transaction builder artifacts -pub mod txbuilder; - /// Internal functions to be used by the generated code pub mod _internal; diff --git a/balius-sdk/src/qol.rs b/balius-sdk/src/qol.rs index 460a28a..f868649 100644 --- a/balius-sdk/src/qol.rs +++ b/balius-sdk/src/qol.rs @@ -1,6 +1,8 @@ use std::marker::PhantomData; +use pallas_primitives::Fragment; use thiserror::Error; +use utxorpc_spec::utxorpc::v1alpha::cardano::PParams; use crate::_internal::Handler; use crate::wit; @@ -9,18 +11,24 @@ use crate::wit; pub enum Error { #[error("internal error: {0}")] Internal(String), + #[error("bad config")] BadConfig, + #[error("bad params")] BadParams, + #[error("bad utxo")] BadUtxo, + #[error("event mismatch, expected {0}")] EventMismatch(String), + #[error("kv error: {0}")] - KV(wit::balius::app::kv::KvError), + KV(#[from] wit::balius::app::kv::KvError), + #[error("ledger error: {0}")] - Ledger(wit::balius::app::ledger::LedgerError), + Ledger(#[from] wit::balius::app::ledger::LedgerError), } impl From for wit::HandleError { @@ -64,24 +72,6 @@ impl From for Error { } } -impl From for Error { - fn from(error: wit::balius::app::kv::KvError) -> Self { - Error::KV(error) - } -} - -impl From for Error { - fn from(error: wit::balius::app::ledger::LedgerError) -> Self { - Error::Ledger(error) - } -} - -impl From for Error { - fn from(error: crate::txbuilder::BuildError) -> Self { - Error::Internal(error.to_string()) - } -} - pub type WorkerResult = std::result::Result; pub struct FnHandler @@ -257,19 +247,6 @@ impl TryFrom for Utxo { } } -pub struct NewTx(pub Box); - -impl TryInto for NewTx { - type Error = Error; - - fn try_into(self) -> Result { - let ledger = crate::txbuilder::ExtLedgerFacade; - let tx = crate::txbuilder::build(self.0, ledger)?; - let cbor = pallas_codec::minicbor::to_vec(&tx).unwrap(); - Ok(wit::Response::PartialTx(cbor)) - } -} - impl crate::_internal::Worker { pub fn new() -> Self { Self::default() diff --git a/balius-sdk/src/txbuilder/asset_math.rs b/balius-sdk/src/txbuilder/asset_math.rs deleted file mode 100644 index 8ebd366..0000000 --- a/balius-sdk/src/txbuilder/asset_math.rs +++ /dev/null @@ -1,322 +0,0 @@ -use pallas_crypto::hash::Hash; -use pallas_primitives::{ - conway::{self, Value}, - AssetName, NonEmptyKeyValuePairs, NonZeroInt, PolicyId, PositiveCoin, -}; -use std::collections::{hash_map::Entry, BTreeMap, HashMap}; - -use super::BuildError; - -fn fold_assets( - acc: &mut HashMap, - item: NonEmptyKeyValuePairs, -) where - T: SafeAdd + Copy, -{ - for (key, value) in item.to_vec() { - match acc.entry(key) { - Entry::Occupied(mut entry) => { - if let Some(new_val) = value.try_add(*entry.get()) { - entry.insert(new_val); - } else { - entry.remove(); - } - } - Entry::Vacant(entry) => { - entry.insert(value); - } - } - } -} - -pub fn fold_multiassets( - acc: &mut HashMap, HashMap>, - item: NonEmptyKeyValuePairs, NonEmptyKeyValuePairs>, -) where - T: SafeAdd + Copy, -{ - for (key, value) in item.to_vec() { - let mut map = acc.remove(&key).unwrap_or_default(); - fold_assets(&mut map, value); - acc.insert(key, map); - } -} - -pub fn aggregate_assets( - items: impl IntoIterator>, -) -> Option> -where - T: SafeAdd + Copy, -{ - let mut total_assets = HashMap::new(); - - for assets in items { - fold_multiassets(&mut total_assets, assets); - } - - let total_assets_vec = total_assets - .into_iter() - .filter_map(|(key, assets)| { - let assets_vec = assets.into_iter().collect(); - Some((key, NonEmptyKeyValuePairs::from_vec(assets_vec)?)) - }) - .collect(); - - NonEmptyKeyValuePairs::from_vec(total_assets_vec) -} - -pub fn aggregate_values(items: impl IntoIterator) -> Value { - let mut total_coin = 0; - let mut assets = vec![]; - - for value in items { - match value { - Value::Coin(x) => { - total_coin += x; - } - Value::Multiasset(x, y) => { - total_coin += x; - assets.push(y); - } - } - } - - if let Some(total_assets) = aggregate_assets(assets) { - Value::Multiasset(total_coin, total_assets) - } else { - Value::Coin(total_coin) - } -} - -pub fn add_mint(value: &Value, mint: &conway::Mint) -> Result { - let (coin, mut og_assets) = match value { - Value::Coin(c) => (*c, BTreeMap::new()), - Value::Multiasset(c, a) => { - let flattened: BTreeMap<&PolicyId, BTreeMap<&AssetName, u64>> = a - .iter() - .map(|(policy, assets)| { - let values = assets - .iter() - .map(move |(name, value)| (name, value.into())) - .collect(); - (policy, values) - }) - .collect(); - (*c, flattened) - } - }; - let mut final_assets = vec![]; - for (policy, mint_assets) in mint.iter() { - let assets = og_assets.remove(policy).unwrap_or_default(); - let mut policy_assets = vec![]; - for (name, value) in mint_assets.iter() { - let old_value = assets.get(name).copied().unwrap_or_default(); - let minted: i64 = value.into(); - let Some(new_value) = old_value.checked_add_signed(minted) else { - return Err(BuildError::OutputsTooHigh); - }; - if let Ok(asset) = PositiveCoin::try_from(new_value) { - policy_assets.push((name.clone(), asset)); - } - } - if let Some(assets) = NonEmptyKeyValuePairs::from_vec(policy_assets) { - final_assets.push((policy.clone(), assets)); - } - } - - if let Some(assets) = NonEmptyKeyValuePairs::from_vec(final_assets) { - Ok(Value::Multiasset(coin, assets)) - } else { - Ok(Value::Coin(coin)) - } -} - -pub fn subtract_value(lhs: &Value, rhs: &Value) -> Result { - let (lhs_coin, lhs_assets) = match lhs { - Value::Coin(c) => (*c, vec![]), - Value::Multiasset(c, a) => (*c, a.iter().collect()), - }; - - let (rhs_coin, mut rhs_assets) = match rhs { - Value::Coin(c) => (*c, HashMap::new()), - Value::Multiasset(c, a) => { - let flattened: HashMap<(&PolicyId, &AssetName), u64> = a - .iter() - .flat_map(|(policy, assets)| { - assets - .iter() - .map(move |(name, value)| ((policy, name), value.into())) - }) - .collect(); - (*c, flattened) - } - }; - - let Some(final_coin) = lhs_coin.checked_sub(rhs_coin) else { - return Err(BuildError::OutputsTooHigh); - }; - - let mut final_assets = vec![]; - for (policy, assets) in lhs_assets { - let mut policy_assets = vec![]; - for (name, value) in assets.iter() { - let lhs_value: u64 = value.into(); - let rhs_value: u64 = rhs_assets.remove(&(policy, name)).unwrap_or_default(); - let Some(final_value) = lhs_value.checked_sub(rhs_value) else { - return Err(BuildError::OutputsTooHigh); - }; - if let Ok(final_coin) = final_value.try_into() { - policy_assets.push((name.clone(), final_coin)); - } - } - if let Some(assets) = NonEmptyKeyValuePairs::from_vec(policy_assets) { - final_assets.push((*policy, assets)); - } - } - - if !rhs_assets.is_empty() { - // We have an output which didn't come from any inputs - return Err(BuildError::OutputsTooHigh); - } - - if let Some(assets) = NonEmptyKeyValuePairs::from_vec(final_assets) { - Ok(Value::Multiasset(final_coin, assets)) - } else { - Ok(Value::Coin(final_coin)) - } -} - -pub fn value_coin(value: &Value) -> u64 { - match value { - Value::Coin(x) => *x, - Value::Multiasset(x, _) => *x, - } -} - -fn try_to_mint( - assets: conway::Multiasset, - f: F, -) -> Result -where - F: Fn(i64) -> Result, -{ - let mut new_assets = vec![]; - for (policy, asset) in assets { - let mut new_asset = vec![]; - for (name, quantity) in asset { - let quantity: u64 = quantity.into(); - if quantity > i64::MAX as u64 { - return Err(BuildError::AssetValueTooHigh); - } - let quantity: NonZeroInt = f(quantity as i64).unwrap(); - new_asset.push((name, quantity)); - } - let asset = NonEmptyKeyValuePairs::from_vec(new_asset).unwrap(); - new_assets.push((policy, asset)); - } - - Ok(NonEmptyKeyValuePairs::from_vec(new_assets).unwrap()) -} - -pub fn multiasset_coin_to_mint( - assets: conway::Multiasset, -) -> Result { - try_to_mint(assets, |quantity| quantity.try_into()) -} - -pub fn multiasset_coin_to_burn( - assets: conway::Multiasset, -) -> Result { - try_to_mint(assets, |quantity| (-quantity).try_into()) -} - -pub fn value_saturating_add_coin(value: Value, coin: i64) -> Value { - match value { - Value::Coin(x) => Value::Coin(x.saturating_add_signed(coin)), - Value::Multiasset(x, assets) => Value::Multiasset(x.saturating_add_signed(coin), assets), - } -} - -pub trait SafeAdd: Sized { - fn try_add(self, other: Self) -> Option; -} - -impl SafeAdd for NonZeroInt { - fn try_add(self, other: Self) -> Option { - let lhs: i64 = self.into(); - let rhs: i64 = other.into(); - NonZeroInt::try_from(lhs.checked_add(rhs)?).ok() - } -} - -impl SafeAdd for PositiveCoin { - fn try_add(self, other: Self) -> Option { - let lhs: u64 = self.into(); - let rhs: u64 = other.into(); - PositiveCoin::try_from(lhs.checked_add(rhs)?).ok() - } -} - -#[cfg(test)] -mod tests { - use std::str::FromStr as _; - - use super::*; - use pallas_primitives::conway::Value; - - #[test] - fn test_add_values_coin_only() { - let value_a = Value::Coin(100); - let value_b = Value::Coin(200); - - let result = aggregate_values(vec![value_a, value_b]); - - assert_eq!(result, Value::Coin(300)); - } - - #[test] - fn test_add_values_same_asset() { - let policy_id = - Hash::<28>::from_str("bb4bc871e84078de932d392186dd3093b8de93505178d88d89b7ac98") - .unwrap(); - - let asset_name = "pepe".as_bytes().to_vec(); - - let value_a = Value::Multiasset( - 100, - NonEmptyKeyValuePairs::Def(vec![( - policy_id, - NonEmptyKeyValuePairs::Def(vec![( - asset_name.clone().into(), - 50.try_into().unwrap(), - )]), - )]), - ); - let value_b = Value::Multiasset( - 200, - NonEmptyKeyValuePairs::Def(vec![( - policy_id, - NonEmptyKeyValuePairs::Def(vec![( - asset_name.clone().into(), - 30.try_into().unwrap(), - )]), - )]), - ); - - let result = aggregate_values(vec![value_a, value_b]); - - assert_eq!( - result, - Value::Multiasset( - 300, - NonEmptyKeyValuePairs::Def(vec![( - policy_id, - NonEmptyKeyValuePairs::Def(vec![( - asset_name.clone().into(), - 80.try_into().unwrap() - )]), - )]), - ) - ); - } -} diff --git a/balius-sdk/src/txbuilder/build.rs b/balius-sdk/src/txbuilder/build.rs deleted file mode 100644 index ab6fe88..0000000 --- a/balius-sdk/src/txbuilder/build.rs +++ /dev/null @@ -1,201 +0,0 @@ -use pallas_traverse::MultiEraValue; -use std::sync::Arc; -use std::{collections::HashMap, ops::Deref as _}; - -use super::{ - asset_math, primitives, BuildContext, BuildError, Ledger, PParams, TxExpr, TxoRef, UtxoPattern, - UtxoSet, -}; - -impl BuildContext { - pub fn mint_redeemer_index(&self, policy: primitives::ScriptHash) -> Result { - if let Some(tx_body) = &self.tx_body { - let mut out: Vec<_> = tx_body - .mint - .iter() - .flat_map(|x| x.iter()) - .map(|(p, _)| *p) - .collect(); - - out.sort(); - out.dedup(); - - if let Some(index) = out.iter().position(|p| *p == policy) { - return Ok(index as u32); - } - } - - Err(BuildError::RedeemerTargetMissing) - } - - pub fn eval_ex_units( - &self, - _script: primitives::ScriptHash, - _data: &primitives::PlutusData, - ) -> primitives::ExUnits { - // TODO - primitives::ExUnits { mem: 8, steps: 8 } - } -} - -pub(crate) struct ExtLedgerFacade; - -impl crate::txbuilder::Ledger for ExtLedgerFacade { - fn read_utxos(&self, refs: &[TxoRef]) -> Result { - let refs: Vec<_> = refs.iter().cloned().map(Into::into).collect(); - let x = crate::wit::balius::app::ledger::read_utxos(&refs)?; - - let x: Vec<_> = x - .into_iter() - .map(|x| (TxoRef::from(x.ref_), x.body.to_vec())) - .collect(); - - Ok(UtxoSet::from_iter(x)) - } - - fn search_utxos(&self, pattern: &UtxoPattern) -> Result { - let pattern = pattern.clone().into(); - let mut utxos = HashMap::new(); - let max_items = 32; - let mut utxo_page = Some(crate::wit::balius::app::ledger::search_utxos( - &pattern, None, max_items, - )?); - while let Some(page) = utxo_page.take() { - for utxo in page.utxos { - utxos.insert(utxo.ref_.into(), utxo.body); - } - if let Some(next) = page.next_token { - utxo_page = Some(crate::wit::balius::app::ledger::search_utxos( - &pattern, - Some(&next), - max_items, - )?); - } - } - Ok(utxos.into()) - } - - fn read_params(&self) -> Result { - let bytes = crate::wit::balius::app::ledger::read_params()?; - - serde_json::from_slice(&bytes) - .map_err(|_| BuildError::LedgerError("failed to parse params json".to_string())) - } -} - -pub fn build(mut tx: T, ledger: L) -> Result -where - T: TxExpr, - L: Ledger + 'static, -{ - let mut ctx = BuildContext { - network: primitives::NetworkId::Testnet, - pparams: ledger.read_params()?, - total_input: primitives::Value::Coin(0), - spent_output: primitives::Value::Coin(0), - estimated_fee: 0, - ledger: Arc::new(Box::new(ledger)), - tx_body: None, - parent_output: None, - }; - - // Build the raw transaction, so we have the info needed to estimate fees and - // compute change. - let body = tx.eval_body(&ctx)?; - - let input_refs: Vec<_> = body - .inputs - .iter() - .map(|i| TxoRef { - hash: i.transaction_id, - index: i.index, - }) - .collect(); - let utxos = ctx.ledger.read_utxos(&input_refs)?; - ctx.total_input = - asset_math::aggregate_values(utxos.txos().map(|txo| input_into_conway(&txo.value()))); - if let Some(mint) = &body.mint { - ctx.total_input = asset_math::add_mint(&ctx.total_input, mint)?; - } - ctx.spent_output = asset_math::aggregate_values(body.outputs.iter().map(output_into_conway)); - // TODO: estimate the fee - ctx.estimated_fee = 2_000_000; - - // Now that we know the inputs/outputs/fee, build the "final" (unsigned)tx - let body = tx.eval_body(&ctx)?; - ctx.tx_body = Some(body); - for _ in 0..3 { - let body = tx.eval_body(&ctx)?; - ctx.tx_body = Some(body); - } - - let wit = tx.eval_witness_set(&ctx).unwrap(); - - let tx = primitives::Tx { - transaction_body: ctx.tx_body.take().unwrap(), - transaction_witness_set: wit, - auxiliary_data: pallas_codec::utils::Nullable::Null, - success: true, - }; - - Ok(tx) -} - -// TODO: this belongs in pallas-traverse -// https://github.com/txpipe/pallas/pull/545 -fn input_into_conway(value: &MultiEraValue) -> primitives::Value { - use pallas_primitives::{alonzo, conway}; - match value { - MultiEraValue::Byron(x) => conway::Value::Coin(*x), - MultiEraValue::AlonzoCompatible(x) => match x.deref() { - alonzo::Value::Coin(x) => conway::Value::Coin(*x), - alonzo::Value::Multiasset(x, assets) => { - let coin = *x; - let assets = assets - .iter() - .filter_map(|(k, v)| { - let v: Vec<(conway::Bytes, conway::PositiveCoin)> = v - .iter() - .filter_map(|(k, v)| Some((k.clone(), (*v).try_into().ok()?))) - .collect(); - Some((*k, conway::NonEmptyKeyValuePairs::from_vec(v)?)) - }) - .collect(); - if let Some(assets) = conway::NonEmptyKeyValuePairs::from_vec(assets) { - conway::Value::Multiasset(coin, assets) - } else { - conway::Value::Coin(coin) - } - } - }, - MultiEraValue::Conway(x) => x.deref().clone(), - _ => panic!("unrecognized value"), - } -} - -fn output_into_conway(output: &primitives::TransactionOutput) -> primitives::Value { - use pallas_primitives::{alonzo, conway}; - match output { - primitives::TransactionOutput::Legacy(o) => match &o.amount { - alonzo::Value::Coin(c) => primitives::Value::Coin(*c), - alonzo::Value::Multiasset(c, assets) => { - let assets = assets - .iter() - .filter_map(|(k, v)| { - let v: Vec<(conway::Bytes, conway::PositiveCoin)> = v - .iter() - .filter_map(|(k, v)| Some((k.clone(), (*v).try_into().ok()?))) - .collect(); - Some((*k, conway::NonEmptyKeyValuePairs::from_vec(v)?)) - }) - .collect(); - if let Some(assets) = conway::NonEmptyKeyValuePairs::from_vec(assets) { - primitives::Value::Multiasset(*c, assets) - } else { - primitives::Value::Coin(*c) - } - } - }, - primitives::TransactionOutput::PostAlonzo(o) => o.value.clone(), - } -} diff --git a/balius-sdk/src/txbuilder/dsl.rs b/balius-sdk/src/txbuilder/dsl.rs deleted file mode 100644 index d912bca..0000000 --- a/balius-sdk/src/txbuilder/dsl.rs +++ /dev/null @@ -1,893 +0,0 @@ -use pallas_primitives::conway; -use pallas_traverse::MultiEraOutput; -use serde::{Deserialize, Serialize}; -use serde_json::from_str; -use serde_with::{serde_as, DisplayFromStr}; -use std::collections::{HashMap, HashSet}; - -use super::*; - -pub type Hash = pallas_crypto::hash::Hash; -pub type Address = pallas_addresses::Address; -pub type Value = pallas_primitives::conway::Value; -pub type Bytes = pallas_codec::utils::Bytes; -pub type KeyValuePairs = pallas_codec::utils::KeyValuePairs; -pub type NonEmptyKeyValuePairs = pallas_codec::utils::NonEmptyKeyValuePairs; -pub type NonEmptySet = pallas_codec::utils::NonEmptySet; - -pub type Cbor = Vec; - -#[derive(Debug, Clone, Default)] -pub struct UtxoSet(HashMap); - -impl UtxoSet { - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - - pub fn iter(&self) -> impl Iterator)> { - self.0.iter().map(|(k, v)| { - ( - k, - MultiEraOutput::decode(pallas_traverse::Era::Conway, v).unwrap(), - ) - }) - } - - pub fn refs(&self) -> impl Iterator { - self.0.keys() - } - - pub fn txos(&self) -> impl Iterator> { - self.0 - .values() - .map(|v| MultiEraOutput::decode(pallas_traverse::Era::Conway, v).unwrap()) - } -} - -impl FromIterator<(TxoRef, Cbor)> for UtxoSet { - fn from_iter>(iter: T) -> Self { - Self(HashMap::from_iter(iter)) - } -} - -impl From> for UtxoSet { - fn from(value: HashMap) -> Self { - UtxoSet(value) - } -} - -#[derive(Clone, Default, Serialize, Deserialize)] -pub struct UtxoPattern { - pub address: Option, - pub asset: Option, -} - -impl From for crate::wit::balius::app::ledger::UtxoPattern { - fn from(value: UtxoPattern) -> Self { - Self { - address: value.address.map(Into::into), - asset: value.asset.map(Into::into), - } - } -} - -#[derive(Clone, Serialize, Deserialize)] -pub struct AddressPattern { - pub exact_address: Vec, -} - -impl From for crate::wit::balius::app::ledger::AddressPattern { - fn from(value: AddressPattern) -> Self { - Self { - exact_address: value.exact_address, - } - } -} - -#[derive(Clone, Serialize, Deserialize)] -pub struct AssetPattern { - pub policy: Vec, - pub name: Option>, -} - -impl From for crate::wit::balius::app::ledger::AssetPattern { - fn from(value: AssetPattern) -> Self { - Self { - policy: value.policy, - name: value.name, - } - } -} - -pub trait InputExpr: 'static + Send + Sync { - fn eval(&self, ctx: &BuildContext) -> Result, BuildError>; -} - -#[derive(Clone, Serialize, Deserialize)] -pub enum UtxoSource { - Refs(Vec), - Search(UtxoPattern), -} - -impl UtxoSource { - pub fn resolve(&self, ctx: &BuildContext) -> Result { - match self { - Self::Refs(refs) => ctx.ledger.read_utxos(refs), - Self::Search(utxo_pattern) => ctx.ledger.search_utxos(utxo_pattern), - } - } -} - -#[serde_as] -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] -pub struct ReferenceScript { - pub ref_txo: conway::TransactionInput, - pub hash: Hash<28>, - #[serde_as(as = "DisplayFromStr")] - pub address: Address, -} - -impl InputExpr for ReferenceScript { - fn eval(&self, _: &dsl::BuildContext) -> Result, BuildError> { - Ok(vec![self.ref_txo.clone()]) - } -} - -#[derive(PartialEq, Eq, Debug, Clone, Hash, Serialize, Deserialize)] -pub struct AssetPolicyId(Hash<28>); - -impl AssetPolicyId { - pub fn new(hash: Hash<28>) -> Self { - Self(hash) - } -} - -impl From> for AssetPolicyId { - fn from(value: Hash<28>) -> Self { - Self(value) - } -} - -impl Into> for AssetPolicyId { - fn into(self) -> Hash<28> { - self.0 - } -} - -impl TryFrom<&str> for AssetPolicyId { - type Error = BuildError; - - fn try_from(value: &str) -> Result { - let hash = as std::str::FromStr>::from_str(value) - .map_err(|_| BuildError::MalformedAssetPolicyIdHex)?; - Ok(AssetPolicyId(hash)) - } -} - -impl std::ops::Deref for AssetPolicyId { - type Target = Hash<28>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl std::fmt::Display for AssetPolicyId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", hex::encode(self.0)) - } -} - -#[derive(PartialEq, Eq, Debug, Clone, Hash, Serialize, Deserialize)] -pub struct AssetName(Bytes); - -impl AssetName { - pub fn new(name: Bytes) -> Result { - if name.len() > 32 { - panic!("Asset name too long"); - } - - Ok(Self(name)) - } -} - -impl TryFrom> for AssetName { - type Error = BuildError; - - fn try_from(value: Vec) -> Result { - Self::new(value.into()) - } -} - -impl TryFrom<&str> for AssetName { - type Error = BuildError; - - fn try_from(value: &str) -> Result { - Self::new(value.as_bytes().to_vec().into()) - } -} - -impl From for Bytes { - fn from(value: AssetName) -> Self { - value.0 - } -} - -impl std::ops::Deref for AssetName { - type Target = Bytes; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Hash, Clone)] -pub struct TxoRef { - pub hash: Hash<32>, - pub index: u64, -} - -impl std::str::FromStr for TxoRef { - type Err = BuildError; - - fn from_str(s: &str) -> Result { - let (hash, index) = s.split_once("#").ok_or(BuildError::MalformedTxoRef)?; - let hash = Hash::from_str(hash).map_err(|_| BuildError::MalformedTxoRef)?; - let index = index.parse().map_err(|_| BuildError::MalformedTxoRef)?; - Ok(TxoRef::new(hash, index)) - } -} - -impl From for TxoRef { - fn from(value: crate::wit::balius::app::ledger::TxoRef) -> Self { - Self::new(Hash::from(value.tx_hash.as_slice()), value.tx_index as u64) - } -} - -impl Into for TxoRef { - fn into(self) -> crate::wit::balius::app::ledger::TxoRef { - crate::wit::balius::app::ledger::TxoRef { - tx_hash: self.hash.to_vec(), - tx_index: self.index as u32, - } - } -} - -impl TxoRef { - pub fn new(hash: Hash<32>, index: u64) -> Self { - Self { hash, index } - } -} - -impl dsl::InputExpr for TxoRef { - fn eval(&self, _: &BuildContext) -> Result, BuildError> { - Ok(vec![self.into()]) - } -} - -impl Into for &TxoRef { - fn into(self) -> conway::TransactionInput { - conway::TransactionInput { - transaction_id: self.hash.into(), - index: self.index, - } - } -} - -impl InputExpr for UtxoSource { - fn eval(&self, ctx: &BuildContext) -> Result, BuildError> { - let out = self.resolve(ctx)?.refs().map(|i| i.into()).collect(); - - Ok(out) - } -} - -pub trait ValueExpr: 'static + Send + Sync { - fn eval(&self, ctx: &BuildContext) -> Result; - - fn eval_as_mint(&self, ctx: &BuildContext) -> Result { - let value = self.eval(ctx)?; - - match value { - conway::Value::Multiasset(_, assets) => asset_math::multiasset_coin_to_mint(assets), - conway::Value::Coin(_) => Err(BuildError::Conflicting), - } - } - - fn eval_as_burn(&self, ctx: &BuildContext) -> Result { - let value = self.eval(ctx)?; - - match value { - conway::Value::Multiasset(_, assets) => asset_math::multiasset_coin_to_burn(assets), - conway::Value::Coin(_) => Err(BuildError::Conflicting), - } - } -} - -impl ValueExpr for u64 { - fn eval(&self, _ctx: &BuildContext) -> Result { - Ok(conway::Value::Coin(*self)) - } -} - -impl ValueExpr for F -where - F: Fn(&BuildContext) -> Result + 'static + Send + Sync, -{ - fn eval(&self, ctx: &BuildContext) -> Result { - self(ctx) - } -} - -impl ValueExpr for Option { - fn eval(&self, ctx: &BuildContext) -> Result { - match self { - Some(v) => v.eval(ctx), - None => Err(BuildError::Incomplete), - } - } -} - -// calculate min utxo lovelace according to spec -// https://cips.cardano.org/cip/CIP-55 -pub struct MinUtxoLovelace; - -impl ValueExpr for MinUtxoLovelace { - fn eval(&self, ctx: &BuildContext) -> Result { - let parent = match &ctx.parent_output { - Some(x) => x, - None => return Ok(conway::Value::Coin(0)), - }; - - let serialized = pallas_codec::minicbor::to_vec(parent).unwrap(); - let min_lovelace = (160u64 + serialized.len() as u64) * ctx.pparams.coins_per_utxo_byte; - let current_value = match parent { - conway::PseudoTransactionOutput::PostAlonzo(x) => &x.value, - _ => unimplemented!(), - }; - - let current_lovelace = asset_math::value_coin(current_value); - - if current_lovelace >= min_lovelace { - return Ok(current_value.clone()); - } - - let optimized = asset_math::value_saturating_add_coin( - current_value.clone(), - (min_lovelace - current_lovelace) as i64, - ); - - Ok(optimized) - } -} - -impl ValueExpr for Box { - fn eval(&self, ctx: &BuildContext) -> Result { - (**self).eval(ctx) - } -} - -impl ValueExpr for Vec { - fn eval(&self, ctx: &BuildContext) -> Result { - let values = self - .iter() - .map(|v| v.eval(ctx)) - .collect::, _>>()?; - - Ok(asset_math::aggregate_values(values)) - } -} - -pub trait AddressExpr: 'static + Send + Sync { - fn eval(&self, ctx: &BuildContext) -> Result; -} - -impl AddressExpr for &'static str { - fn eval(&self, _ctx: &BuildContext) -> Result { - Address::from_bech32(*self).map_err(|_| BuildError::MalformedAddress) - } -} - -impl AddressExpr for String { - fn eval(&self, _ctx: &BuildContext) -> Result { - Address::from_bech32(self).map_err(|_| BuildError::MalformedAddress) - } -} - -impl AddressExpr for Address { - fn eval(&self, _ctx: &BuildContext) -> Result { - Ok(self.clone()) - } -} - -impl AddressExpr for Box { - fn eval(&self, ctx: &BuildContext) -> Result { - (**self).eval(ctx) - } -} - -impl AddressExpr for Option { - fn eval(&self, ctx: &BuildContext) -> Result { - match self { - Some(v) => v.eval(ctx), - None => Err(BuildError::Incomplete), - } - } -} - -impl AddressExpr for F -where - F: Fn(&BuildContext) -> Result + 'static + Send + Sync, -{ - fn eval(&self, ctx: &BuildContext) -> Result { - self(ctx) - } -} - -pub trait OutputExpr: 'static + Send + Sync { - fn eval(&mut self, ctx: &BuildContext) -> Result; -} - -pub struct ChangeAddress(pub UtxoSource); - -impl AddressExpr for ChangeAddress { - fn eval(&self, ctx: &BuildContext) -> Result { - let utxo_set = &self.0.resolve(ctx)?; - - if utxo_set.is_empty() { - return Err(BuildError::EmptyUtxoSet); - } - - let addresses: HashSet<_> = utxo_set - .txos() - .map(|x| x.address()) - .collect::, _>>() - .map_err(|_| BuildError::UtxoDecode)?; - - if addresses.len() > 1 { - return Err(BuildError::Conflicting); - } - - Ok(addresses.into_iter().next().unwrap()) - } -} - -pub struct TotalChange; - -impl ValueExpr for TotalChange { - fn eval(&self, ctx: &BuildContext) -> Result { - let change = asset_math::subtract_value(&ctx.total_input, &ctx.spent_output)?; - let fee = ctx.estimated_fee; - let diff = asset_math::value_saturating_add_coin(change, -(fee as i64)); - Ok(diff) - } -} - -pub struct FeeChangeReturn(pub UtxoSource); - -impl OutputExpr for FeeChangeReturn { - fn eval(&mut self, ctx: &BuildContext) -> Result { - OutputBuilder::new() - .address(ChangeAddress(self.0.clone())) - .with_value(TotalChange) - .eval(ctx) - } -} - -pub trait PlutusDataExpr: 'static + Send + Sync { - fn eval(&self, ctx: &BuildContext) -> Result; -} - -impl PlutusDataExpr for conway::PlutusData { - fn eval(&self, _ctx: &BuildContext) -> Result { - Ok(self.clone()) - } -} - -impl PlutusDataExpr for F -where - F: Fn(&BuildContext) -> Result + 'static + Send + Sync, -{ - fn eval(&self, ctx: &BuildContext) -> Result { - self(ctx) - } -} - -impl PlutusDataExpr for Box { - fn eval(&self, ctx: &BuildContext) -> Result { - (**self).eval(ctx) - } -} - -impl PlutusDataExpr for () { - fn eval(&self, _ctx: &BuildContext) -> Result { - Ok(conway::PlutusData::Constr(conway::Constr { - tag: 121, - any_constructor: None, - fields: conway::MaybeIndefArray::Def(vec![]), - })) - } -} - -pub trait MintExpr: 'static + Send + Sync { - fn eval(&self, ctx: &BuildContext) -> Result, BuildError>; - fn eval_redeemer(&self, ctx: &BuildContext) -> Result, BuildError>; -} - -#[derive(Default)] -pub struct MintBuilder { - pub assets: Vec>, - pub burn: Vec>, - pub redeemer: Option>, -} - -impl MintBuilder { - pub fn new() -> Self { - Self::default() - } - - pub fn with_asset(mut self, asset: impl ValueExpr) -> Self { - self.assets.push(Box::new(asset)); - self - } - - pub fn with_burn(mut self, burn: impl ValueExpr) -> Self { - self.burn.push(Box::new(burn)); - self - } - - pub fn using_redeemer(mut self, redeemer: impl PlutusDataExpr) -> Self { - self.redeemer = Some(Box::new(redeemer)); - self - } -} - -impl MintExpr for MintBuilder { - fn eval(&self, ctx: &BuildContext) -> Result, BuildError> { - let out = HashMap::new(); - - let out = self.assets.iter().try_fold(out, |mut acc, v| { - let v = v.eval_as_mint(ctx)?; - asset_math::fold_multiassets(&mut acc, v); - Result::<_, BuildError>::Ok(acc) - })?; - - let out = self.burn.iter().try_fold(out, |mut acc, v| { - let v = v.eval_as_burn(ctx)?; - asset_math::fold_multiassets(&mut acc, v); - Result::<_, BuildError>::Ok(acc) - })?; - - let mint: Vec<_> = out - .into_iter() - .filter_map(|(policy, assets)| { - let assets = assets.into_iter().collect(); - Some((policy, NonEmptyKeyValuePairs::from_vec(assets)?)) - }) - .collect(); - - Ok(NonEmptyKeyValuePairs::from_vec(mint)) - } - - fn eval_redeemer(&self, ctx: &BuildContext) -> Result, BuildError> { - let Some(mint) = self.eval(ctx)? else { - return Ok(None); - }; - - if mint.is_empty() { - return Err(BuildError::Incomplete); - } - - if mint.len() > 1 { - return Err(BuildError::Conflicting); - } - - let (policy, _) = mint.iter().next().unwrap(); - - let data = self - .redeemer - .as_ref() - .ok_or(BuildError::Incomplete)? - .eval(ctx)?; - - let out = conway::Redeemer { - tag: conway::RedeemerTag::Mint, - index: ctx.mint_redeemer_index(*policy)?, - ex_units: ctx.eval_ex_units(*policy, &data), - data, - }; - - Ok(Some(out)) - } -} - -pub trait ScriptExpr: 'static + Send + Sync { - fn eval(&self, ctx: &BuildContext) -> Result; -} - -impl ScriptExpr for conway::ScriptRef { - fn eval(&self, _ctx: &BuildContext) -> Result { - Ok(self.clone()) - } -} - -impl ScriptExpr for conway::PlutusScript<3> { - fn eval(&self, _ctx: &BuildContext) -> Result { - Ok(conway::ScriptRef::PlutusV3Script(self.clone())) - } -} - -#[derive(Default)] -pub struct OutputBuilder { - pub previous: Option, - pub address: Option>, - pub values: Vec>, - pub script: Option>, - // TODO: inline / hash datum -} - -impl OutputBuilder { - pub fn new() -> Self { - Self::default() - } - - pub fn address(mut self, address: impl AddressExpr + 'static) -> Self { - self.address = Some(Box::new(address)); - self - } - - pub fn with_value(mut self, value: impl ValueExpr + 'static) -> Self { - self.values.push(Box::new(value)); - self - } - - pub fn with_script(mut self, script: impl ScriptExpr + 'static) -> Self { - self.script = Some(Box::new(script)); - self - } -} - -impl OutputExpr for OutputBuilder { - fn eval(&mut self, ctx: &BuildContext) -> Result { - let ctx = match &self.previous { - Some(x) => &ctx.with_parent_output(x.clone()), - None => ctx, - }; - - let value = self.values.eval(ctx)?; - - let address = self.address.eval(ctx)?.to_vec().into(); - - let script_ref = self - .script - .as_ref() - .map(|s| s.eval(ctx)) - .transpose()? - .map(pallas_codec::utils::CborWrap); - - let output = conway::TransactionOutput::PostAlonzo(conway::PostAlonzoTransactionOutput { - value, - address, - script_ref, - datum_option: None, // TODO - }); - - self.previous = Some(output.clone()); - - Ok(output) - } -} - -pub trait TxExpr: 'static + Send + Sync { - fn eval_body(&mut self, ctx: &BuildContext) -> Result; - fn eval_witness_set(&mut self, ctx: &BuildContext) -> Result; -} - -impl TxExpr for &'static mut T { - fn eval_body(&mut self, ctx: &BuildContext) -> Result { - (**self).eval_body(ctx) - } - - fn eval_witness_set(&mut self, ctx: &BuildContext) -> Result { - (**self).eval_witness_set(ctx) - } -} - -impl TxExpr for Box { - fn eval_body(&mut self, ctx: &BuildContext) -> Result { - (**self).eval_body(ctx) - } - - fn eval_witness_set(&mut self, ctx: &BuildContext) -> Result { - (**self).eval_witness_set(ctx) - } -} - -#[derive(Default)] -pub struct TxBuilder { - pub reference_inputs: Vec>, - pub inputs: Vec>, - pub outputs: Vec>, - pub mint: Vec>, - pub fee: Option, - // pub valid_from_slot: Option, - // pub invalid_from_slot: Option, - // pub network_id: Option, - // pub collateral_inputs: Option>, - // pub collateral_output: Option, - // pub disclosed_signers: Option>, - // pub scripts: Option>, - // pub datums: Option>, - // pub redeemers: Option, - // pub script_data_hash: Option, - // pub signature_amount_override: Option, - // #[serde_as(as = "Option")] - // pub change_address: Option
, - // pub certificates: TODO - // pub withdrawals: TODO - // pub updates: TODO - // pub auxiliary_data: TODO - // pub phase_2_valid: TODO -} - -impl TxBuilder { - pub fn new() -> Self { - Self::default() - } - - pub fn with_reference_input(mut self, input: impl InputExpr) -> Self { - self.reference_inputs.push(Box::new(input)); - self - } - - pub fn with_input(mut self, input: impl InputExpr) -> Self { - self.inputs.push(Box::new(input)); - self - } - - pub fn with_output(mut self, output: impl OutputExpr) -> Self { - self.outputs.push(Box::new(output)); - self - } - - pub fn with_mint(mut self, mint: impl MintExpr) -> Self { - self.mint.push(Box::new(mint)); - self - } - - pub fn with_fee(mut self, fee: u64) -> Self { - self.fee = Some(fee); - self - } -} - -impl TxExpr for TxBuilder { - fn eval_body(&mut self, ctx: &BuildContext) -> Result { - let out = conway::TransactionBody { - inputs: self - .inputs - .iter() - .map(|i| i.eval(ctx)) - .collect::, _>>()? - .into_iter() - .flatten() - .collect::>() - .into(), - outputs: self - .outputs - .iter_mut() - .map(|o| o.eval(ctx)) - .collect::, _>>()?, - fee: ctx.estimated_fee, - ttl: None, - validity_interval_start: None, - certificates: None, - withdrawals: None, - auxiliary_data_hash: None, - mint: { - let mints = self - .mint - .iter() - .map(|m| m.eval(ctx)) - .collect::, _>>()? - .into_iter() - .filter_map(|m| m); - - asset_math::aggregate_assets(mints) - }, - script_data_hash: None, - collateral: None, - required_signers: None, - network_id: None, - collateral_return: None, - total_collateral: None, - reference_inputs: { - let refs: Vec<_> = self - .reference_inputs - .iter() - .map(|i| i.eval(ctx)) - .collect::, _>>()? - .into_iter() - .flatten() - .collect(); - - NonEmptySet::from_vec(refs) - }, - voting_procedures: None, - proposal_procedures: None, - treasury_value: None, - donation: None, - }; - - Ok(out) - } - - fn eval_witness_set(&mut self, ctx: &BuildContext) -> Result { - let out = conway::WitnessSet { - redeemer: { - let redeemers: Vec<_> = self - .mint - .iter() - .map(|m| m.eval_redeemer(ctx)) - .collect::, _>>()? - .into_iter() - .filter_map(|r| r) - .collect(); - - if redeemers.is_empty() { - None - } else { - Some(conway::Redeemers::List(conway::MaybeIndefArray::Def( - redeemers, - ))) - } - }, - vkeywitness: None, - native_script: None, - bootstrap_witness: None, - plutus_v1_script: None, - plutus_data: None, - plutus_v2_script: None, - plutus_v3_script: None, - }; - - Ok(out) - } -} - -#[macro_export] -macro_rules! define_asset_class { - ($struct_name:ident, $policy:expr) => { - #[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] - pub struct $struct_name($crate::txbuilder::Bytes, u64); - - impl $struct_name { - pub fn value(name: $crate::txbuilder::AssetName, quantity: u64) -> Self { - Self(name.into(), quantity) - } - } - - impl $crate::txbuilder::ValueExpr for $struct_name { - fn eval( - &self, - _: &$crate::txbuilder::BuildContext, - ) -> std::result::Result<$crate::txbuilder::Value, $crate::txbuilder::BuildError> { - let policy = $crate::txbuilder::Hash::from(*$policy); - let name = $crate::txbuilder::Bytes::from(self.0.clone()); - let Ok(amount) = self.1.try_into() else { - return Ok($crate::txbuilder::Value::Coin(0)); - }; - let asset = $crate::txbuilder::NonEmptyKeyValuePairs::Def(vec![(name, amount)]); - let val = $crate::txbuilder::Value::Multiasset( - 0, - $crate::txbuilder::NonEmptyKeyValuePairs::Def(vec![(policy, asset)]), - ); - - Ok(val) - } - } - }; -} - -define_asset_class!(MyAssetClass, b"abcabcababcabcababcabcababca"); diff --git a/balius-sdk/src/txbuilder/mod.rs b/balius-sdk/src/txbuilder/mod.rs deleted file mode 100644 index 317703a..0000000 --- a/balius-sdk/src/txbuilder/mod.rs +++ /dev/null @@ -1,87 +0,0 @@ -pub(crate) mod asset_math; -pub mod plutus; - -#[derive(Debug, thiserror::Error)] -pub enum BuildError { - #[error("Builder is incomplete")] - Incomplete, - #[error("Conflicting requirement")] - Conflicting, - #[error("UTxO decoding failed")] - UtxoDecode, - #[error("UTxO set is empty")] - EmptyUtxoSet, - #[error("Transaction has no inputs")] - MalformedScript, - #[error("Could not decode address")] - MalformedAddress, - #[error("Could not decode datum bytes")] - MalformedDatum, - #[error("Invalid bytes length for datum hash")] - MalformedDatumHash, - #[error("Input/policy pointed to by redeemer not found in tx")] - RedeemerTargetMissing, - #[error("Invalid network ID")] - InvalidNetworkId, - #[error("Corrupted transaction bytes in built transaction")] - CorruptedTxBytes, - #[error("Public key for private key is malformed")] - MalformedKey, - #[error("Asset name must be 32 bytes or less")] - AssetNameTooLong, - #[error("Asset value must be less than 9223372036854775807")] - AssetValueTooHigh, - #[error("Total outputs of this transaction are greater than total inputs")] - OutputsTooHigh, - #[error("Invalid asset policy id hex")] - MalformedAssetPolicyIdHex, - #[error("Malformed TxoRef")] - MalformedTxoRef, - #[error("Ledger error: {0}")] - LedgerError(String), -} - -impl From for BuildError { - fn from(err: crate::wit::balius::app::ledger::LedgerError) -> Self { - BuildError::LedgerError(err.to_string()) - } -} - -use std::sync::Arc; - -pub use pallas_codec as codec; -pub use pallas_primitives::conway as primitives; -pub use utxorpc_spec::utxorpc::v1alpha::cardano::PParams; - -pub trait Ledger { - fn read_utxos(&self, refs: &[dsl::TxoRef]) -> Result; - fn search_utxos(&self, pattern: &dsl::UtxoPattern) -> Result; - fn read_params(&self) -> Result; -} - -#[derive(Clone)] -pub struct BuildContext { - pub network: primitives::NetworkId, - pub pparams: PParams, - pub total_input: primitives::Value, - pub spent_output: primitives::Value, - pub estimated_fee: u64, - pub ledger: Arc>, - - pub tx_body: Option, - pub parent_output: Option, -} - -impl BuildContext { - pub fn with_parent_output(&self, output: primitives::TransactionOutput) -> Self { - let mut ctx = self.clone(); - ctx.parent_output = Some(output); - ctx - } -} - -mod build; -mod dsl; - -pub use build::*; -pub use dsl::*; diff --git a/balius-sdk/src/txbuilder/plutus.rs b/balius-sdk/src/txbuilder/plutus.rs deleted file mode 100644 index 0a1f111..0000000 --- a/balius-sdk/src/txbuilder/plutus.rs +++ /dev/null @@ -1,51 +0,0 @@ -//use crate::txbuilder::primitives::{BoundedBytes, PlutusData}; - -pub use pallas_codec::utils::Int; -pub use pallas_primitives::{BigInt, BoundedBytes, Constr, MaybeIndefArray, PlutusData}; - -pub trait IntoData { - fn into_data(&self) -> PlutusData; -} - -#[macro_export] -macro_rules! constr { - ($tag:expr, $($field:expr),*) => { - { - use $crate::txbuilder::plutus::{Constr, PlutusData, MaybeIndefArray}; - - let inner = Constr { - tag: 121 + $tag, - any_constructor: None, - fields: MaybeIndefArray::Def(vec![$($field.into_data()),*]), - }; - - PlutusData::Constr(inner) - } - }; -} - -impl IntoData for [u8; N] { - fn into_data(&self) -> PlutusData { - PlutusData::BoundedBytes(BoundedBytes::from(self.to_vec())) - } -} - -impl IntoData for Vec { - fn into_data(&self) -> PlutusData { - PlutusData::BoundedBytes(BoundedBytes::from(self.clone())) - } -} - -impl IntoData for u64 { - fn into_data(&self) -> PlutusData { - PlutusData::BigInt(BigInt::Int(Int::from(*self as i64))) - } -} - -mod test { - use super::*; - - fn construct_constr() { - let x = constr!(0, b"abc", vec![1, 2, 3]); - } -}