tx3_lang/
lib.rs

1//! The Tx3 language
2//!
3//! This crate provides the parser, analyzer and lowering logic for the Tx3
4//! language.
5//!
6//! # Parsing
7//!
8//! ```
9//! let program = tx3_lang::parsing::parse_string("tx swap() {}").unwrap();
10//! ```
11//!
12//! # Analyzing
13//!
14//! ```
15//! let mut program = tx3_lang::parsing::parse_string("tx swap() {}").unwrap();
16//! tx3_lang::analyzing::analyze(&mut program).ok().unwrap();
17//! ```
18//!
19//! # Lowering
20//!
21//! ```
22//! let mut program = tx3_lang::parsing::parse_string("tx swap() {}").unwrap();
23//! tx3_lang::analyzing::analyze(&mut program).ok().unwrap();
24//! let ir = tx3_lang::lowering::lower(&program, "swap").unwrap();
25//! ```
26
27pub mod analyzing;
28pub mod applying;
29pub mod ast;
30pub mod backend;
31pub mod ir;
32pub mod loading;
33pub mod lowering;
34pub mod parsing;
35
36// chain specific
37pub mod cardano;
38
39#[macro_export]
40macro_rules! include_tx3_build {
41    ($package: tt) => {
42        include!(concat!(env!("OUT_DIR"), concat!("/", $package, ".rs")));
43    };
44}
45
46#[derive(Encode, Decode, Serialize, Deserialize, Debug, Clone, Hash, PartialEq, Eq)]
47pub struct UtxoRef {
48    pub txid: Vec<u8>,
49    pub index: u32,
50}
51
52impl UtxoRef {
53    pub fn new(txid: &[u8], index: u32) -> Self {
54        Self {
55            txid: txid.to_vec(),
56            index,
57        }
58    }
59}
60
61pub type AssetPolicy = Vec<u8>;
62pub type AssetName = Vec<u8>;
63
64#[derive(Encode, Decode, Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
65pub enum AssetClass {
66    Naked,
67    Named(AssetName),
68    Defined(AssetPolicy, AssetName),
69}
70
71impl AssetClass {
72    pub fn is_defined(&self) -> bool {
73        matches!(self, AssetClass::Defined(_, _))
74    }
75
76    pub fn is_named(&self) -> bool {
77        matches!(self, AssetClass::Named(_))
78    }
79
80    pub fn is_naked(&self) -> bool {
81        matches!(self, AssetClass::Naked)
82    }
83
84    pub fn policy(&self) -> Option<&[u8]> {
85        match self {
86            AssetClass::Defined(policy, _) => Some(policy),
87            _ => None,
88        }
89    }
90
91    pub fn name(&self) -> Option<&[u8]> {
92        match self {
93            AssetClass::Defined(_, name) => Some(name),
94            AssetClass::Named(name) => Some(name),
95            _ => None,
96        }
97    }
98}
99
100#[derive(Encode, Decode, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
101pub struct CanonicalAssets(HashMap<AssetClass, i128>);
102
103impl std::ops::Deref for CanonicalAssets {
104    type Target = HashMap<AssetClass, i128>;
105
106    fn deref(&self) -> &Self::Target {
107        &self.0
108    }
109}
110
111impl CanonicalAssets {
112    pub fn empty() -> Self {
113        Self(HashMap::new())
114    }
115
116    pub fn from_naked_amount(amount: i128) -> Self {
117        Self(HashMap::from([(AssetClass::Naked, amount)]))
118    }
119
120    pub fn from_named_asset(asset_name: &[u8], amount: i128) -> Self {
121        Self(HashMap::from([(
122            AssetClass::Named(asset_name.to_vec()),
123            amount,
124        )]))
125    }
126
127    pub fn from_defined_asset(policy: &[u8], asset_name: &[u8], amount: i128) -> Self {
128        Self(HashMap::from([(
129            AssetClass::Defined(policy.to_vec(), asset_name.to_vec()),
130            amount,
131        )]))
132    }
133
134    pub fn from_asset(policy: Option<&[u8]>, name: Option<&[u8]>, amount: i128) -> Self {
135        match (policy, name) {
136            (Some(policy), Some(name)) => Self::from_defined_asset(policy, name, amount),
137            (Some(policy), None) => Self::from_defined_asset(policy, &[], amount),
138            (None, Some(name)) => Self::from_named_asset(name, amount),
139            (None, None) => Self::from_naked_amount(amount),
140        }
141    }
142
143    pub fn naked_amount(&self) -> Option<i128> {
144        self.get(&AssetClass::Naked).cloned()
145    }
146
147    pub fn contains_total(&self, other: &Self) -> bool {
148        for (class, amount) in other.iter() {
149            let Some(self_amount) = self.get(class) else {
150                return false;
151            };
152
153            if self_amount < amount {
154                return false;
155            }
156        }
157
158        true
159    }
160
161    pub fn contains_some(&self, other: &Self) -> bool {
162        for (class, _) in other.iter() {
163            if let Some(self_amount) = self.get(class) {
164                if *self_amount > 0 {
165                    return true;
166                }
167            }
168        }
169
170        false
171    }
172
173    pub fn is_empty_or_negative(&self) -> bool {
174        for (_, value) in self.iter() {
175            if *value > 0 {
176                return false;
177            }
178        }
179
180        true
181    }
182}
183
184impl std::ops::Neg for CanonicalAssets {
185    type Output = Self;
186
187    fn neg(self) -> Self {
188        let mut negated = self.0;
189
190        for (_, value) in negated.iter_mut() {
191            *value = -*value;
192        }
193
194        Self(negated)
195    }
196}
197
198impl std::ops::Add for CanonicalAssets {
199    type Output = Self;
200
201    fn add(self, other: Self) -> Self {
202        let mut aggregated = self.0;
203
204        for (key, value) in other.0 {
205            *aggregated.entry(key).or_default() += value;
206        }
207
208        Self(aggregated)
209    }
210}
211
212impl std::ops::Sub for CanonicalAssets {
213    type Output = Self;
214
215    fn sub(self, other: Self) -> Self {
216        let mut aggregated = self.0;
217
218        for (key, value) in other.0 {
219            *aggregated.entry(key).or_default() -= value;
220        }
221
222        Self(aggregated)
223    }
224}
225
226#[derive(Encode, Decode, Serialize, Deserialize, Debug, Clone)]
227pub struct Utxo {
228    pub r#ref: UtxoRef,
229    pub address: Vec<u8>,
230    pub datum: Option<ir::Expression>,
231    pub assets: CanonicalAssets,
232    pub script: Option<ir::Expression>,
233}
234
235impl std::hash::Hash for Utxo {
236    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
237        self.r#ref.hash(state);
238    }
239}
240
241impl PartialEq for Utxo {
242    fn eq(&self, other: &Self) -> bool {
243        self.r#ref == other.r#ref
244    }
245}
246
247impl Eq for Utxo {}
248
249pub type UtxoSet = HashSet<Utxo>;
250
251#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
252pub enum ArgValue {
253    Int(i128),
254    Bool(bool),
255    String(String),
256    Bytes(Vec<u8>),
257    Address(Vec<u8>),
258    UtxoSet(UtxoSet),
259    UtxoRef(UtxoRef),
260}
261
262impl From<Vec<u8>> for ArgValue {
263    fn from(value: Vec<u8>) -> Self {
264        Self::Bytes(value)
265    }
266}
267
268impl From<String> for ArgValue {
269    fn from(value: String) -> Self {
270        Self::String(value)
271    }
272}
273
274impl From<&str> for ArgValue {
275    fn from(value: &str) -> Self {
276        Self::String(value.to_string())
277    }
278}
279
280impl From<bool> for ArgValue {
281    fn from(value: bool) -> Self {
282        Self::Bool(value)
283    }
284}
285
286macro_rules! impl_from_int_for_arg_value {
287    ($($t:ty),*) => {
288        $(
289            impl From<$t> for ArgValue {
290                fn from(value: $t) -> Self {
291                    Self::Int(value as i128)
292                }
293            }
294        )*
295    };
296}
297
298impl_from_int_for_arg_value!(i8, i16, i32, i64, i128, u8, u16, u32, u64, u128);
299
300pub struct Protocol {
301    pub(crate) ast: ast::Program,
302    pub(crate) env_args: std::collections::HashMap<String, ArgValue>,
303}
304
305impl Protocol {
306    pub fn from_file(path: impl AsRef<std::path::Path>) -> loading::ProtocolLoader {
307        loading::ProtocolLoader::from_file(path)
308    }
309
310    pub fn from_string(code: String) -> loading::ProtocolLoader {
311        loading::ProtocolLoader::from_string(code)
312    }
313
314    pub fn new_tx(&self, template: &str) -> Result<ProtoTx, lowering::Error> {
315        let ir = lowering::lower(&self.ast, template)?;
316        let mut tx = ProtoTx::from(ir);
317
318        if !self.env_args.is_empty() {
319            for (k, v) in &self.env_args {
320                tx.set_arg(k, v.clone());
321            }
322        }
323
324        // TODO: merge lower and apply errors?
325        let tx = tx.apply().unwrap();
326
327        Ok(tx)
328    }
329
330    pub fn ast(&self) -> &ast::Program {
331        &self.ast
332    }
333
334    pub fn txs(&self) -> impl Iterator<Item = &ast::TxDef> {
335        self.ast.txs.iter()
336    }
337}
338
339use std::collections::{HashMap, HashSet};
340
341pub use applying::{apply_args, apply_fees, apply_inputs, find_params, find_queries, reduce};
342use bincode::{Decode, Encode};
343use serde::{Deserialize, Serialize};
344
345#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
346pub struct ProtoTx {
347    ir: ir::Tx,
348    args: std::collections::BTreeMap<String, ArgValue>,
349    inputs: std::collections::BTreeMap<String, UtxoSet>,
350    fees: Option<u64>,
351}
352
353impl From<ir::Tx> for ProtoTx {
354    fn from(ir: ir::Tx) -> Self {
355        Self {
356            ir,
357            args: std::collections::BTreeMap::new(),
358            inputs: std::collections::BTreeMap::new(),
359            fees: None,
360        }
361    }
362}
363
364impl From<ProtoTx> for ir::Tx {
365    fn from(tx: ProtoTx) -> Self {
366        tx.ir
367    }
368}
369
370impl ProtoTx {
371    pub fn find_params(&self) -> std::collections::BTreeMap<String, ir::Type> {
372        find_params(&self.ir)
373    }
374
375    pub fn find_queries(&self) -> std::collections::BTreeMap<String, ir::InputQuery> {
376        find_queries(&self.ir)
377    }
378
379    pub fn set_arg(&mut self, name: &str, value: ArgValue) {
380        self.args.insert(name.to_lowercase().to_string(), value);
381    }
382
383    pub fn with_arg(mut self, name: &str, value: ArgValue) -> Self {
384        self.args.insert(name.to_lowercase().to_string(), value);
385        self
386    }
387
388    pub fn set_input(&mut self, name: &str, value: UtxoSet) {
389        self.inputs.insert(name.to_lowercase().to_string(), value);
390    }
391
392    pub fn set_fees(&mut self, value: u64) {
393        self.fees = Some(value);
394    }
395
396    pub fn apply(self) -> Result<Self, applying::Error> {
397        let tx = apply_args(self.ir, &self.args)?;
398
399        let tx = if let Some(fees) = self.fees {
400            apply_fees(tx, fees)?
401        } else {
402            tx
403        };
404
405        let tx = apply_inputs(tx, &self.inputs)?;
406
407        let tx = reduce(tx)?;
408
409        Ok(tx.into())
410    }
411
412    pub fn ir_bytes(&self) -> Vec<u8> {
413        let config = bincode::config::standard();
414        bincode::encode_to_vec(&self.ir, config).unwrap()
415    }
416
417    pub fn from_ir_bytes(bytes: &[u8]) -> Result<Self, bincode::error::DecodeError> {
418        let config = bincode::config::standard();
419        let (ir, _) = bincode::decode_from_slice::<ir::Tx, _>(bytes, config)?;
420        Ok(Self::from(ir))
421    }
422}
423
424impl AsRef<ir::Tx> for ProtoTx {
425    fn as_ref(&self) -> &ir::Tx {
426        &self.ir
427    }
428}
429
430#[cfg(test)]
431mod tests {
432    use std::collections::HashSet;
433
434    use super::*;
435
436    #[test]
437    fn happy_path() {
438        let manifest_dir = env!("CARGO_MANIFEST_DIR");
439        let code = format!("{manifest_dir}/../../examples/transfer.tx3");
440
441        let protocol = Protocol::from_file(&code)
442            .with_env_arg("sender", ArgValue::Address(b"sender".to_vec()))
443            .load()
444            .unwrap();
445
446        let tx = protocol.new_tx("transfer").unwrap();
447
448        dbg!(&tx.find_params());
449        dbg!(&tx.find_queries());
450
451        let mut tx = tx
452            .with_arg("quantity", ArgValue::Int(100_000_000))
453            .apply()
454            .unwrap();
455
456        dbg!(&tx.find_params());
457        dbg!(&tx.find_queries());
458
459        tx.set_input(
460            "source",
461            HashSet::from([Utxo {
462                r#ref: UtxoRef {
463                    txid: b"fafafafafafafafafafafafafafafafafafafafafafafafafafafafafafafafa"
464                        .to_vec(),
465                    index: 0,
466                },
467                address: b"abababa".to_vec(),
468                datum: None,
469                assets: CanonicalAssets::from_defined_asset(b"abababa", b"asset", 100),
470                script: Some(ir::Expression::Bytes(b"abce".to_vec())),
471            }]),
472        );
473
474        let tx = tx.apply().unwrap();
475
476        dbg!(&tx.find_params());
477        dbg!(&tx.find_queries());
478    }
479}