1pub mod analyzing;
28pub mod applying;
29pub mod ast;
30pub mod backend;
31pub mod ir;
32pub mod loading;
33pub mod lowering;
34pub mod parsing;
35
36pub 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 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}