use std::path::PathBuf;
use std::time::Duration;
use crate::cli::commands::publish::{find_metadata_cbor, load_project_config};
use crate::cli::config::Config;
use crate::cli::util::get_default_metadata_server_url;
use anyhow::{Context, anyhow};
use clap::Parser;
use dialoguer::Confirm;
use tari_engine_types::published_template::PublishedTemplateAddress;
use tari_ootle_publish_lib::publisher::{SignedMetadataPayload, TemplatePublisher};
use tari_ootle_template_metadata::TemplateMetadata;
use url::Url;
const DEFAULT_MAX_RETRIES: u32 = 6;
const DEFAULT_INITIAL_BACKOFF_SECS: u64 = 10;
#[derive(Clone, Parser, Debug)]
pub struct PublishMetadataArgs {
#[arg(default_value = ".")]
pub path: PathBuf,
#[arg(long, short = 't')]
pub template_address: Option<PublishedTemplateAddress>,
#[arg(long)]
pub metadata_server_url: Option<Url>,
#[arg(long, default_value_t = DEFAULT_MAX_RETRIES)]
pub max_retries: u32,
#[arg(long)]
pub signed: bool,
#[arg(long, default_value_t = 0)]
pub key_index: u64,
#[arg(long)]
pub wallet_daemon_url: Option<url::Url>,
}
pub async fn handle(config: Config, args: PublishMetadataArgs) -> anyhow::Result<()> {
let cbor_path = find_metadata_cbor(&args.path).await?;
let mut cbor_bytes = std::fs::read(&cbor_path).context("reading metadata CBOR file")?;
let url_override = args.wallet_daemon_url.as_ref().or(config.wallet_daemon_url.as_ref());
let project_config = load_project_config(&args.path, url_override).await?;
let publisher = TemplatePublisher::new(project_config.network().clone());
let metadata_server_url = args
.metadata_server_url
.as_ref()
.or(project_config.metadata_server_url())
.or(config.metadata_server_url.as_ref())
.cloned();
let metadata_server_url = match metadata_server_url {
Some(url) => url,
None => {
let resp = publisher
.wallet_daemon_client()
.await?
.get_settings()
.await
.context("fetching network settings from wallet daemon")?;
let default_url = get_default_metadata_server_url(&resp.network.name)
.ok_or_else(|| anyhow!("no default metadata server for {}", resp.network.name))?;
let default_url: Url = default_url.parse().expect("parse default url");
default_url
},
};
let mut metadata =
TemplateMetadata::from_cbor(&cbor_bytes).context("metadata CBOR is invalid — cannot publish corrupt data")?;
let cargo_toml_path = args.path.join("Cargo.toml");
if cargo_toml_path.exists() {
match tari_ootle_template_metadata::from_cargo_toml(&cargo_toml_path) {
Ok(current) if current != metadata => {
println!("⚠️ Built metadata does not match Cargo.toml (metadata may be stale)");
let rebuild = Confirm::new()
.with_prompt("Rebuild to update metadata?")
.default(true)
.interact()?;
if rebuild {
crate::cli::commands::publish::build_template(&args.path).await?;
let new_cbor_path = find_metadata_cbor(&args.path).await?;
cbor_bytes = std::fs::read(&new_cbor_path).context("reading rebuilt metadata CBOR")?;
metadata = TemplateMetadata::from_cbor(&cbor_bytes).context("rebuilt metadata CBOR is invalid")?;
println!("✅ Metadata rebuilt");
}
},
Ok(_) => {},
Err(e) => {
println!("⚠️ Could not read Cargo.toml metadata for freshness check: {e}");
},
}
}
let addr = args
.template_address
.or_else(|| project_config.template_address().cloned())
.ok_or_else(|| {
anyhow!(
"No template address provided. Use --template-address or publish the template first \
(`tari publish`) to save the address in tari.config.toml."
)
})?;
println!(
"📄 Publishing metadata for {} v{} to {} (template: {})",
metadata.name, metadata.version, metadata_server_url, addr
);
if args.signed {
let payload = publisher
.sign_metadata_for_publish(args.key_index, addr.as_template_address(), metadata)
.await
.context("signing metadata via wallet daemon")?;
println!("🔑 Signed as author: {}", payload.public_key);
publish_metadata_signed(&metadata_server_url, &addr, &payload, args.max_retries).await
} else {
publish_metadata_to_server(&metadata_server_url, &addr, &cbor_bytes, args.max_retries).await
}
}
pub async fn publish_metadata_to_server(
server_url: &Url,
template_address: &PublishedTemplateAddress,
cbor_bytes: &[u8],
max_retries: u32,
) -> anyhow::Result<()> {
let addr = template_address.as_template_address();
let url = server_url
.join(&format!("/api/templates/{addr}/metadata"))
.context("building metadata endpoint URL")?;
let client = reqwest::Client::new();
let mut backoff = Duration::from_secs(DEFAULT_INITIAL_BACKOFF_SECS);
for attempt in 0..=max_retries {
let resp = client
.post(url.clone())
.header("Content-Type", "application/cbor")
.body(cbor_bytes.to_vec())
.send()
.await
.with_context(|| format!("POST {url}"))?;
let status = resp.status();
if status.is_success() {
println!("✅ Metadata published successfully");
return Ok(());
}
let body = resp.text().await.unwrap_or_default();
if status == reqwest::StatusCode::NOT_FOUND && attempt < max_retries {
println!(
"⏳ Template not yet synced by server (attempt {}/{}), retrying in {}s...",
attempt + 1,
max_retries + 1,
backoff.as_secs()
);
tokio::time::sleep(backoff).await;
backoff *= 2;
continue;
}
return Err(anyhow!("Metadata server returned {status}: {body}"));
}
unreachable!()
}
pub async fn publish_metadata_signed(
server_url: &Url,
template_address: &PublishedTemplateAddress,
payload: &SignedMetadataPayload,
max_retries: u32,
) -> anyhow::Result<()> {
let addr = template_address.as_template_address();
let url = server_url
.join(&format!("/api/templates/{addr}/metadata/signed"))
.context("building signed metadata endpoint URL")?;
let client = reqwest::Client::new();
let mut backoff = Duration::from_secs(DEFAULT_INITIAL_BACKOFF_SECS);
for attempt in 0..=max_retries {
let resp = client
.post(url.clone())
.header("Content-Type", "application/json")
.json(payload)
.send()
.await
.with_context(|| format!("POST {url}"))?;
let status = resp.status();
if status.is_success() {
println!("✅ Signed metadata published successfully");
return Ok(());
}
let resp_body = resp.text().await.unwrap_or_default();
if status == reqwest::StatusCode::NOT_FOUND && attempt < max_retries {
println!(
"⏳ Template not yet synced by server (attempt {}/{}), retrying in {}s...",
attempt + 1,
max_retries + 1,
backoff.as_secs()
);
tokio::time::sleep(backoff).await;
backoff *= 2;
continue;
}
return Err(anyhow!("Metadata server returned {status}: {resp_body}"));
}
unreachable!()
}