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 }
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}