Skip to main content

balius_sdk/txbuilder/
build.rs

1use pallas_traverse::MultiEraValue;
2use std::sync::Arc;
3use std::{collections::HashMap, ops::Deref as _};
4
5use super::{
6    asset_math, primitives, BuildContext, BuildError, Ledger, PParams, TxExpr, TxoRef, UtxoPattern,
7    UtxoSet,
8};
9
10impl BuildContext {
11    pub fn mint_redeemer_index(&self, policy: primitives::ScriptHash) -> Result<u32, BuildError> {
12        if let Some(tx_body) = &self.tx_body {
13            let mut out: Vec<_> = tx_body
14                .mint
15                .iter()
16                .flat_map(|x| x.iter())
17                .map(|(p, _)| *p)
18                .collect();
19
20            out.sort();
21            out.dedup();
22
23            if let Some(index) = out.iter().position(|p| *p == policy) {
24                return Ok(index as u32);
25            }
26        }
27
28        Err(BuildError::RedeemerTargetMissing)
29    }
30
31    pub fn eval_ex_units(
32        &self,
33        _script: primitives::ScriptHash,
34        _data: &primitives::PlutusData,
35    ) -> primitives::ExUnits {
36        // TODO
37        primitives::ExUnits { mem: 8, steps: 8 }
38    }
39}
40
41pub(crate) struct ExtLedgerFacade;
42
43impl crate::txbuilder::Ledger for ExtLedgerFacade {
44    fn read_utxos(&self, refs: &[TxoRef]) -> Result<UtxoSet, BuildError> {
45        let refs: Vec<_> = refs.iter().cloned().map(Into::into).collect();
46        let x = crate::wit::balius::app::ledger::read_utxos(&refs)?;
47
48        let x: Vec<_> = x
49            .into_iter()
50            .map(|x| (TxoRef::from(x.ref_), x.body.to_vec()))
51            .collect();
52
53        Ok(UtxoSet::from_iter(x))
54    }
55
56    fn search_utxos(&self, pattern: &UtxoPattern) -> Result<UtxoSet, BuildError> {
57        let pattern = pattern.clone().into();
58        let mut utxos = HashMap::new();
59        let max_items = 32;
60        let mut utxo_page = Some(crate::wit::balius::app::ledger::search_utxos(
61            &pattern, None, max_items,
62        )?);
63        while let Some(page) = utxo_page.take() {
64            for utxo in page.utxos {
65                utxos.insert(utxo.ref_.into(), utxo.body);
66            }
67            if let Some(next) = page.next_token {
68                utxo_page = Some(crate::wit::balius::app::ledger::search_utxos(
69                    &pattern,
70                    Some(&next),
71                    max_items,
72                )?);
73            }
74        }
75        Ok(utxos.into())
76    }
77
78    fn read_params(&self) -> Result<PParams, BuildError> {
79        let bytes = crate::wit::balius::app::ledger::read_params()?;
80
81        serde_json::from_slice(&bytes)
82            .map_err(|_| BuildError::LedgerError("failed to parse params json".to_string()))
83    }
84}
85
86pub fn build<T, L>(mut tx: T, ledger: L) -> Result<primitives::Tx, BuildError>
87where
88    T: TxExpr,
89    L: Ledger + 'static,
90{
91    let mut ctx = BuildContext {
92        network: primitives::NetworkId::Testnet,
93        pparams: ledger.read_params()?,
94        total_input: primitives::Value::Coin(0),
95        spent_output: primitives::Value::Coin(0),
96        estimated_fee: 0,
97        ledger: Arc::new(Box::new(ledger)),
98        tx_body: None,
99        parent_output: None,
100    };
101
102    // Build the raw transaction, so we have the info needed to estimate fees and
103    // compute change.
104    let body = tx.eval_body(&ctx)?;
105
106    let input_refs: Vec<_> = body
107        .inputs
108        .iter()
109        .map(|i| TxoRef {
110            hash: i.transaction_id,
111            index: i.index,
112        })
113        .collect();
114    let utxos = ctx.ledger.read_utxos(&input_refs)?;
115    ctx.total_input =
116        asset_math::aggregate_values(utxos.txos().map(|txo| input_into_conway(&txo.value())));
117    if let Some(mint) = &body.mint {
118        ctx.total_input = asset_math::add_mint(&ctx.total_input, mint)?;
119    }
120    ctx.spent_output = asset_math::aggregate_values(body.outputs.iter().map(output_into_conway));
121    // TODO: estimate the fee
122    ctx.estimated_fee = 2_000_000;
123
124    // Now that we know the inputs/outputs/fee, build the "final" (unsigned)tx
125    let body = tx.eval_body(&ctx)?;
126    ctx.tx_body = Some(body);
127    for _ in 0..3 {
128        let body = tx.eval_body(&ctx)?;
129        ctx.tx_body = Some(body);
130    }
131
132    let wit = tx.eval_witness_set(&ctx).unwrap();
133
134    let tx = primitives::Tx {
135        transaction_body: ctx.tx_body.take().unwrap(),
136        transaction_witness_set: wit,
137        auxiliary_data: pallas_codec::utils::Nullable::Null,
138        success: true,
139    };
140
141    Ok(tx)
142}
143
144// TODO: this belongs in pallas-traverse
145// https://github.com/txpipe/pallas/pull/545
146fn input_into_conway(value: &MultiEraValue) -> primitives::Value {
147    use pallas_primitives::{alonzo, conway};
148    match value {
149        MultiEraValue::Byron(x) => conway::Value::Coin(*x),
150        MultiEraValue::AlonzoCompatible(x) => match x.deref() {
151            alonzo::Value::Coin(x) => conway::Value::Coin(*x),
152            alonzo::Value::Multiasset(x, assets) => {
153                let coin = *x;
154                let assets = assets
155                    .iter()
156                    .filter_map(|(k, v)| {
157                        let v: Vec<(conway::Bytes, conway::PositiveCoin)> = v
158                            .iter()
159                            .filter_map(|(k, v)| Some((k.clone(), (*v).try_into().ok()?)))
160                            .collect();
161                        Some((*k, conway::NonEmptyKeyValuePairs::from_vec(v)?))
162                    })
163                    .collect();
164                if let Some(assets) = conway::NonEmptyKeyValuePairs::from_vec(assets) {
165                    conway::Value::Multiasset(coin, assets)
166                } else {
167                    conway::Value::Coin(coin)
168                }
169            }
170        },
171        MultiEraValue::Conway(x) => x.deref().clone(),
172        _ => panic!("unrecognized value"),
173    }
174}
175
176fn output_into_conway(output: &primitives::TransactionOutput) -> primitives::Value {
177    use pallas_primitives::{alonzo, conway};
178    match output {
179        primitives::TransactionOutput::Legacy(o) => match &o.amount {
180            alonzo::Value::Coin(c) => primitives::Value::Coin(*c),
181            alonzo::Value::Multiasset(c, assets) => {
182                let assets = assets
183                    .iter()
184                    .filter_map(|(k, v)| {
185                        let v: Vec<(conway::Bytes, conway::PositiveCoin)> = v
186                            .iter()
187                            .filter_map(|(k, v)| Some((k.clone(), (*v).try_into().ok()?)))
188                            .collect();
189                        Some((*k, conway::NonEmptyKeyValuePairs::from_vec(v)?))
190                    })
191                    .collect();
192                if let Some(assets) = conway::NonEmptyKeyValuePairs::from_vec(assets) {
193                    primitives::Value::Multiasset(*c, assets)
194                } else {
195                    primitives::Value::Coin(*c)
196                }
197            }
198        },
199        primitives::TransactionOutput::PostAlonzo(o) => o.value.clone(),
200    }
201}