use std::path::PathBuf;
use std::time::Duration;
use crate::cli::commands::publish::{
find_metadata_cbor, load_project_config, resolve_active_network, resolve_wallet_daemon_url,
};
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_common_types::Network;
use tari_ootle_publish_lib::NetworkConfig;
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,
network_override: Option<Network>,
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 project_config = load_project_config(&args.path).await?;
let network = resolve_active_network(network_override, &project_config, &config);
let wallet_daemon_url =
resolve_wallet_daemon_url(args.wallet_daemon_url.as_ref(), &project_config, &config, network);
println!("🌐 Network: {network}");
let publisher = TemplatePublisher::new(NetworkConfig::new(wallet_daemon_url));
let metadata_server_url = args
.metadata_server_url
.as_ref()
.or(project_config.metadata_server_url(network))
.or(config.metadata_server_url(network))
.cloned();
let metadata_server_url = match metadata_server_url {
Some(url) => url,
None => {
let default_url = get_default_metadata_server_url(network)
.ok_or_else(|| anyhow!("no default metadata server for {network}"))?;
default_url.parse::<Url>().expect("parse 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(network).cloned())
.ok_or_else(|| {
anyhow!(
"No template address provided for network '{network}'. \
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!()
}