pub mod cli_handler; pub mod config; pub mod grpc_brain; pub mod grpc_dtpm; pub mod packaging; pub mod utils; use crate::config::Config; use crate::snp; use crate::utils::shorten_string; use crate::{constants::HRATLS_APP_PORT, utils::block_on}; use detee_shared::{ app_proto::{ AppContract as AppContractPB, AppNodeFilters, AppNodeListResp, AppResource, ListAppContractsReq, NewAppRes, }, sgx::types::brain::Resource, }; use grpc_brain::get_one_app_node; use serde::{Deserialize, Serialize}; use std::sync::LazyLock; use tabled::Tabled; #[derive(thiserror::Error, Debug)] pub enum Error { #[error(transparent)] Config(#[from] crate::config::Error), #[error("Could not find a contract with the ID {0}")] AppContractNotFound(String), #[error("Brain returned the following error: {0}")] Brain(#[from] grpc_brain::Error), #[error("Could not read file from disk: {0}")] FileNotFound(#[from] std::io::Error), } #[derive(Tabled, Debug, Serialize, Deserialize)] pub struct AppContract { #[tabled(rename = "Location")] pub location: String, #[tabled(rename = "UUID pfx", display_with = "shorten_string")] pub uuid: String, pub name: String, #[tabled(rename = "Cores")] pub vcpu: u32, #[tabled(rename = "Mem (MB)")] pub memory_mb: u32, #[tabled(rename = "Disk (MB)")] pub disk_mb: u32, #[tabled(rename = "LP/h")] pub cost_h: String, #[tabled(rename = "time left", display_with = "display_mins")] pub time_left: u64, #[tabled(rename = "Node IP")] pub node_ip: String, #[tabled(rename = "Exposed ports", display_with = "display_ports")] pub exposed_host_ports: Vec<(u32, u32)>, } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct PublicIndex { packages: Vec, } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct PackageElement { package_name: String, package_url: String, launch_config_url: String, mr_enclave: [u8; 32], } pub static PACKAGES_INDEX: LazyLock = LazyLock::new(|| { PublicIndex { packages: vec![ PackageElement{ package_name: "actix-static-server".to_string(), package_url: "https://registry.detee.ltd/sgx/packages/actix-static-server_package_2025-04-16_21-27-07.tar.gz".to_string(), launch_config_url: "https://registry.detee.ltd/sgx/launch_configs/actix-static-server-launch-config_001.yaml".to_string(), mr_enclave: [97, 9, 55, 254, 254, 21, 143, 123, 239, 36, 47, 228, 8, 224, 114, 237, 159, 40, 32, 244, 54, 253, 126, 19, 13, 86, 42, 142, 248, 20, 89, 58], }, PackageElement{ package_name: "base-package".to_string(), package_url: "https://registry.detee.ltd/sgx/packages/base_package_2025-04-17_11-01-08.tar.gz".to_string(), launch_config_url: "https://registry.detee.ltd/sgx/launch_configs/base-package-launch-config_001.yaml".to_string(), mr_enclave: [52, 183, 102, 210, 251, 219, 218, 140, 168, 118, 10, 193, 98, 240, 147, 124, 240, 189, 46, 95, 138, 172, 15, 246, 227, 114, 70, 159, 232, 212, 9, 234], }, PackageElement{ package_name: "actix-app-info".to_string(), package_url: "https://registry.detee.ltd/sgx/packages/actix-app-info_package_2025-04-16_21-59-38.tar.gz".to_string(), launch_config_url: "https://registry.detee.ltd/sgx/launch_configs/actix-app-info-launch-config_001.yaml".to_string(), mr_enclave: [128, 0, 97, 103, 165, 103, 68, 203, 240, 145, 153, 254, 34, 129, 75, 140, 8, 186, 63, 226, 144, 129, 201, 187, 175, 66, 80, 1, 151, 114, 183, 159], }, PackageElement{ package_name: "go-app-info".to_string(), package_url: "https://registry.detee.ltd/sgx/packages/go-app-info_package_2025-04-16_21-39-18.tar.gz".to_string(), launch_config_url: "https://registry.detee.ltd/sgx/launch_configs/go-gin-app-info-launch-config_001.yaml".to_string(), mr_enclave: [188, 233, 211, 196, 237, 6, 46, 236, 229, 173, 239, 94, 99, 172, 233, 37, 255, 20, 54, 212, 172, 30, 182, 71, 219, 76, 78, 11, 72, 68, 46, 204], } ], } }); pub fn package_entry_from_name(package_name: &str) -> Option { PACKAGES_INDEX.packages.iter().find(|package| package.package_name == package_name).cloned() } fn display_mins(minutes: &u64) -> String { let mins = minutes % 60; let hours = minutes / 60; format!("{hours}h {mins}m") } fn display_ports(ports: &[(u32, u32)]) -> String { ports.iter().map(|port| format!("({}:{})", port.0, port.1,)).collect::>().join(", ") } impl crate::HumanOutput for Vec { fn human_cli_print(&self) { let style = tabled::settings::Style::rounded(); let mut table = tabled::Table::new(self); table.with(style); println!("{table}"); } } impl From for AppContract { fn from(brain_app_contract: AppContractPB) -> Self { let node_pubkey = brain_app_contract.node_pubkey.clone(); let location = match block_on(get_one_app_node(AppNodeFilters { node_pubkey: node_pubkey.clone(), ..Default::default() })) { Ok(node) => format!("{}, {} ({})", node.city, node.region, node.country), Err(e) => { log::warn!("Could not get information about node {node_pubkey} fram brain: {e:?}"); String::new() } }; let AppResource { vcpu, memory_mb, disk_mb, .. } = brain_app_contract.resource.unwrap_or_default(); let exposed_host_ports = brain_app_contract .mapped_ports .iter() .map(|port| (port.host_port, port.app_port)) .collect::>(); Self { location, uuid: brain_app_contract.uuid, name: brain_app_contract.app_name, vcpu, memory_mb, disk_mb, cost_h: format!( "{:.4}", (brain_app_contract.nano_per_minute * 60) as f64 / 1_000_000_000.0 ), time_left: brain_app_contract.locked_nano / brain_app_contract.nano_per_minute, node_ip: brain_app_contract.public_ipv4, exposed_host_ports, } } } pub async fn get_one_contract(uuid: &str) -> Result { let req = ListAppContractsReq { admin_pubkey: Config::get_detee_wallet()?, uuid: uuid.to_string(), ..Default::default() }; let contracts = grpc_brain::list_contracts(req).await?; if contracts.is_empty() { return Err(Error::AppContractNotFound(uuid.to_string())); } // let _ = write_uuid_list(&contracts); Ok(contracts[0].clone()) } #[derive(Debug, Serialize, Deserialize)] pub struct AppDeployResponse { pub status: String, pub uuid: String, pub name: String, pub node_ip: String, pub hratls_port: u32, pub error: String, } impl crate::HumanOutput for AppDeployResponse { fn human_cli_print(&self) { println!("The application got deployed under the UUID: {}", self.uuid); } } impl From<(NewAppRes, String)> for AppDeployResponse { fn from((value, name): (NewAppRes, String)) -> Self { Self { status: value.status, uuid: value.uuid, name, node_ip: value.ip_address, hratls_port: value .mapped_ports .iter() .find(|port| port.app_port == HRATLS_APP_PORT) .map(|port| port.host_port) .unwrap_or(HRATLS_APP_PORT), error: value.error, } } } #[derive(Debug, Serialize, Deserialize)] pub struct AppDeleteResponse { pub uuid: String, pub message: String, } impl crate::HumanOutput for AppDeleteResponse { fn human_cli_print(&self) { println!("App deleted successfully: UUID: {}", self.uuid); } } pub async fn get_app_node( resource: Resource, location: snp::deploy::Location, ) -> Result { let app_node_filter = AppNodeFilters { vcpus: resource.vcpu, memory_mb: resource.memory_mb, storage_mb: resource.disk_mb, country: location.country.clone().unwrap_or_default(), region: location.region.clone().unwrap_or_default(), city: location.city.clone().unwrap_or_default(), ip: location.node_ip.clone().unwrap_or_default(), node_pubkey: String::new(), }; get_one_app_node(app_node_filter).await } pub fn inspect_node(ip: String) -> Result { let req = AppNodeFilters { ip, ..Default::default() }; block_on(get_one_app_node(req)) } #[derive(Tabled, Debug, Serialize, Deserialize)] pub struct TabledAppNode { #[tabled(rename = "Operator")] pub operator: String, #[tabled(rename = "City, Region, Country")] pub location: String, #[tabled(rename = "IP")] pub public_ip: String, #[tabled(rename = "Price per unit")] pub price: String, #[tabled(rename = "Reports")] pub reports: usize, } impl From for TabledAppNode { fn from(brain_node: AppNodeListResp) -> Self { Self { operator: brain_node.operator, location: brain_node.city + ", " + &brain_node.region + ", " + &brain_node.country, public_ip: brain_node.ip, price: format!("{} nanoLP/min", brain_node.price), reports: brain_node.reports.len(), } } } impl super::HumanOutput for Vec { fn human_cli_print(&self) { let nodes: Vec = self.iter().map(|n| n.clone().into()).collect(); let style = tabled::settings::Style::rounded(); let mut table = tabled::Table::new(nodes); table.with(style); println!("{table}"); } } pub fn print_nodes() -> Result, grpc_brain::Error> { log::debug!("This will support flags in the future, but we have only one node atm."); let req = AppNodeFilters { ..Default::default() }; block_on(grpc_brain::get_app_node_list(req)) } pub async fn get_app_node_by_contract(uuid: &str) -> Result { let contract = get_one_contract(uuid).await?; Ok(get_one_app_node(AppNodeFilters { node_pubkey: contract.node_pubkey, ..Default::default() }) .await?) } fn write_uuid_list(app_contracts: &[AppContract]) -> Result<(), Error> { let app_uuid_list_path = Config::app_uuid_list_path()?; let mut file = std::fs::File::create(app_uuid_list_path)?; let output: String = app_contracts .iter() .map(|app| format!("{}\t{}", app.uuid, app.name).to_string()) .collect::>() .join("\n"); let output = output + "\n"; std::io::Write::write_all(&mut file, output.as_bytes())?; Ok(()) } pub fn append_uuid_list(uuid: &str, app_name: &str) -> Result<(), Error> { use std::{fs::OpenOptions, io::prelude::*}; let mut file = OpenOptions::new().create(true).append(true).open(Config::app_uuid_list_path()?).unwrap(); writeln!(file, "{uuid}\t{app_name}")?; Ok(()) }