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