use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::collections::{BTreeMap, HashMap};
use thiserror::Error;
use crate::{
core::{ArgMap, TirEnvelope},
tii::spec::{Profile, Transaction},
};
mod schema;
pub mod spec;
pub use schema::{ParamMap, ParamType, VariantCase};
#[derive(Debug, Error)]
pub enum Error {
#[error("invalid TII JSON: {0}")]
InvalidJson(#[from] serde_json::Error),
#[error("failed to read file: {0}")]
IoError(#[from] std::io::Error),
#[error("unknown tx: {0}")]
UnknownTx(String),
#[error("unknown profile: {0}")]
UnknownProfile(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Protocol {
spec: spec::TiiFile,
}
impl Protocol {
pub fn from_json(json: serde_json::Value) -> Result<Protocol, Error> {
let spec = serde_json::from_value(json)?;
Ok(Protocol { spec })
}
pub fn from_string(code: String) -> Result<Protocol, Error> {
let json = serde_json::from_str(&code)?;
Self::from_json(json)
}
pub fn from_file(path: impl AsRef<std::path::Path>) -> Result<Protocol, Error> {
let code = std::fs::read_to_string(path)?;
Self::from_string(code)
}
fn ensure_tx(&self, key: &str) -> Result<&Transaction, Error> {
let tx = self.spec.transactions.get(key);
let tx = tx.ok_or(Error::UnknownTx(key.to_string()))?;
Ok(tx)
}
fn ensure_profile(&self, key: &str) -> Result<&Profile, Error> {
let env = self
.spec
.profiles
.get(key)
.ok_or_else(|| Error::UnknownProfile(key.to_string()))?;
Ok(env)
}
pub fn invoke(&self, tx: &str, profile: Option<&str>) -> Result<Invocation, Error> {
let tx = self.ensure_tx(tx)?;
let profile = profile.map(|x| self.ensure_profile(x)).transpose()?;
let mut out = Invocation {
tir: tx.tir.clone(),
params: ParamMap::new(),
args: ArgMap::new(),
};
let components: HashMap<String, Value> = self
.spec
.components
.as_ref()
.map(|c| c.schemas.clone())
.unwrap_or_default();
for party in self.spec.parties.keys() {
out.params.insert(party.to_lowercase(), ParamType::Address);
}
if let Some(env) = &self.spec.environment {
out.params.extend(schema::params_from_schema(env, &components));
}
out.params.extend(schema::params_from_schema(&tx.params, &components));
if let Some(profile) = profile {
if let Some(env) = profile.environment.as_object() {
let values = env.clone();
out.set_args(values);
}
for (key, value) in profile.parties.iter() {
out.set_arg(key, json!(value));
}
}
Ok(out)
}
pub fn txs(&self) -> &HashMap<String, spec::Transaction> {
&self.spec.transactions
}
pub fn parties(&self) -> &HashMap<String, spec::Party> {
&self.spec.parties
}
pub fn profiles(&self) -> &HashMap<String, spec::Profile> {
&self.spec.profiles
}
pub fn client(self) -> crate::facade::Tx3ClientBuilder {
crate::facade::Tx3ClientBuilder::from_protocol(self)
}
}
pub struct InputQuery {}
pub type QueryMap = BTreeMap<String, InputQuery>;
#[derive(Debug, Clone)]
pub struct Invocation {
tir: TirEnvelope,
params: ParamMap,
args: ArgMap,
}
impl Invocation {
pub fn params(&mut self) -> &ParamMap {
&self.params
}
pub fn unspecified_params(&mut self) -> impl Iterator<Item = (&String, &ParamType)> {
self.params
.iter()
.filter(|(k, _)| !self.args.contains_key(k.as_str()))
}
pub fn set_arg(&mut self, name: &str, value: serde_json::Value) {
self.args.insert(name.to_lowercase().to_string(), value);
}
pub fn set_args(&mut self, args: ArgMap) {
self.args.extend(args);
}
pub fn with_arg(mut self, name: &str, value: serde_json::Value) -> Self {
self.args.insert(name.to_lowercase().to_string(), value);
self
}
pub fn with_args(mut self, args: ArgMap) -> Self {
self.args.extend(args);
self
}
pub fn into_resolve_request(self) -> Result<crate::trp::ResolveParams, Error> {
let args = self.args.clone().into_iter().collect();
let tir = self.tir.clone();
Ok(crate::trp::ResolveParams {
tir,
args,
env: None,
})
}
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use serde_json::json;
use super::*;
#[test]
fn happy_path_smoke_test() {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let tii = format!("{manifest_dir}/tests/fixtures/transfer.tii");
let protocol = Protocol::from_file(&tii).unwrap();
let invoke = protocol.invoke("transfer", Some("preprod")).unwrap();
let mut invoke = invoke
.with_arg("sender", json!("addr1abc"))
.with_arg("quantity", json!(100_000_000));
let all_params: HashSet<_> = invoke.params().keys().collect();
assert_eq!(all_params.len(), 5);
assert!(all_params.contains(&"sender".to_string()));
assert!(all_params.contains(&"middleman".to_string()));
assert!(all_params.contains(&"receiver".to_string()));
assert!(all_params.contains(&"tax".to_string()));
assert!(all_params.contains(&"quantity".to_string()));
let unspecified_params: HashSet<_> = invoke.unspecified_params().map(|(k, _)| k).collect();
assert_eq!(unspecified_params.len(), 2);
assert!(unspecified_params.contains(&"middleman".to_string()));
assert!(unspecified_params.contains(&"receiver".to_string()));
let tx = invoke.into_resolve_request().unwrap();
dbg!(&tx);
}
#[test]
fn invoke_interprets_complex_param_types() {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let tii = format!("{manifest_dir}/tests/fixtures/complex.tii");
let protocol = Protocol::from_file(&tii).unwrap();
let mut invoke = protocol.invoke("complex", None).unwrap();
let params = invoke.params();
assert!(matches!(params["quantity"], ParamType::Integer));
assert!(matches!(params["flag"], ParamType::Boolean));
assert!(matches!(params["nothing"], ParamType::Unit));
assert!(matches!(params["recipient"], ParamType::Address));
assert!(matches!(params["source"], ParamType::UtxoRef));
assert!(matches!(params["bag"], ParamType::AnyAsset));
assert!(matches!(params["sender"], ParamType::Address));
assert!(matches!(params["receiver"], ParamType::Address));
assert!(matches!(params["amounts"], ParamType::List(_)));
assert!(matches!(params["pair"], ParamType::Tuple(_)));
assert!(matches!(params["labels"], ParamType::Map(_)));
match ¶ms["asset"] {
ParamType::Record(fields) => assert!(matches!(fields["policy"], ParamType::Bytes)),
other => panic!("expected asset record, got {other:?}"),
}
match ¶ms["side"] {
ParamType::Variant(cases) => assert!(!cases.is_empty()),
other => panic!("expected side variant, got {other:?}"),
}
}
}