use async_trait::async_trait;
use serde_json::{json, Value};
use super::caatinga::{
dependencies_installed, placeholder_bindings, require_caatinga_project, run_caatinga,
DEPENDENCIES_BLOCKED,
};
use super::Tool;
pub struct GenerateBindingsTool;
#[async_trait]
impl Tool for GenerateBindingsTool {
fn name(&self) -> &str {
"generate_bindings"
}
fn capability(&self) -> crate::risk::Capability {
crate::risk::Capability::Write
}
fn description(&self) -> &str {
"Generate TypeScript bindings for contracts Caatinga has deployed, reading the contract \
ids from its artifacts. Covers every deployed contract unless one is named. Caatinga \
already does this after a deploy, so this is for regenerating them on demand. Only works \
in a Caatinga project."
}
fn input_schema(&self) -> Value {
json!({
"type": "object",
"properties": {
"contract": {
"type": "string",
"description": "Name of a contract from caatinga.config.ts. Omit for every deployed contract."
},
"network": {
"type": "string",
"description": "Network name as configured in caatinga.config.ts (e.g. testnet)"
},
"strict_network": {
"type": "boolean",
"description": "Fail instead of doing nothing when the network has no deployment artifacts"
}
},
"required": []
})
}
async fn execute(&self, input: Value) -> Result<String, String> {
require_caatinga_project().await?;
let mut args = vec!["generate".to_string()];
if let Some(contract) = input.get("contract").and_then(|v| v.as_str()) {
args.push(contract.to_string());
}
if let Some(network) = input.get("network").and_then(|v| v.as_str()) {
args.push("--network".to_string());
args.push(network.to_string());
}
if input.get("strict_network").and_then(|v| v.as_bool()) == Some(true) {
args.push("--strict-network".to_string());
}
if dependencies_installed().await == Some(false) {
return Err(DEPENDENCIES_BLOCKED.to_string());
}
let stdout = run_caatinga(&args).await?;
let stale = placeholder_bindings().await;
if !stale.is_empty() {
return Ok(format!(
"Bindings generated, but these are still the committed placeholders: {}. Every \
method in them throws PLACEHOLDER_BINDING. Caatinga only writes bindings for \
contracts it has deployed on the network asked for, so deploy them first \
(caatinga_deploy) and run this again with the matching --network.\n\n{}",
stale.join(", "),
stdout
));
}
Ok(format!("Bindings generated.\n\n{}", stdout))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_schema_takes_a_contract_name_not_an_id_or_a_path() {
let schema = GenerateBindingsTool.input_schema();
let props = schema["properties"].as_object().unwrap();
assert!(props.contains_key("contract"));
for absent in ["contract_id", "output_dir", "language"] {
assert!(
!props.contains_key(absent),
"{} must not be a parameter: it comes from the config or the artifacts",
absent
);
}
}
#[tokio::test]
async fn it_refuses_a_project_without_a_caatinga_config() {
let err = GenerateBindingsTool
.execute(json!({}))
.await
.expect_err("generate must refuse a non-Caatinga project");
assert!(err.contains("not a Caatinga project"), "{}", err);
}
}