Skip to main content

balius_sdk/txbuilder/
dsl.rs

1use pallas_primitives::conway;
2use pallas_traverse::MultiEraOutput;
3use serde::{Deserialize, Serialize};
4use serde_with::{serde_as, DisplayFromStr};
5use std::collections::{HashMap, HashSet};
6
7use super::*;
8
9pub type Hash<const N: usize> = pallas_crypto::hash::Hash<N>;
10pub type Address = pallas_addresses::Address;
11pub type Value = pallas_primitives::conway::Value;
12pub type Bytes = pallas_codec::utils::Bytes;
13pub type KeyValuePairs<K, V> = pallas_codec::utils::KeyValuePairs<K, V>;
14pub type NonEmptyKeyValuePairs<K, V> = pallas_codec::utils::NonEmptyKeyValuePairs<K, V>;
15pub type NonEmptySet<T> = pallas_codec::utils::NonEmptySet<T>;
16
17pub type Cbor = Vec<u8>;
18
19#[derive(Debug, Clone, Default)]
20pub struct UtxoSet(HashMap<TxoRef, Cbor>);
21
22impl UtxoSet {
23    pub fn is_empty(&self) -> bool {
24        self.0.is_empty()
25    }
26
27    pub fn iter(&self) -> impl Iterator<Item = (&TxoRef, MultiEraOutput<'_>)> {
28        self.0.iter().map(|(k, v)| {
29            (
30                k,
31                MultiEraOutput::decode(pallas_traverse::Era::Conway, v).unwrap(),
32            )
33        })
34    }
35
36    pub fn refs(&self) -> impl Iterator<Item = &TxoRef> {
37        self.0.keys()
38    }
39
40    pub fn txos(&self) -> impl Iterator<Item = MultiEraOutput<'_>> {
41        self.0
42            .values()
43            .map(|v| MultiEraOutput::decode(pallas_traverse::Era::Conway, v).unwrap())
44    }
45}
46
47impl FromIterator<(TxoRef, Cbor)> for UtxoSet {
48    fn from_iter<T: IntoIterator<Item = (TxoRef, Cbor)>>(iter: T) -> Self {
49        Self(HashMap::from_iter(iter))
50    }
51}
52
53impl From<HashMap<TxoRef, Cbor>> for UtxoSet {
54    fn from(value: HashMap<TxoRef, Cbor>) -> Self {
55        UtxoSet(value)
56    }
57}
58
59#[derive(Clone, Default, Serialize, Deserialize)]
60pub struct UtxoPattern {
61    pub address: Option<AddressPattern>,
62    pub asset: Option<AssetPattern>,
63}
64
65impl From<UtxoPattern> for crate::wit::balius::app::ledger::UtxoPattern {
66    fn from(value: UtxoPattern) -> Self {
67        Self {
68            address: value.address.map(Into::into),
69            asset: value.asset.map(Into::into),
70        }
71    }
72}
73
74#[derive(Clone, Serialize, Deserialize)]
75pub struct AddressPattern {
76    pub exact_address: Vec<u8>,
77}
78
79impl From<AddressPattern> for crate::wit::balius::app::ledger::AddressPattern {
80    fn from(value: AddressPattern) -> Self {
81        Self {
82            exact_address: value.exact_address,
83        }
84    }
85}
86
87#[derive(Clone, Serialize, Deserialize)]
88pub struct AssetPattern {
89    pub policy: Vec<u8>,
90    pub name: Option<Vec<u8>>,
91}
92
93impl From<AssetPattern> for crate::wit::balius::app::ledger::AssetPattern {
94    fn from(value: AssetPattern) -> Self {
95        Self {
96            policy: value.policy,
97            name: value.name,
98        }
99    }
100}
101
102pub trait InputExpr: 'static + Send + Sync {
103    fn eval(&self, ctx: &BuildContext) -> Result<Vec<conway::TransactionInput>, BuildError>;
104}
105
106#[derive(Clone, Serialize, Deserialize)]
107pub enum UtxoSource {
108    Refs(Vec<TxoRef>),
109    Search(UtxoPattern),
110}
111
112impl UtxoSource {
113    pub fn resolve(&self, ctx: &BuildContext) -> Result<UtxoSet, BuildError> {
114        match self {
115            Self::Refs(refs) => ctx.ledger.read_utxos(refs),
116            Self::Search(utxo_pattern) => ctx.ledger.search_utxos(utxo_pattern),
117        }
118    }
119}
120
121#[serde_as]
122#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
123pub struct ReferenceScript {
124    pub ref_txo: conway::TransactionInput,
125    pub hash: Hash<28>,
126    #[serde_as(as = "DisplayFromStr")]
127    pub address: Address,
128}
129
130impl InputExpr for ReferenceScript {
131    fn eval(&self, _: &dsl::BuildContext) -> Result<Vec<conway::TransactionInput>, BuildError> {
132        Ok(vec![self.ref_txo.clone()])
133    }
134}
135
136#[derive(PartialEq, Eq, Debug, Clone, Hash, Serialize, Deserialize)]
137pub struct AssetPolicyId(Hash<28>);
138
139impl AssetPolicyId {
140    pub fn new(hash: Hash<28>) -> Self {
141        Self(hash)
142    }
143}
144
145impl From<Hash<28>> for AssetPolicyId {
146    fn from(value: Hash<28>) -> Self {
147        Self(value)
148    }
149}
150
151impl From<AssetPolicyId> for Hash<28> {
152    fn from(value: AssetPolicyId) -> Self {
153        value.0
154    }
155}
156
157impl TryFrom<&str> for AssetPolicyId {
158    type Error = BuildError;
159
160    fn try_from(value: &str) -> Result<Self, Self::Error> {
161        let hash = <Hash<28> as std::str::FromStr>::from_str(value)
162            .map_err(|_| BuildError::MalformedAssetPolicyIdHex)?;
163        Ok(AssetPolicyId(hash))
164    }
165}
166
167impl std::ops::Deref for AssetPolicyId {
168    type Target = Hash<28>;
169
170    fn deref(&self) -> &Self::Target {
171        &self.0
172    }
173}
174
175impl std::fmt::Display for AssetPolicyId {
176    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
177        write!(f, "{}", hex::encode(self.0))
178    }
179}
180
181#[derive(PartialEq, Eq, Debug, Clone, Hash, Serialize, Deserialize)]
182pub struct AssetName(Bytes);
183
184impl AssetName {
185    pub fn new(name: Bytes) -> Result<Self, BuildError> {
186        if name.len() > 32 {
187            panic!("Asset name too long");
188        }
189
190        Ok(Self(name))
191    }
192}
193
194impl TryFrom<Vec<u8>> for AssetName {
195    type Error = BuildError;
196
197    fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
198        Self::new(value.into())
199    }
200}
201
202impl TryFrom<&str> for AssetName {
203    type Error = BuildError;
204
205    fn try_from(value: &str) -> Result<Self, Self::Error> {
206        Self::new(value.as_bytes().to_vec().into())
207    }
208}
209
210impl From<AssetName> for Bytes {
211    fn from(value: AssetName) -> Self {
212        value.0
213    }
214}
215
216impl std::ops::Deref for AssetName {
217    type Target = Bytes;
218
219    fn deref(&self) -> &Self::Target {
220        &self.0
221    }
222}
223
224#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Hash, Clone)]
225pub struct TxoRef {
226    pub hash: Hash<32>,
227    pub index: u64,
228}
229
230impl std::str::FromStr for TxoRef {
231    type Err = BuildError;
232
233    fn from_str(s: &str) -> Result<Self, Self::Err> {
234        let (hash, index) = s.split_once("#").ok_or(BuildError::MalformedTxoRef)?;
235        let hash = Hash::from_str(hash).map_err(|_| BuildError::MalformedTxoRef)?;
236        let index = index.parse().map_err(|_| BuildError::MalformedTxoRef)?;
237        Ok(TxoRef::new(hash, index))
238    }
239}
240
241impl From<crate::wit::balius::app::ledger::TxoRef> for TxoRef {
242    fn from(value: crate::wit::balius::app::ledger::TxoRef) -> Self {
243        Self::new(Hash::from(value.tx_hash.as_slice()), value.tx_index as u64)
244    }
245}
246
247impl From<TxoRef> for crate::wit::balius::app::ledger::TxoRef {
248    fn from(value: TxoRef) -> crate::wit::balius::app::ledger::TxoRef {
249        crate::wit::balius::app::ledger::TxoRef {
250            tx_hash: value.hash.to_vec(),
251            tx_index: value.index as u32,
252        }
253    }
254}
255
256impl TxoRef {
257    pub fn new(hash: Hash<32>, index: u64) -> Self {
258        Self { hash, index }
259    }
260}
261
262impl dsl::InputExpr for TxoRef {
263    fn eval(&self, _: &BuildContext) -> Result<Vec<conway::TransactionInput>, BuildError> {
264        Ok(vec![self.into()])
265    }
266}
267
268impl From<&TxoRef> for conway::TransactionInput {
269    fn from(value: &TxoRef) -> Self {
270        conway::TransactionInput {
271            transaction_id: value.hash,
272            index: value.index,
273        }
274    }
275}
276
277impl InputExpr for UtxoSource {
278    fn eval(&self, ctx: &BuildContext) -> Result<Vec<conway::TransactionInput>, BuildError> {
279        let out = self.resolve(ctx)?.refs().map(|i| i.into()).collect();
280
281        Ok(out)
282    }
283}
284
285pub trait ValueExpr: 'static + Send + Sync {
286    fn eval(&self, ctx: &BuildContext) -> Result<conway::Value, BuildError>;
287
288    fn eval_as_mint(&self, ctx: &BuildContext) -> Result<conway::Mint, BuildError> {
289        let value = self.eval(ctx)?;
290
291        match value {
292            conway::Value::Multiasset(_, assets) => asset_math::multiasset_coin_to_mint(assets),
293            conway::Value::Coin(_) => Err(BuildError::Conflicting),
294        }
295    }
296
297    fn eval_as_burn(&self, ctx: &BuildContext) -> Result<conway::Mint, BuildError> {
298        let value = self.eval(ctx)?;
299
300        match value {
301            conway::Value::Multiasset(_, assets) => asset_math::multiasset_coin_to_burn(assets),
302            conway::Value::Coin(_) => Err(BuildError::Conflicting),
303        }
304    }
305}
306
307impl ValueExpr for u64 {
308    fn eval(&self, _ctx: &BuildContext) -> Result<conway::Value, BuildError> {
309        Ok(conway::Value::Coin(*self))
310    }
311}
312
313impl<F> ValueExpr for F
314where
315    F: Fn(&BuildContext) -> Result<conway::Value, BuildError> + 'static + Send + Sync,
316{
317    fn eval(&self, ctx: &BuildContext) -> Result<conway::Value, BuildError> {
318        self(ctx)
319    }
320}
321
322impl<T: ValueExpr> ValueExpr for Option<T> {
323    fn eval(&self, ctx: &BuildContext) -> Result<conway::Value, BuildError> {
324        match self {
325            Some(v) => v.eval(ctx),
326            None => Err(BuildError::Incomplete),
327        }
328    }
329}
330
331// calculate min utxo lovelace according to spec
332// https://cips.cardano.org/cip/CIP-55
333pub struct MinUtxoLovelace;
334
335impl ValueExpr for MinUtxoLovelace {
336    fn eval(&self, ctx: &BuildContext) -> Result<conway::Value, BuildError> {
337        let parent = match &ctx.parent_output {
338            Some(x) => x,
339            None => return Ok(conway::Value::Coin(0)),
340        };
341
342        let serialized = pallas_codec::minicbor::to_vec(parent).unwrap();
343        let min_lovelace = (160u64 + serialized.len() as u64) * ctx.pparams.coins_per_utxo_byte;
344        let current_value = match parent {
345            conway::PseudoTransactionOutput::PostAlonzo(x) => &x.value,
346            _ => unimplemented!(),
347        };
348
349        let current_lovelace = asset_math::value_coin(current_value);
350
351        if current_lovelace >= min_lovelace {
352            return Ok(current_value.clone());
353        }
354
355        let optimized = asset_math::value_saturating_add_coin(
356            current_value.clone(),
357            (min_lovelace - current_lovelace) as i64,
358        );
359
360        Ok(optimized)
361    }
362}
363
364impl ValueExpr for Box<dyn ValueExpr> {
365    fn eval(&self, ctx: &BuildContext) -> Result<conway::Value, BuildError> {
366        (**self).eval(ctx)
367    }
368}
369
370impl<T: ValueExpr> ValueExpr for Vec<T> {
371    fn eval(&self, ctx: &BuildContext) -> Result<conway::Value, BuildError> {
372        let values = self
373            .iter()
374            .map(|v| v.eval(ctx))
375            .collect::<Result<Vec<_>, _>>()?;
376
377        Ok(asset_math::aggregate_values(values))
378    }
379}
380
381pub trait AddressExpr: 'static + Send + Sync {
382    fn eval(&self, ctx: &BuildContext) -> Result<Address, BuildError>;
383}
384
385impl AddressExpr for &'static str {
386    fn eval(&self, _ctx: &BuildContext) -> Result<Address, BuildError> {
387        Address::from_bech32(self).map_err(|_| BuildError::MalformedAddress)
388    }
389}
390
391impl AddressExpr for String {
392    fn eval(&self, _ctx: &BuildContext) -> Result<Address, BuildError> {
393        Address::from_bech32(self).map_err(|_| BuildError::MalformedAddress)
394    }
395}
396
397impl AddressExpr for Address {
398    fn eval(&self, _ctx: &BuildContext) -> Result<Address, BuildError> {
399        Ok(self.clone())
400    }
401}
402
403impl AddressExpr for Box<dyn AddressExpr> {
404    fn eval(&self, ctx: &BuildContext) -> Result<Address, BuildError> {
405        (**self).eval(ctx)
406    }
407}
408
409impl<T: AddressExpr> AddressExpr for Option<T> {
410    fn eval(&self, ctx: &BuildContext) -> Result<Address, BuildError> {
411        match self {
412            Some(v) => v.eval(ctx),
413            None => Err(BuildError::Incomplete),
414        }
415    }
416}
417
418impl<F> AddressExpr for F
419where
420    F: Fn(&BuildContext) -> Result<Address, BuildError> + 'static + Send + Sync,
421{
422    fn eval(&self, ctx: &BuildContext) -> Result<Address, BuildError> {
423        self(ctx)
424    }
425}
426
427pub trait OutputExpr: 'static + Send + Sync {
428    fn eval(&mut self, ctx: &BuildContext) -> Result<conway::TransactionOutput, BuildError>;
429}
430
431pub struct ChangeAddress(pub UtxoSource);
432
433impl AddressExpr for ChangeAddress {
434    fn eval(&self, ctx: &BuildContext) -> Result<Address, BuildError> {
435        let utxo_set = &self.0.resolve(ctx)?;
436
437        if utxo_set.is_empty() {
438            return Err(BuildError::EmptyUtxoSet);
439        }
440
441        let addresses: HashSet<_> = utxo_set
442            .txos()
443            .map(|x| x.address())
444            .collect::<Result<HashSet<_>, _>>()
445            .map_err(|_| BuildError::UtxoDecode)?;
446
447        if addresses.len() > 1 {
448            return Err(BuildError::Conflicting);
449        }
450
451        Ok(addresses.into_iter().next().unwrap())
452    }
453}
454
455pub struct TotalChange;
456
457impl ValueExpr for TotalChange {
458    fn eval(&self, ctx: &BuildContext) -> Result<conway::Value, BuildError> {
459        let change = asset_math::subtract_value(&ctx.total_input, &ctx.spent_output)?;
460        let fee = ctx.estimated_fee;
461        let diff = asset_math::value_saturating_add_coin(change, -(fee as i64));
462        Ok(diff)
463    }
464}
465
466pub struct FeeChangeReturn(pub UtxoSource);
467
468impl OutputExpr for FeeChangeReturn {
469    fn eval(&mut self, ctx: &BuildContext) -> Result<conway::TransactionOutput, BuildError> {
470        OutputBuilder::new()
471            .address(ChangeAddress(self.0.clone()))
472            .with_value(TotalChange)
473            .eval(ctx)
474    }
475}
476
477pub trait PlutusDataExpr: 'static + Send + Sync {
478    fn eval(&self, ctx: &BuildContext) -> Result<conway::PlutusData, BuildError>;
479}
480
481impl PlutusDataExpr for conway::PlutusData {
482    fn eval(&self, _ctx: &BuildContext) -> Result<conway::PlutusData, BuildError> {
483        Ok(self.clone())
484    }
485}
486
487impl<F> PlutusDataExpr for F
488where
489    F: Fn(&BuildContext) -> Result<conway::PlutusData, BuildError> + 'static + Send + Sync,
490{
491    fn eval(&self, ctx: &BuildContext) -> Result<conway::PlutusData, BuildError> {
492        self(ctx)
493    }
494}
495
496impl PlutusDataExpr for Box<dyn PlutusDataExpr> {
497    fn eval(&self, ctx: &BuildContext) -> Result<conway::PlutusData, BuildError> {
498        (**self).eval(ctx)
499    }
500}
501
502impl PlutusDataExpr for () {
503    fn eval(&self, _ctx: &BuildContext) -> Result<conway::PlutusData, BuildError> {
504        Ok(conway::PlutusData::Constr(conway::Constr {
505            tag: 121,
506            any_constructor: None,
507            fields: conway::MaybeIndefArray::Def(vec![]),
508        }))
509    }
510}
511
512pub trait MintExpr: 'static + Send + Sync {
513    fn eval(&self, ctx: &BuildContext) -> Result<Option<conway::Mint>, BuildError>;
514    fn eval_redeemer(&self, ctx: &BuildContext) -> Result<Option<conway::Redeemer>, BuildError>;
515}
516
517#[derive(Default)]
518pub struct MintBuilder {
519    pub assets: Vec<Box<dyn ValueExpr>>,
520    pub burn: Vec<Box<dyn ValueExpr>>,
521    pub redeemer: Option<Box<dyn PlutusDataExpr>>,
522}
523
524impl MintBuilder {
525    pub fn new() -> Self {
526        Self::default()
527    }
528
529    pub fn with_asset(mut self, asset: impl ValueExpr) -> Self {
530        self.assets.push(Box::new(asset));
531        self
532    }
533
534    pub fn with_burn(mut self, burn: impl ValueExpr) -> Self {
535        self.burn.push(Box::new(burn));
536        self
537    }
538
539    pub fn using_redeemer(mut self, redeemer: impl PlutusDataExpr) -> Self {
540        self.redeemer = Some(Box::new(redeemer));
541        self
542    }
543}
544
545impl MintExpr for MintBuilder {
546    fn eval(&self, ctx: &BuildContext) -> Result<Option<primitives::Mint>, BuildError> {
547        let out = HashMap::new();
548
549        let out = self.assets.iter().try_fold(out, |mut acc, v| {
550            let v = v.eval_as_mint(ctx)?;
551            asset_math::fold_multiassets(&mut acc, v);
552            Result::<_, BuildError>::Ok(acc)
553        })?;
554
555        let out = self.burn.iter().try_fold(out, |mut acc, v| {
556            let v = v.eval_as_burn(ctx)?;
557            asset_math::fold_multiassets(&mut acc, v);
558            Result::<_, BuildError>::Ok(acc)
559        })?;
560
561        let mint: Vec<_> = out
562            .into_iter()
563            .filter_map(|(policy, assets)| {
564                let assets = assets.into_iter().collect();
565                Some((policy, NonEmptyKeyValuePairs::from_vec(assets)?))
566            })
567            .collect();
568
569        Ok(NonEmptyKeyValuePairs::from_vec(mint))
570    }
571
572    fn eval_redeemer(&self, ctx: &BuildContext) -> Result<Option<conway::Redeemer>, BuildError> {
573        let Some(mint) = self.eval(ctx)? else {
574            return Ok(None);
575        };
576
577        if mint.is_empty() {
578            return Err(BuildError::Incomplete);
579        }
580
581        if mint.len() > 1 {
582            return Err(BuildError::Conflicting);
583        }
584
585        let (policy, _) = mint.iter().next().unwrap();
586
587        let data = self
588            .redeemer
589            .as_ref()
590            .ok_or(BuildError::Incomplete)?
591            .eval(ctx)?;
592
593        let out = conway::Redeemer {
594            tag: conway::RedeemerTag::Mint,
595            index: ctx.mint_redeemer_index(*policy)?,
596            ex_units: ctx.eval_ex_units(*policy, &data),
597            data,
598        };
599
600        Ok(Some(out))
601    }
602}
603
604pub trait ScriptExpr: 'static + Send + Sync {
605    fn eval(&self, ctx: &BuildContext) -> Result<conway::ScriptRef, BuildError>;
606}
607
608impl ScriptExpr for conway::ScriptRef {
609    fn eval(&self, _ctx: &BuildContext) -> Result<conway::ScriptRef, BuildError> {
610        Ok(self.clone())
611    }
612}
613
614impl ScriptExpr for conway::PlutusScript<3> {
615    fn eval(&self, _ctx: &BuildContext) -> Result<conway::ScriptRef, BuildError> {
616        Ok(conway::ScriptRef::PlutusV3Script(self.clone()))
617    }
618}
619
620#[derive(Default)]
621pub struct OutputBuilder {
622    pub previous: Option<conway::TransactionOutput>,
623    pub address: Option<Box<dyn dsl::AddressExpr>>,
624    pub values: Vec<Box<dyn dsl::ValueExpr>>,
625    pub script: Option<Box<dyn ScriptExpr>>,
626    // TODO: inline / hash datum
627}
628
629impl OutputBuilder {
630    pub fn new() -> Self {
631        Self::default()
632    }
633
634    pub fn address(mut self, address: impl AddressExpr + 'static) -> Self {
635        self.address = Some(Box::new(address));
636        self
637    }
638
639    pub fn with_value(mut self, value: impl ValueExpr + 'static) -> Self {
640        self.values.push(Box::new(value));
641        self
642    }
643
644    pub fn with_script(mut self, script: impl ScriptExpr + 'static) -> Self {
645        self.script = Some(Box::new(script));
646        self
647    }
648}
649
650impl OutputExpr for OutputBuilder {
651    fn eval(&mut self, ctx: &BuildContext) -> Result<conway::TransactionOutput, BuildError> {
652        let ctx = match &self.previous {
653            Some(x) => &ctx.with_parent_output(x.clone()),
654            None => ctx,
655        };
656
657        let value = self.values.eval(ctx)?;
658
659        let address = self.address.eval(ctx)?.to_vec().into();
660
661        let script_ref = self
662            .script
663            .as_ref()
664            .map(|s| s.eval(ctx))
665            .transpose()?
666            .map(pallas_codec::utils::CborWrap);
667
668        let output = conway::TransactionOutput::PostAlonzo(conway::PostAlonzoTransactionOutput {
669            value,
670            address,
671            script_ref,
672            datum_option: None, // TODO
673        });
674
675        self.previous = Some(output.clone());
676
677        Ok(output)
678    }
679}
680
681pub trait TxExpr: 'static + Send + Sync {
682    fn eval_body(&mut self, ctx: &BuildContext) -> Result<conway::TransactionBody, BuildError>;
683    fn eval_witness_set(&mut self, ctx: &BuildContext) -> Result<conway::WitnessSet, BuildError>;
684}
685
686impl<T: TxExpr> TxExpr for &'static mut T {
687    fn eval_body(&mut self, ctx: &BuildContext) -> Result<conway::TransactionBody, BuildError> {
688        (**self).eval_body(ctx)
689    }
690
691    fn eval_witness_set(&mut self, ctx: &BuildContext) -> Result<conway::WitnessSet, BuildError> {
692        (**self).eval_witness_set(ctx)
693    }
694}
695
696impl TxExpr for Box<dyn TxExpr> {
697    fn eval_body(&mut self, ctx: &BuildContext) -> Result<conway::TransactionBody, BuildError> {
698        (**self).eval_body(ctx)
699    }
700
701    fn eval_witness_set(&mut self, ctx: &BuildContext) -> Result<conway::WitnessSet, BuildError> {
702        (**self).eval_witness_set(ctx)
703    }
704}
705
706#[derive(Default)]
707pub struct TxBuilder {
708    pub reference_inputs: Vec<Box<dyn InputExpr>>,
709    pub inputs: Vec<Box<dyn InputExpr>>,
710    pub outputs: Vec<Box<dyn OutputExpr>>,
711    pub mint: Vec<Box<dyn MintExpr>>,
712    pub fee: Option<u64>,
713    // pub valid_from_slot: Option<u64>,
714    // pub invalid_from_slot: Option<u64>,
715    // pub network_id: Option<u8>,
716    // pub collateral_inputs: Option<Vec<TxoRef>>,
717    // pub collateral_output: Option<Output>,
718    // pub disclosed_signers: Option<Vec<PubKeyHash>>,
719    // pub scripts: Option<HashMap<ScriptHash, Script>>,
720    // pub datums: Option<HashMap<DatumHash, DatumBytes>>,
721    // pub redeemers: Option<Redeemers>,
722    // pub script_data_hash: Option<ScriptDataHash>,
723    // pub signature_amount_override: Option<u8>,
724    // #[serde_as(as = "Option<DisplayFromStr>")]
725    // pub change_address: Option<Address>,
726    // pub certificates: TODO
727    // pub withdrawals: TODO
728    // pub updates: TODO
729    // pub auxiliary_data: TODO
730    // pub phase_2_valid: TODO
731}
732
733impl TxBuilder {
734    pub fn new() -> Self {
735        Self::default()
736    }
737
738    pub fn with_reference_input(mut self, input: impl InputExpr) -> Self {
739        self.reference_inputs.push(Box::new(input));
740        self
741    }
742
743    pub fn with_input(mut self, input: impl InputExpr) -> Self {
744        self.inputs.push(Box::new(input));
745        self
746    }
747
748    pub fn with_output(mut self, output: impl OutputExpr) -> Self {
749        self.outputs.push(Box::new(output));
750        self
751    }
752
753    pub fn with_mint(mut self, mint: impl MintExpr) -> Self {
754        self.mint.push(Box::new(mint));
755        self
756    }
757
758    pub fn with_fee(mut self, fee: u64) -> Self {
759        self.fee = Some(fee);
760        self
761    }
762}
763
764impl TxExpr for TxBuilder {
765    fn eval_body(&mut self, ctx: &BuildContext) -> Result<conway::TransactionBody, BuildError> {
766        let out = conway::TransactionBody {
767            inputs: self
768                .inputs
769                .iter()
770                .map(|i| i.eval(ctx))
771                .collect::<Result<Vec<_>, _>>()?
772                .into_iter()
773                .flatten()
774                .collect::<Vec<_>>()
775                .into(),
776            outputs: self
777                .outputs
778                .iter_mut()
779                .map(|o| o.eval(ctx))
780                .collect::<Result<Vec<_>, _>>()?,
781            fee: ctx.estimated_fee,
782            ttl: None,
783            validity_interval_start: None,
784            certificates: None,
785            withdrawals: None,
786            auxiliary_data_hash: None,
787            mint: {
788                let mints = self
789                    .mint
790                    .iter()
791                    .map(|m| m.eval(ctx))
792                    .collect::<Result<Vec<_>, _>>()?
793                    .into_iter()
794                    .flatten();
795
796                asset_math::aggregate_assets(mints)
797            },
798            script_data_hash: None,
799            collateral: None,
800            required_signers: None,
801            network_id: None,
802            collateral_return: None,
803            total_collateral: None,
804            reference_inputs: {
805                let refs: Vec<_> = self
806                    .reference_inputs
807                    .iter()
808                    .map(|i| i.eval(ctx))
809                    .collect::<Result<Vec<_>, _>>()?
810                    .into_iter()
811                    .flatten()
812                    .collect();
813
814                NonEmptySet::from_vec(refs)
815            },
816            voting_procedures: None,
817            proposal_procedures: None,
818            treasury_value: None,
819            donation: None,
820        };
821
822        Ok(out)
823    }
824
825    fn eval_witness_set(&mut self, ctx: &BuildContext) -> Result<conway::WitnessSet, BuildError> {
826        let out = conway::WitnessSet {
827            redeemer: {
828                let redeemers: Vec<_> = self
829                    .mint
830                    .iter()
831                    .map(|m| m.eval_redeemer(ctx))
832                    .collect::<Result<Vec<_>, _>>()?
833                    .into_iter()
834                    .flatten()
835                    .collect();
836
837                if redeemers.is_empty() {
838                    None
839                } else {
840                    Some(conway::Redeemers::List(conway::MaybeIndefArray::Def(
841                        redeemers,
842                    )))
843                }
844            },
845            vkeywitness: None,
846            native_script: None,
847            bootstrap_witness: None,
848            plutus_v1_script: None,
849            plutus_data: None,
850            plutus_v2_script: None,
851            plutus_v3_script: None,
852        };
853
854        Ok(out)
855    }
856}
857
858#[macro_export]
859macro_rules! define_asset_class {
860    ($struct_name:ident, $policy:expr) => {
861        #[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
862        pub struct $struct_name($crate::txbuilder::Bytes, u64);
863
864        impl $struct_name {
865            pub fn value(name: $crate::txbuilder::AssetName, quantity: u64) -> Self {
866                Self(name.into(), quantity)
867            }
868        }
869
870        impl $crate::txbuilder::ValueExpr for $struct_name {
871            fn eval(
872                &self,
873                _: &$crate::txbuilder::BuildContext,
874            ) -> std::result::Result<$crate::txbuilder::Value, $crate::txbuilder::BuildError> {
875                let policy = $crate::txbuilder::Hash::from(*$policy);
876                let name = $crate::txbuilder::Bytes::from(self.0.clone());
877                let Ok(amount) = self.1.try_into() else {
878                    return Ok($crate::txbuilder::Value::Coin(0));
879                };
880                let asset = $crate::txbuilder::NonEmptyKeyValuePairs::Def(vec![(name, amount)]);
881                let val = $crate::txbuilder::Value::Multiasset(
882                    0,
883                    $crate::txbuilder::NonEmptyKeyValuePairs::Def(vec![(policy, asset)]),
884                );
885
886                Ok(val)
887            }
888        }
889    };
890}
891
892define_asset_class!(MyAssetClass, b"abcabcababcabcababcabcababca");