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