tx3_sdk/tii/
mod.rs

1use schemars::schema::{InstanceType, Schema, SingleOrVec};
2use serde::{Deserialize, Serialize};
3use serde_json::json;
4use std::collections::{BTreeMap, HashMap};
5use thiserror::Error;
6
7use crate::{
8    core::{ArgMap, TirEnvelope},
9    tii::spec::{Profile, Transaction},
10};
11
12pub mod spec;
13
14#[derive(Debug, Error)]
15pub enum Error {
16    #[error("invalid TII JSON: {0}")]
17    InvalidJson(#[from] serde_json::Error),
18
19    #[error("failed to read file: {0}")]
20    IoError(#[from] std::io::Error),
21
22    #[error("unknown tx: {0}")]
23    UnknownTx(String),
24
25    #[error("unknown profile: {0}")]
26    UnknownProfile(String),
27
28    #[error("invalid params schema")]
29    InvalidParamsSchema,
30
31    #[error("invalid param type")]
32    InvalidParamType,
33}
34
35fn params_from_schema(schema: Schema) -> Result<ParamMap, Error> {
36    let mut params = ParamMap::new();
37
38    let as_object = schema.into_object();
39
40    for (key, value) in as_object.object.unwrap().properties {
41        params.insert(key, ParamType::from_json_schema(value)?);
42    }
43
44    Ok(params)
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct Protocol {
49    spec: spec::TiiFile,
50}
51
52impl Protocol {
53    pub fn from_json(json: serde_json::Value) -> Result<Protocol, Error> {
54        let spec = serde_json::from_value(json)?;
55
56        Ok(Protocol { spec })
57    }
58
59    pub fn from_string(code: String) -> Result<Protocol, Error> {
60        let json = serde_json::from_str(&code)?;
61        Self::from_json(json)
62    }
63
64    pub fn from_file(path: impl AsRef<std::path::Path>) -> Result<Protocol, Error> {
65        let code = std::fs::read_to_string(path)?;
66        Self::from_string(code)
67    }
68
69    fn ensure_tx(&self, key: &str) -> Result<&Transaction, Error> {
70        let tx = self.spec.transactions.get(key);
71        let tx = tx.ok_or(Error::UnknownTx(key.to_string()))?;
72
73        Ok(tx)
74    }
75
76    fn ensure_profile(&self, key: &str) -> Result<&Profile, Error> {
77        let env = self
78            .spec
79            .profiles
80            .get(key)
81            .ok_or_else(|| Error::UnknownProfile(key.to_string()))?;
82
83        Ok(env)
84    }
85
86    pub fn invoke(&self, tx: &str, profile: Option<&str>) -> Result<Invocation, Error> {
87        let tx = self.ensure_tx(tx)?;
88
89        let profile = profile.map(|x| self.ensure_profile(x)).transpose()?;
90
91        let mut out = Invocation {
92            tir: tx.tir.clone(),
93            params: ParamMap::new(),
94            args: ArgMap::new(),
95        };
96
97        for party in self.spec.parties.keys() {
98            out.params.insert(party.to_lowercase(), ParamType::Address);
99        }
100
101        if let Some(env) = &self.spec.environment {
102            out.params.extend(params_from_schema(env.clone())?);
103        }
104
105        out.params.extend(params_from_schema(tx.params.clone())?);
106
107        if let Some(profile) = profile {
108            if let Some(env) = profile.environment.as_object() {
109                let values = env.clone();
110                out.set_args(values);
111            }
112
113            for (key, value) in profile.parties.iter() {
114                out.set_arg(&key, json!(value));
115            }
116        }
117
118        Ok(out)
119    }
120
121    pub fn txs(&self) -> &HashMap<String, spec::Transaction> {
122        &self.spec.transactions
123    }
124}
125
126#[derive(Debug, Clone)]
127pub enum ParamType {
128    Bytes,
129    Integer,
130    Boolean,
131    UtxoRef,
132    Address,
133    List(Box<ParamType>),
134    Custom(Schema),
135}
136
137impl ParamType {
138    fn from_json_type(instance_type: InstanceType) -> Result<ParamType, Error> {
139        match instance_type {
140            InstanceType::Integer => Ok(ParamType::Integer),
141            InstanceType::Boolean => Ok(ParamType::Boolean),
142            _ => Err(Error::InvalidParamType),
143        }
144    }
145
146    pub fn from_json_schema(schema: Schema) -> Result<ParamType, Error> {
147        let as_object = schema.into_object();
148
149        if let Some(reference) = &as_object.reference {
150            return match reference.as_str() {
151                "https://tx3.land/specs/v1beta0/core#Bytes" => Ok(ParamType::Bytes),
152                "https://tx3.land/specs/v1beta0/core#Address" => Ok(ParamType::Address),
153                "https://tx3.land/specs/v1beta0/core#UtxoRef" => Ok(ParamType::UtxoRef),
154                _ => Err(Error::InvalidParamType),
155            };
156        }
157
158        if let Some(inner) = as_object.instance_type {
159            return match inner {
160                SingleOrVec::Single(x) => Self::from_json_type(*x),
161                SingleOrVec::Vec(_) => Err(Error::InvalidParamType),
162            };
163        }
164
165        Err(Error::InvalidParamType)
166    }
167}
168
169pub struct InputQuery {}
170
171pub type ParamMap = HashMap<String, ParamType>;
172pub type QueryMap = BTreeMap<String, InputQuery>;
173
174#[derive(Debug, Clone)]
175pub struct Invocation {
176    tir: TirEnvelope,
177    params: ParamMap,
178    args: ArgMap,
179    // TODO: support explicit input specification
180    // input_override: HashMap<String, v1beta0::UtxoSet>,
181
182    // TODO: support explicit fee specification
183    // fee_override: Option<u64>,
184}
185
186impl Invocation {
187    pub fn params(&mut self) -> &ParamMap {
188        &self.params
189    }
190
191    pub fn unspecified_params(&mut self) -> impl Iterator<Item = (&String, &ParamType)> {
192        self.params
193            .iter()
194            .filter(|(k, _)| !self.args.contains_key(k.as_str()))
195    }
196
197    pub fn set_arg(&mut self, name: &str, value: serde_json::Value) {
198        self.args.insert(name.to_lowercase().to_string(), value);
199    }
200
201    pub fn set_args(&mut self, args: ArgMap) {
202        self.args.extend(args);
203    }
204
205    pub fn with_arg(mut self, name: &str, value: serde_json::Value) -> Self {
206        self.args.insert(name.to_lowercase().to_string(), value);
207        self
208    }
209
210    pub fn with_args(mut self, args: ArgMap) -> Self {
211        self.args.extend(args);
212        self
213    }
214
215    pub fn into_resolve_request(self) -> Result<crate::trp::ResolveParams, Error> {
216        let args = self
217            .args
218            .clone()
219            .into_iter()
220            .map(|(k, v)| (k, v.into()))
221            .collect();
222
223        let tir = self.tir.clone();
224
225        Ok(crate::trp::ResolveParams { tir, args })
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use std::collections::HashSet;
232
233    use serde_json::json;
234
235    use super::*;
236
237    #[test]
238    fn happy_path_smoke_test() {
239        let manifest_dir = env!("CARGO_MANIFEST_DIR");
240        let tii = format!("{manifest_dir}/../examples/transfer.tii");
241
242        let protocol = Protocol::from_file(&tii).unwrap();
243
244        let invoke = protocol.invoke("transfer", Some("preview")).unwrap();
245
246        let mut invoke = invoke
247            .with_arg("sender", json!("addr1abc"))
248            .with_arg("quantity", json!(100_000_000));
249
250        let all_params: HashSet<_> = invoke.params().keys().collect();
251
252        assert_eq!(all_params.len(), 5);
253        assert!(all_params.contains(&"sender".to_string()));
254        assert!(all_params.contains(&"middleman".to_string()));
255        assert!(all_params.contains(&"receiver".to_string()));
256        assert!(all_params.contains(&"tax".to_string()));
257        assert!(all_params.contains(&"quantity".to_string()));
258
259        let unspecified_params: HashSet<_> = invoke.unspecified_params().map(|(k, _)| k).collect();
260
261        assert_eq!(unspecified_params.len(), 1);
262        assert!(unspecified_params.contains(&"receiver".to_string()));
263
264        let tx = invoke.into_resolve_request().unwrap();
265
266        dbg!(&tx);
267    }
268}