diff --git a/Cargo.lock b/Cargo.lock index 270b8e54a5..3018163b98 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5541,6 +5541,7 @@ dependencies = [ "qrcode", "rand 0.7.3", "regex", + "reqwest", "rpassword", "rustyline", "serde", @@ -5571,6 +5572,7 @@ dependencies = [ "tui", "unicode-segmentation", "unicode-width", + "url 2.3.1", "zeroize", "zxcvbn", ] diff --git a/applications/tari_app_grpc/src/conversions/sidechain_feature.rs b/applications/tari_app_grpc/src/conversions/sidechain_feature.rs index ce1c83cf17..90f5da2ad4 100644 --- a/applications/tari_app_grpc/src/conversions/sidechain_feature.rs +++ b/applications/tari_app_grpc/src/conversions/sidechain_feature.rs @@ -54,7 +54,7 @@ impl From for grpc::side_chain_feature::SideChainFeature { SideChainFeature::ValidatorNodeRegistration(template_reg) => { grpc::side_chain_feature::SideChainFeature::ValidatorNodeRegistration(template_reg.into()) }, - SideChainFeature::TemplateRegistration(template_reg) => { + SideChainFeature::CodeTemplateRegistration(template_reg) => { grpc::side_chain_feature::SideChainFeature::TemplateRegistration(template_reg.into()) }, SideChainFeature::ConfidentialOutput(output_data) => { @@ -73,7 +73,7 @@ impl TryFrom for SideChainFeature { Ok(SideChainFeature::ValidatorNodeRegistration(vn_reg.try_into()?)) }, grpc::side_chain_feature::SideChainFeature::TemplateRegistration(template_reg) => { - Ok(SideChainFeature::TemplateRegistration(template_reg.try_into()?)) + Ok(SideChainFeature::CodeTemplateRegistration(template_reg.try_into()?)) }, grpc::side_chain_feature::SideChainFeature::ConfidentialOutput(output_data) => { Ok(SideChainFeature::ConfidentialOutput(output_data.try_into()?)) diff --git a/applications/tari_console_wallet/Cargo.toml b/applications/tari_console_wallet/Cargo.toml index 676da62142..316b589d70 100644 --- a/applications/tari_console_wallet/Cargo.toml +++ b/applications/tari_console_wallet/Cargo.toml @@ -40,6 +40,7 @@ log = { version = "0.4.8", features = ["std"] } qrcode = { version = "0.12" } rand = "0.7.3" regex = "1.5.4" +reqwest = "0.11.18" rpassword = "5.0" rustyline = "9.0" serde = "1.0.136" @@ -53,6 +54,7 @@ unicode-segmentation = "1.6.0" unicode-width = "0.1" zeroize = "1" zxcvbn = "2" +url = "2.3.1" [dependencies.tari_core] path = "../../base_layer/core" diff --git a/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs b/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs index fd70dbe2ea..4e3eeb590d 100644 --- a/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs +++ b/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs @@ -960,7 +960,7 @@ impl wallet_server::Wallet for WalletGrpcServer { let mut output = output_manager .create_output_with_features(1 * T, OutputFeatures { output_type: OutputType::CodeTemplateRegistration, - sidechain_feature: Some(SideChainFeature::TemplateRegistration(template_registration)), + sidechain_feature: Some(SideChainFeature::CodeTemplateRegistration(template_registration)), ..Default::default() }) .await diff --git a/applications/tari_console_wallet/src/ui/app.rs b/applications/tari_console_wallet/src/ui/app.rs index 34292d10e0..314a99f77f 100644 --- a/applications/tari_console_wallet/src/ui/app.rs +++ b/applications/tari_console_wallet/src/ui/app.rs @@ -42,6 +42,7 @@ use crate::{ network_tab::NetworkTab, notification_tab::NotificationTab, receive_tab::ReceiveTab, + register_template_tab::RegisterTemplateTab, send_tab::SendTab, tabs_container::TabsContainer, transactions_tab::TransactionsTab, @@ -90,6 +91,7 @@ impl App { .add("Send".into(), Box::new(SendTab::new(&app_state))) .add("Receive".into(), Box::new(ReceiveTab::new())) .add("Burn".into(), Box::new(BurnTab::new(&app_state))) + .add("Templates".into(), Box::new(RegisterTemplateTab::new(&app_state))) .add("Contacts".into(), Box::new(ContactsTab::new())) .add("Network".into(), Box::new(NetworkTab::new(base_node_selected))) .add("Events".into(), Box::new(EventsComponent::new())) diff --git a/applications/tari_console_wallet/src/ui/components/mod.rs b/applications/tari_console_wallet/src/ui/components/mod.rs index 1b54f78c51..6fc38299bd 100644 --- a/applications/tari_console_wallet/src/ui/components/mod.rs +++ b/applications/tari_console_wallet/src/ui/components/mod.rs @@ -36,6 +36,7 @@ pub use self::component::*; pub mod burn_tab; pub mod contacts_tab; pub mod events_component; +pub mod register_template_tab; #[derive(PartialEq, Eq)] pub enum KeyHandled { diff --git a/applications/tari_console_wallet/src/ui/components/register_template_tab.rs b/applications/tari_console_wallet/src/ui/components/register_template_tab.rs new file mode 100644 index 0000000000..a2dd8ca547 --- /dev/null +++ b/applications/tari_console_wallet/src/ui/components/register_template_tab.rs @@ -0,0 +1,861 @@ +// Copyright 2022 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use std::{path::Path, str::FromStr}; + +use digest::Digest; +use log::*; +use regex::Regex; +use reqwest::StatusCode; +use tari_core::transactions::{tari_amount::MicroTari, transaction_components::TemplateType}; +use tari_crypto::{hash::blake2::Blake256, hash_domain, hashing::DomainSeparation}; +use tari_utilities::hex::Hex; +use tari_wallet::output_manager_service::UtxoSelectionCriteria; +use tokio::{ + runtime::{Handle, Runtime}, + sync::watch, +}; +use tui::{ + backend::Backend, + layout::{Constraint, Direction, Layout, Rect}, + style::{Color, Modifier, Style}, + text::{Span, Spans}, + widgets::{Block, Borders, Paragraph, Wrap}, + Frame, +}; +use unicode_width::UnicodeWidthStr; +use url::Url; + +use crate::ui::{ + components::{balance::Balance, Component, KeyHandled}, + state::{AppState, UiTransactionSendStatus}, + widgets::draw_dialog, +}; + +const LOG_TARGET: &str = "wallet::console_wallet::register_template_tab "; + +fn maybe_extract_git_repo(git_url: &str) -> Option { + let url = match Url::parse(git_url) { + Ok(git_url) => git_url, + Err(_) => return None, + }; + + match url.domain() { + Some("github.com") | Some("bitbucket.org") | Some("gitlab.com") => url + .path_segments() + .map(|x| x.collect::>()) + .map(|segments| match segments.as_slice() { + &[owner, repo, ..] if url.domain().is_some() => Some(format!( + "{}://{}/{}/{}", + url.scheme(), + url.domain().unwrap(), + owner, + repo + )), + _ => None, + }) + .unwrap_or_default(), + + _ => None, + } +} + +fn maybe_extract_template_type(url: &str) -> Option { + let url = match Url::parse(url) { + Ok(url) => url, + Err(_) => return None, + }; + + if let Some(ext) = Path::new(url.path()).extension() { + match ext.to_ascii_uppercase().to_str()? { + "WASM" => Some("WASM:1".to_string()), + _ => None, + } + } else { + None + } +} + +fn maybe_extract_template_name_and_version(url: &str) -> (Option, Option) { + let url = match Url::parse(url) { + Ok(url) => url, + Err(_) => return (None, None), + }; + + if let Some(name) = Path::new(url.path()).file_stem() { + if let Some(name) = name.to_str() { + let regex = Regex::new(r"(.*)\.(\d+)").unwrap(); + if let Some(captures) = regex.captures(name) { + let first_part = captures.get(1).unwrap().as_str(); + let second_part = captures.get(2).unwrap().as_str(); + (Some(first_part.to_string()), Some(second_part.to_string())) + } else { + (Some(name.to_string()), None) + } + } else { + (None, None) + } + } else { + (None, None) + } +} + +pub struct RegisterTemplateTab { + balance: Balance, + input_mode: InputMode, + binary_url: String, + repository_url: String, + repository_commit_hash: String, + binary_checksum: String, + template_name: String, + template_version: String, + fee_per_gram: String, + template_type: String, + error_message: Option, + success_message: Option, + offline_message: Option, + result_watch: Option>, + confirmation_dialog: Option, +} + +impl RegisterTemplateTab { + pub fn new(app_state: &AppState) -> Self { + Self { + balance: Balance::new(), + input_mode: InputMode::None, + binary_url: String::new(), + repository_url: String::new(), + repository_commit_hash: String::new(), + binary_checksum: String::new(), + template_version: String::new(), + template_name: String::new(), + error_message: None, + success_message: None, + offline_message: None, + result_watch: None, + confirmation_dialog: None, + template_type: String::new(), + fee_per_gram: app_state.get_default_fee_per_gram().as_u64().to_string(), + } + } + + #[allow(clippy::too_many_lines)] + fn draw_form(&self, f: &mut Frame, area: Rect, _app_state: &AppState) + where B: Backend { + let block = Block::default().borders(Borders::ALL).title(Span::styled( + "Register Code Template", + Style::default().fg(Color::White).add_modifier(Modifier::BOLD), + )); + f.render_widget(block, area); + + let form_layout = Layout::default() + .constraints( + [ + Constraint::Length(4), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + ] + .as_ref(), + ) + .margin(1) + .split(area); + + let instructions = Paragraph::new(vec![ + Spans::from(vec![ + Span::raw("Press "), + Span::styled("B", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to edit "), + Span::styled("Binary URL", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" field, "), + Span::styled("U", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to edit "), + Span::styled("Git Repository URL", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" field, "), + Span::styled("H", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to edit "), + Span::styled( + "Git Repository Commit Hash", + Style::default().add_modifier(Modifier::BOLD), + ), + Span::raw(" field, "), + Span::styled("N", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to edit "), + Span::styled("Template Name", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" and "), + Span::styled("V", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to edit "), + Span::styled("Template Version", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" field, "), + Span::styled("T", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to edit "), + Span::styled("Template Type", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" field, "), + Span::styled("F", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to edit "), + Span::styled("Fee-per-gram", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" field."), + ]), + Spans::from(vec![ + Span::raw("Press "), + Span::styled("S", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to send a template registration transaction."), + ]), + ]) + .wrap(Wrap { trim: false }) + .block(Block::default()); + f.render_widget(instructions, form_layout[0]); + + // ---------------------------------------------------------------------------- + // layouts + // ---------------------------------------------------------------------------- + + let first_row_layout = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(100)].as_ref()) + .split(form_layout[1]); + + let second_row_layout = Layout::default() + .direction(Direction::Horizontal) + .constraints( + [ + Constraint::Percentage(50), + Constraint::Percentage(25), + Constraint::Percentage(25), + ] + .as_ref(), + ) + .split(form_layout[2]); + + let third_row_layout = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(100)].as_ref()) + .split(form_layout[3]); + + let fourth_row_layout = Layout::default() + .direction(Direction::Horizontal) + .constraints( + [ + Constraint::Percentage(40), + Constraint::Percentage(40), + Constraint::Percentage(20), + ] + .as_ref(), + ) + .split(form_layout[4]); + + // ---------------------------------------------------------------------------- + // First row - Binary URL + // ---------------------------------------------------------------------------- + + let binary_url = Paragraph::new(self.binary_url.as_ref()) + .style(match self.input_mode { + InputMode::BinaryUrl => Style::default().fg(Color::Magenta), + _ => Style::default(), + }) + .block(Block::default().borders(Borders::ALL).title("(B)inary URL:")); + f.render_widget(binary_url, first_row_layout[0]); + + // ---------------------------------------------------------------------------- + // Second row - Template Name, Template Version, Template Type + // ---------------------------------------------------------------------------- + + let template_name = Paragraph::new(self.template_name.as_ref()) + .style(match self.input_mode { + InputMode::TemplateName => Style::default().fg(Color::Magenta), + _ => Style::default(), + }) + .block(Block::default().borders(Borders::ALL).title("Template (N)ame:")); + f.render_widget(template_name, second_row_layout[0]); + + let template_version = Paragraph::new(self.template_version.to_string()) + .style(match self.input_mode { + InputMode::TemplateVersion => Style::default().fg(Color::Magenta), + _ => Style::default(), + }) + .block(Block::default().borders(Borders::ALL).title("Template (V)ersion:")); + f.render_widget(template_version, second_row_layout[1]); + + let template_type = Paragraph::new(self.template_type.as_ref()) + .style(match self.input_mode { + InputMode::TemplateType => Style::default().fg(Color::Magenta), + _ => Style::default(), + }) + .block(Block::default().borders(Borders::ALL).title("Template (T)ype:")); + f.render_widget(template_type, second_row_layout[2]); + + // ---------------------------------------------------------------------------- + // Third row - Repository URL + // ---------------------------------------------------------------------------- + + let repository_url = Paragraph::new(self.repository_url.as_ref()) + .style(match self.input_mode { + InputMode::RepositoryUrl => Style::default().fg(Color::Magenta), + _ => Style::default(), + }) + .block(Block::default().borders(Borders::ALL).title("Repository (U)RL:")); + f.render_widget(repository_url, third_row_layout[0]); + + // ---------------------------------------------------------------------------- + // Fourth row - Binary checksum, Repository Commit Hash, Fee per gram + // ---------------------------------------------------------------------------- + + let binary_checksum = Paragraph::new(self.binary_checksum.as_ref()) + .style(Style::default().fg(Color::Gray)) + .block(Block::default().borders(Borders::ALL).title("Binary Checksum:")); + f.render_widget(binary_checksum, fourth_row_layout[0]); + + let repository_commit_hash = Paragraph::new(self.repository_commit_hash.as_ref()) + .style(match self.input_mode { + InputMode::RepositoryCommitHash => Style::default().fg(Color::Magenta), + _ => Style::default(), + }) + .block( + Block::default() + .borders(Borders::ALL) + .title("Repository Commit (H)ash:"), + ); + f.render_widget(repository_commit_hash, fourth_row_layout[1]); + + let fee_per_gram = Paragraph::new(self.fee_per_gram.as_ref()) + .style(match self.input_mode { + InputMode::FeePerGram => Style::default().fg(Color::Magenta), + _ => Style::default(), + }) + .block(Block::default().borders(Borders::ALL).title("(F)ee-per-gram:")); + f.render_widget(fee_per_gram, fourth_row_layout[2]); + + // ---------------------------------------------------------------------------- + // field cursor placement + // ---------------------------------------------------------------------------- + + match self.input_mode { + InputMode::None => (), + InputMode::FeePerGram => f.set_cursor( + fourth_row_layout[2].x + self.fee_per_gram.width() as u16 + 1, + fourth_row_layout[2].y + 1, + ), + InputMode::TemplateName => f.set_cursor( + second_row_layout[0].x + self.template_name.width() as u16 + 1, + second_row_layout[0].y + 1, + ), + InputMode::TemplateVersion => f.set_cursor( + second_row_layout[1].x + self.template_version.width() as u16 + 1, + second_row_layout[1].y + 1, + ), + InputMode::TemplateType => f.set_cursor( + second_row_layout[2].x + self.template_type.width() as u16 + 1, + second_row_layout[2].y + 1, + ), + InputMode::BinaryUrl => f.set_cursor( + first_row_layout[0].x + self.binary_url.width() as u16 + 1, + first_row_layout[0].y + 1, + ), + InputMode::RepositoryUrl => f.set_cursor( + third_row_layout[0].x + self.repository_url.width() as u16 + 1, + third_row_layout[0].y + 1, + ), + InputMode::RepositoryCommitHash => f.set_cursor( + fourth_row_layout[1].x + self.repository_commit_hash.width() as u16 + 1, + fourth_row_layout[1].y + 1, + ), + } + } + + #[allow(clippy::too_many_lines)] + fn on_key_confirmation_dialog(&mut self, c: char, app_state: &mut AppState) -> KeyHandled { + match self.confirmation_dialog { + Some(ConfirmationDialogType::AutoFill) => { + match c { + 'n' => { + self.confirmation_dialog = None; + self.input_mode = InputMode::TemplateName; + }, + 'y' => { + self.input_mode = InputMode::RepositoryCommitHash; + let (template_name, template_version) = + maybe_extract_template_name_and_version(self.binary_url.as_str()); + if self.repository_url.is_empty() { + if let Some(repository_url) = maybe_extract_git_repo(self.binary_url.as_str()) { + self.repository_url = repository_url; + } else { + self.input_mode = InputMode::RepositoryUrl; + } + } + + if self.template_type.is_empty() { + if let Some(template_type) = maybe_extract_template_type(self.binary_url.as_str()) { + self.template_type = template_type; + } else { + self.input_mode = InputMode::TemplateType; + } + } + + if self.template_version.is_empty() { + if let Some(template_version) = template_version { + self.template_version = template_version; + } else { + self.input_mode = InputMode::TemplateVersion; + } + } + + if self.template_name.is_empty() { + if let Some(template_name) = template_name { + self.template_name = template_name; + } else { + self.input_mode = InputMode::TemplateName; + } + } + self.confirmation_dialog = None; + }, + _ => (), + } + KeyHandled::Handled + }, + Some(ConfirmationDialogType::Normal) => match c { + 'n' => { + self.confirmation_dialog = None; + KeyHandled::Handled + }, + 'y' => { + let template_version = if let Ok(version) = self.template_version.parse::() { + version + } else { + self.confirmation_dialog = None; + self.error_message = + Some("Template version should be an integer\nPress Enter to continue.".to_string()); + return KeyHandled::Handled; + }; + + let template_type = match self.template_type.to_lowercase().as_str() { + "flow" => TemplateType::Flow, + "manifest" => TemplateType::Manifest, + s => match s.split(':').collect::>().as_slice() { + &[typ, abi_version] if &typ.to_lowercase() == "wasm" => { + let abi_version = match abi_version.parse::() { + Ok(abi_version) => abi_version, + Err(_) => { + self.confirmation_dialog = None; + self.error_message = Some(format!( + "Invalid `abi_version` for the `wasm` template type\n{}\nPress Enter to \ + continue.", + self.template_type + )); + return KeyHandled::Handled; + }, + }; + + TemplateType::Wasm { abi_version } + }, + + _ => { + self.confirmation_dialog = None; + self.error_message = Some(format!( + "Unrecognized template type\n{}\nPress Enter to continue.", + self.template_type + )); + return KeyHandled::Handled; + }, + }, + }; + + let fee_per_gram = if let Ok(fee_per_gram) = MicroTari::from_str(self.fee_per_gram.as_str()) { + fee_per_gram + } else { + self.confirmation_dialog = None; + self.error_message = + Some("Fee-per-gram should be an integer\nPress Enter to continue.".to_string()); + return KeyHandled::Handled; + }; + + let (tx, rx) = watch::channel(UiTransactionSendStatus::Initiated); + + let mut reset_fields = false; + + match Handle::current().block_on(app_state.register_code_template( + self.template_name.clone(), + template_version, + template_type, + self.binary_url.clone(), + self.binary_checksum.clone(), + self.repository_url.clone(), + self.repository_commit_hash.clone(), + fee_per_gram, + UtxoSelectionCriteria::default(), + tx, + )) { + Err(e) => { + self.confirmation_dialog = None; + self.error_message = Some(format!( + "Failed to register code template:\n{:?}\nPress Enter to continue.", + e + )) + }, + Ok(_) => { + Handle::current().block_on(app_state.update_cache()); + reset_fields = true + }, + } + + if reset_fields { + self.input_mode = InputMode::None; + self.result_watch = Some(rx); + } + + self.confirmation_dialog = None; + KeyHandled::Handled + }, + _ => KeyHandled::Handled, + }, + None => KeyHandled::NotHandled, + } + } + + fn on_key_send_input(&mut self, c: char) -> KeyHandled { + if self.input_mode != InputMode::None { + match self.input_mode { + InputMode::None => (), + InputMode::BinaryUrl => match c { + '\n' => { + let rt = Runtime::new().expect("Failed to start tokio runtime"); + let url = self.binary_url.clone(); + let mut error = None; + let mut hex_string = String::new(); + rt.block_on(async { + let data = reqwest::get(url).await; + match data { + Ok(data) => match data.status() { + StatusCode::OK => match data.bytes().await { + Ok(bytes) => { + let mut hasher = Blake256::new(); + hash_domain!(TariEngineHashDomain, "tari.dan.engine", 0); + TariEngineHashDomain::add_domain_separation_tag(&mut hasher, "Template"); + let hash: [u8; 32] = hasher.chain(bytes).finalize().into(); + hex_string = hash.to_hex(); + }, + Err(e) => { + error = Some(format!("Error {:?}\nPress Enter to continue.", e)); + }, + }, + code => { + error = Some(format!("Error {:?}\nPress Enter to continue.", code)); + }, + }, + Err(e) => { + error = Some(format!("Error {:?}\nPress Enter to continue.", e)); + }, + } + }); + if error.is_some() { + self.error_message = error; + } else { + self.confirmation_dialog = Some(ConfirmationDialogType::AutoFill); + self.binary_checksum = hex_string; + self.input_mode = InputMode::None; + } + }, + c => { + self.binary_url.push(c); + return KeyHandled::Handled; + }, + }, + InputMode::TemplateName => match c { + '\n' => self.input_mode = InputMode::TemplateVersion, + c => { + self.template_name.push(c); + return KeyHandled::Handled; + }, + }, + InputMode::TemplateVersion => match c { + '\n' => self.input_mode = InputMode::TemplateType, + c => { + self.template_version.push(c); + return KeyHandled::Handled; + }, + }, + InputMode::TemplateType => match c { + '\n' => self.input_mode = InputMode::RepositoryUrl, + c => { + self.template_type.push(c.to_uppercase().collect::>()[0]); + return KeyHandled::Handled; + }, + }, + + InputMode::RepositoryUrl => match c { + '\n' => self.input_mode = InputMode::RepositoryCommitHash, + c => { + self.repository_url.push(c); + return KeyHandled::Handled; + }, + }, + InputMode::RepositoryCommitHash => match c { + '\n' => self.input_mode = InputMode::FeePerGram, + c => { + if c.is_numeric() || ('a'..='f').contains(&c) || ('A'..='F').contains(&c) { + self.repository_commit_hash.push(c); + } + return KeyHandled::Handled; + }, + }, + InputMode::FeePerGram => match c { + '\n' => self.input_mode = InputMode::None, + c => { + if c.is_numeric() || ['t', 'T', 'u', 'U'].contains(&c) { + self.fee_per_gram.push(c); + } + return KeyHandled::Handled; + }, + }, + } + } + + KeyHandled::NotHandled + } +} + +impl Component for RegisterTemplateTab { + #[allow(clippy::too_many_lines)] + fn draw(&mut self, f: &mut Frame, area: Rect, app_state: &AppState) { + let areas = Layout::default() + .constraints( + [ + Constraint::Length(3), + Constraint::Length(18), + Constraint::Min(42), + Constraint::Length(1), + Constraint::Length(1), + ] + .as_ref(), + ) + .split(area); + + self.balance.draw(f, areas[0], app_state); + self.draw_form(f, areas[1], app_state); + + let rx_option = self.result_watch.take(); + if let Some(rx) = rx_option { + trace!(target: LOG_TARGET, "{:?}", (*rx.borrow()).clone()); + let status = match (*rx.borrow()).clone() { + UiTransactionSendStatus::Initiated => "Initiated", + UiTransactionSendStatus::Error(e) => { + self.error_message = Some(format!("Error sending transaction: {}, Press Enter to continue.", e)); + return; + }, + UiTransactionSendStatus::TransactionComplete => { + self.fee_per_gram = app_state.get_default_fee_per_gram().as_u64().to_string(); + self.template_name = "".to_string(); + self.template_type = "".to_string(); + self.binary_url = "".to_string(); + self.binary_checksum = "".to_string(); + self.repository_url = "".to_string(); + self.repository_commit_hash = "".to_string(); + self.success_message = + Some("Transaction completed successfully!\nPlease press Enter to continue".to_string()); + return; + }, + status => { + warn!("unhandled transaction status {:?}", status); + return; + }, + }; + draw_dialog( + f, + area, + "Please Wait".to_string(), + format!("Template Registration Status: {}", status), + Color::Green, + 120, + 10, + ); + self.result_watch = Some(rx); + } + + if let Some(msg) = self.success_message.clone() { + draw_dialog(f, area, "Success!".to_string(), msg, Color::Green, 120, 9); + } + + if let Some(msg) = self.offline_message.clone() { + draw_dialog(f, area, "Offline!".to_string(), msg, Color::Green, 120, 9); + } + + match self.confirmation_dialog { + None => (), + Some(ConfirmationDialogType::AutoFill) => draw_dialog( + f, + area, + "Confirm autofill".to_string(), + "Do you want to autofill (if possible) empty fields from the binary URL?\n(Y)es / (N)o".to_string(), + Color::Blue, + 120, + 9, + ), + Some(ConfirmationDialogType::Normal) => { + draw_dialog( + f, + area, + "Confirm Code Template Registration".to_string(), + "Are you sure you want to register this template?\n(Y)es / (N)o".to_string(), + Color::Red, + 120, + 9, + ); + }, + } + + if let Some(msg) = self.error_message.clone() { + draw_dialog(f, area, "Error!".to_string(), msg, Color::Red, 120, 9); + } + } + + fn on_key(&mut self, app_state: &mut AppState, c: char) { + if self.error_message.is_some() { + if '\n' == c { + self.error_message = None; + } + return; + } + + if self.success_message.is_some() { + if '\n' == c { + self.success_message = None; + } + return; + } + + if self.offline_message.is_some() { + if '\n' == c { + self.offline_message = None; + } + return; + } + + if self.result_watch.is_some() { + return; + } + + if self.on_key_confirmation_dialog(c, app_state) == KeyHandled::Handled { + return; + } + + if self.on_key_send_input(c) == KeyHandled::Handled { + return; + } + + match c { + 'f' => self.input_mode = InputMode::FeePerGram, + 'n' => self.input_mode = InputMode::TemplateName, + 'v' => self.input_mode = InputMode::TemplateVersion, + 't' => self.input_mode = InputMode::TemplateType, + 'b' => self.input_mode = InputMode::BinaryUrl, + 'u' => self.input_mode = InputMode::RepositoryUrl, + 'h' => self.input_mode = InputMode::RepositoryCommitHash, + 's' => { + // ---------------------------------------------------------------------------- + // basic field value validation + // ---------------------------------------------------------------------------- + + if self.template_name.is_empty() { + self.error_message = Some("Template Name is empty\nPress Enter to continue.".to_string()); + return; + } + + if self.template_version.is_empty() { + self.error_message = Some("Template Version is empty\nPress Enter to continue.".to_string()); + return; + } + + if self.template_type.is_empty() { + self.error_message = Some("Template Type is empty\nPress Enter to continue.".to_string()); + return; + } + + if self.binary_url.is_empty() { + self.error_message = Some("Binary URL is empty\nPress Enter to continue.".to_string()); + return; + } + + if self.binary_checksum.is_empty() { + self.error_message = Some("Binary Checksum is empty\nPress Enter to continue.".to_string()); + return; + } + + if self.repository_url.is_empty() { + self.error_message = Some("Repository URL is empty\nPress Enter to continue.".to_string()); + return; + } + + if self.repository_commit_hash.is_empty() { + self.error_message = Some("Repository Commit Hash is empty\nPress Enter to continue.".to_string()); + return; + } + + if self.fee_per_gram.parse::().is_err() { + self.error_message = + Some("Fee-Per-Gram should be a valid amount of Tari\nPress Enter to continue.".to_string()); + return; + } + + self.confirmation_dialog = Some(ConfirmationDialogType::Normal); + }, + _ => {}, + } + } + + fn on_up(&mut self, _app_state: &mut AppState) {} + + fn on_down(&mut self, _app_state: &mut AppState) {} + + fn on_esc(&mut self, _: &mut AppState) { + if self.confirmation_dialog.is_some() { + return; + } + + self.input_mode = InputMode::None; + } + + fn on_backspace(&mut self, _app_state: &mut AppState) { + match self.input_mode { + InputMode::TemplateName => { + let _ = self.template_name.pop(); + }, + InputMode::TemplateVersion => { + let _ = self.template_version.pop(); + }, + InputMode::TemplateType => { + let _ = self.template_type.pop(); + }, + InputMode::BinaryUrl => { + let _ = self.binary_url.pop(); + }, + InputMode::RepositoryUrl => { + let _ = self.repository_url.pop(); + }, + InputMode::RepositoryCommitHash => { + let _ = self.repository_commit_hash.pop(); + }, + InputMode::FeePerGram => { + let _ = self.fee_per_gram.pop(); + }, + InputMode::None => {}, + } + } +} + +#[derive(PartialEq, Debug)] +enum InputMode { + None, + TemplateName, + TemplateVersion, + TemplateType, + BinaryUrl, + RepositoryUrl, + RepositoryCommitHash, + FeePerGram, +} + +#[derive(PartialEq, Debug)] +enum ConfirmationDialogType { + AutoFill, + Normal, +} diff --git a/applications/tari_console_wallet/src/ui/components/send_tab.rs b/applications/tari_console_wallet/src/ui/components/send_tab.rs index f4fec78a35..98da365280 100644 --- a/applications/tari_console_wallet/src/ui/components/send_tab.rs +++ b/applications/tari_console_wallet/src/ui/components/send_tab.rs @@ -78,6 +78,8 @@ impl SendTab { Constraint::Length(3), Constraint::Length(3), Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), ] .as_ref(), ) diff --git a/applications/tari_console_wallet/src/ui/state/app_state.rs b/applications/tari_console_wallet/src/ui/state/app_state.rs index 2728659f0d..42b368e4ca 100644 --- a/applications/tari_console_wallet/src/ui/state/app_state.rs +++ b/applications/tari_console_wallet/src/ui/state/app_state.rs @@ -46,7 +46,7 @@ use tari_comms::{ use tari_contacts::contacts_service::{handle::ContactsLivenessEvent, types::Contact}; use tari_core::transactions::{ tari_amount::{uT, MicroTari}, - transaction_components::OutputFeatures, + transaction_components::{OutputFeatures, TemplateType}, weight::TransactionWeight, }; use tari_shutdown::ShutdownSignal; @@ -74,7 +74,12 @@ use crate::{ ui::{ state::{ debouncer::BalanceEnquiryDebouncer, - tasks::{send_burn_transaction_task, send_one_sided_transaction_task, send_transaction_task}, + tasks::{ + send_burn_transaction_task, + send_one_sided_transaction_task, + send_register_template_transaction_task, + send_transaction_task, + }, wallet_event_monitor::WalletEventMonitor, }, ui_burnt_proof::UiBurntProof, @@ -437,6 +442,41 @@ impl AppState { Ok(()) } + pub async fn register_code_template( + &mut self, + template_name: String, + template_version: u16, + template_type: TemplateType, + binary_url: String, + binary_sha: String, + repository_url: String, + repository_commit_hash: String, + fee_per_gram: MicroTari, + selection_criteria: UtxoSelectionCriteria, + result_tx: watch::Sender, + ) -> Result<(), UiError> { + let inner = self.inner.write().await; + let tx_service_handle = inner.wallet.transaction_service.clone(); + + send_register_template_transaction_task( + template_name, + template_version, + template_type, + repository_url, + repository_commit_hash, + binary_url, + binary_sha, + fee_per_gram, + selection_criteria, + tx_service_handle, + inner.wallet.db.clone(), + result_tx, + ) + .await; + + Ok(()) + } + pub async fn cancel_transaction(&mut self, tx_id: TxId) -> Result<(), UiError> { let inner = self.inner.write().await; let mut tx_service_handle = inner.wallet.transaction_service.clone(); diff --git a/applications/tari_console_wallet/src/ui/state/tasks.rs b/applications/tari_console_wallet/src/ui/state/tasks.rs index d2c8c9b2c9..31a877a2bd 100644 --- a/applications/tari_console_wallet/src/ui/state/tasks.rs +++ b/applications/tari_console_wallet/src/ui/state/tasks.rs @@ -20,12 +20,24 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use std::path::PathBuf; +use std::{convert::TryFrom, path::PathBuf}; -use log::warn; -use rand::random; -use tari_common_types::{tari_address::TariAddress, types::PublicKey}; -use tari_core::transactions::{tari_amount::MicroTari, transaction_components::OutputFeatures}; +use log::{error, warn}; +use rand::{random, rngs::OsRng}; +use tari_common_types::{ + tari_address::TariAddress, + types::{FixedHash, PublicKey, Signature}, +}; +use tari_core::{ + consensus::{DomainSeparatedConsensusHasher, MaxSizeBytes, MaxSizeString}, + transactions::{ + tari_amount::MicroTari, + transaction_components::{BuildInfo, OutputFeatures, TemplateType}, + TransactionHashDomain, + }, +}; +use tari_crypto::{hash::blake2::Blake256, keys::PublicKey as PublicKeyTrait, ristretto::RistrettoSecretKey}; +use tari_key_manager::key_manager::KeyManager; use tari_utilities::{hex::Hex, ByteArray}; use tari_wallet::{ output_manager_service::UtxoSelectionCriteria, @@ -324,3 +336,174 @@ pub async fn send_burn_transaction_task( } } } + +#[allow(clippy::too_many_arguments, clippy::too_many_lines)] +pub async fn send_register_template_transaction_task( + template_name: String, + template_version: u16, + template_type: TemplateType, + repository_url: String, + repository_commit_hash: String, + binary_url: String, + binary_sha: String, + fee_per_gram: MicroTari, + _selection_criteria: UtxoSelectionCriteria, + mut transaction_service_handle: TransactionServiceHandle, + _db: WalletDatabase, + result_tx: watch::Sender, +) { + result_tx.send(UiTransactionSendStatus::Initiated).unwrap(); + let mut event_stream = transaction_service_handle.get_event_stream(); + + // ---------------------------------------------------------------------------- + // preparing data + // ---------------------------------------------------------------------------- + + let template_name = match MaxSizeString::<32>::try_from(template_name) { + Err(e) => { + error!(target: LOG_TARGET, "failed to process `template_name`: {}", e); + result_tx + .send(UiTransactionSendStatus::Error(format!("Template name error: {}", e))) + .unwrap(); + return; + }, + Ok(template_name) => template_name, + }; + + let binary_url = match MaxSizeString::<255>::try_from(binary_url) { + Ok(binary_url) => binary_url, + Err(e) => { + error!(target: LOG_TARGET, "failed to process `binary_url`: {}", e); + result_tx + .send(UiTransactionSendStatus::Error(format!("Binary url error: {}", e))) + .unwrap(); + return; + }, + }; + let binary_sha = match MaxSizeBytes::<32>::try_from(binary_sha) { + Ok(binary_sha) => binary_sha, + Err(e) => { + error!(target: LOG_TARGET, "failed to process `binary_sha`: {}", e); + result_tx + .send(UiTransactionSendStatus::Error(format!("Binary checksum error: {}", e))) + .unwrap(); + return; + }, + }; + + let repository_url = match MaxSizeString::<255>::try_from(repository_url) { + Ok(repository_url) => repository_url, + Err(e) => { + error!(target: LOG_TARGET, "failed to process `repository_url`: {}", e); + result_tx + .send(UiTransactionSendStatus::Error(format!("Repository url error: {}", e))) + .unwrap(); + return; + }, + }; + + let repository_commit_hash = match MaxSizeBytes::<32>::try_from(repository_commit_hash) { + Ok(repository_commit_hash) => repository_commit_hash, + Err(e) => { + error!(target: LOG_TARGET, "failed to process `repository_commit_hash`: {}", e); + result_tx + .send(UiTransactionSendStatus::Error(format!( + "Repository commit hash error: {}", + e + ))) + .unwrap(); + return; + }, + }; + + // ---------------------------------------------------------------------------- + // signing and sending code template registration request + // ---------------------------------------------------------------------------- + + let mut km = KeyManager::::new(); + + let author_private_key = match km.next_key() { + Ok(secret_key) => secret_key.k, + Err(e) => { + error!(target: LOG_TARGET, "failed to generate key: {}", e); + result_tx.send(UiTransactionSendStatus::Error(e.to_string())).unwrap(); + return; + }, + }; + + let author_public_key = PublicKey::from_secret_key(&author_private_key); + let (secret_nonce, public_nonce) = PublicKey::random_keypair(&mut OsRng); + let challenge = FixedHash::from( + DomainSeparatedConsensusHasher::::new("template_registration") + .chain(&author_public_key) + .chain(&public_nonce) + .chain(&binary_sha) + .chain(&b"") + .finalize(), + ); + + let author_signature = Signature::sign_raw(&author_private_key, secret_nonce, &*challenge) + .expect("Sign cannot fail with 32-byte challenge and a RistrettoPublicKey"); + + // ---------------------------------------------------------------------------- + // ============================================================================ + // ---------------------------------------------------------------------------- + + let result = transaction_service_handle + .register_code_template( + author_public_key, + author_signature, + template_name, + template_version, + template_type, + BuildInfo { + repo_url: repository_url, + commit_hash: repository_commit_hash, + }, + binary_sha, + binary_url, + fee_per_gram, + ) + .await; + + let sent_tx_id = match result { + Ok(tx_id) => tx_id, + Err(e) => { + error!(target: LOG_TARGET, "failed to register code template: {:?}", e); + + result_tx + .send(UiTransactionSendStatus::Error(UiError::from(e).to_string())) + .unwrap(); + return; + }, + }; + + // ---------------------------------------------------------------------------- + // starting a feedback loop to wait for the answer from the transaction service + // ---------------------------------------------------------------------------- + + loop { + match event_stream.recv().await { + Ok(event) => { + if let TransactionEvent::TransactionCompletedImmediately(completed_tx_id) = &*event { + if sent_tx_id == *completed_tx_id { + result_tx.send(UiTransactionSendStatus::TransactionComplete).unwrap(); + return; + } + } else { + warn!(target: LOG_TARGET, "Encountered an unexpected event"); + todo!() + } + }, + + Err(e @ broadcast::error::RecvError::Lagged(_)) => { + warn!(target: LOG_TARGET, "Error reading from event broadcast channel {:?}", e); + continue; + }, + + Err(broadcast::error::RecvError::Closed) => { + break; + }, + } + } +} diff --git a/base_layer/core/src/chain_storage/lmdb_db/lmdb_db.rs b/base_layer/core/src/chain_storage/lmdb_db/lmdb_db.rs index def41ddf8b..9e8722e915 100644 --- a/base_layer/core/src/chain_storage/lmdb_db/lmdb_db.rs +++ b/base_layer/core/src/chain_storage/lmdb_db/lmdb_db.rs @@ -1340,7 +1340,7 @@ impl LMDBDatabase { .features .sidechain_feature .as_ref() - .and_then(|f| f.template_registration()) + .and_then(|f| f.code_template_registration()) { let record = TemplateRegistrationEntry { registration_data: template_reg.clone(), diff --git a/base_layer/core/src/consensus/consensus_encoding/bytes.rs b/base_layer/core/src/consensus/consensus_encoding/bytes.rs index fd8836ab61..eac4355f96 100644 --- a/base_layer/core/src/consensus/consensus_encoding/bytes.rs +++ b/base_layer/core/src/consensus/consensus_encoding/bytes.rs @@ -24,6 +24,7 @@ use std::{cmp, convert::TryFrom, ops::Deref}; use borsh::{BorshDeserialize, BorshSerialize}; use serde::{Deserialize, Serialize}; +use tari_utilities::hex::{from_hex, HexError}; #[derive( Debug, @@ -73,13 +74,33 @@ impl From> for Vec { } impl TryFrom> for MaxSizeBytes { - type Error = Vec; + type Error = MaxSizeBytesError; fn try_from(value: Vec) -> Result { if value.len() > MAX { - return Err(value); + Err(MaxSizeBytesError::MaxSizeBytesLengthError { + expected: MAX, + actual: value.len(), + }) + } else { + Ok(MaxSizeBytes { inner: value }) } - Ok(MaxSizeBytes { inner: value }) + } +} + +impl TryFrom<&str> for MaxSizeBytes { + type Error = MaxSizeBytesError; + + fn try_from(value: &str) -> Result { + Self::try_from(from_hex(value)?) + } +} + +impl TryFrom for MaxSizeBytes { + type Error = MaxSizeBytesError; + + fn try_from(value: String) -> Result { + Self::try_from(from_hex(value.as_str())?) } } @@ -96,3 +117,11 @@ impl Deref for MaxSizeBytes { &self.inner } } + +#[derive(Debug, thiserror::Error)] +pub enum MaxSizeBytesError { + #[error("Invalid Bytes length: expected {expected}, got {actual}")] + MaxSizeBytesLengthError { expected: usize, actual: usize }, + #[error("Conversion error: {0}")] + HexError(#[from] HexError), +} diff --git a/base_layer/core/src/covenants/test.rs b/base_layer/core/src/covenants/test.rs index 053815fd55..5b95233107 100644 --- a/base_layer/core/src/covenants/test.rs +++ b/base_layer/core/src/covenants/test.rs @@ -76,5 +76,5 @@ pub fn make_sample_sidechain_feature() -> SideChainFeature { binary_sha: Default::default(), binary_url: "https://github.com/tari-project/tari.git".try_into().unwrap(), }; - SideChainFeature::TemplateRegistration(template_reg) + SideChainFeature::CodeTemplateRegistration(template_reg) } diff --git a/base_layer/core/src/proto/sidechain_feature.rs b/base_layer/core/src/proto/sidechain_feature.rs index 27069877f3..4430148947 100644 --- a/base_layer/core/src/proto/sidechain_feature.rs +++ b/base_layer/core/src/proto/sidechain_feature.rs @@ -56,7 +56,7 @@ impl From for proto::types::side_chain_feature::SideChainFeatu SideChainFeature::ValidatorNodeRegistration(template_reg) => { proto::types::side_chain_feature::SideChainFeature::ValidatorNodeRegistration(template_reg.into()) }, - SideChainFeature::TemplateRegistration(template_reg) => { + SideChainFeature::CodeTemplateRegistration(template_reg) => { proto::types::side_chain_feature::SideChainFeature::TemplateRegistration(template_reg.into()) }, SideChainFeature::ConfidentialOutput(output_data) => { @@ -75,7 +75,7 @@ impl TryFrom for SideChainFe Ok(SideChainFeature::ValidatorNodeRegistration(vn_reg.try_into()?)) }, proto::types::side_chain_feature::SideChainFeature::TemplateRegistration(template_reg) => { - Ok(SideChainFeature::TemplateRegistration(template_reg.try_into()?)) + Ok(SideChainFeature::CodeTemplateRegistration(template_reg.try_into()?)) }, proto::types::side_chain_feature::SideChainFeature::ConfidentialOutput(output_data) => { Ok(SideChainFeature::ConfidentialOutput(output_data.try_into()?)) diff --git a/base_layer/core/src/transactions/transaction_components/output_features.rs b/base_layer/core/src/transactions/transaction_components/output_features.rs index 527a82a66d..5400210d33 100644 --- a/base_layer/core/src/transactions/transaction_components/output_features.rs +++ b/base_layer/core/src/transactions/transaction_components/output_features.rs @@ -31,14 +31,19 @@ use serde::{Deserialize, Serialize}; use tari_common_types::types::{PublicKey, Signature}; use super::OutputFeaturesVersion; -use crate::transactions::transaction_components::{ - range_proof_type::RangeProofType, - side_chain::SideChainFeature, - CodeTemplateRegistration, - ConfidentialOutputData, - OutputType, - ValidatorNodeRegistration, - ValidatorNodeSignature, +use crate::{ + consensus::{MaxSizeBytes, MaxSizeString}, + transactions::transaction_components::{ + range_proof_type::RangeProofType, + side_chain::SideChainFeature, + BuildInfo, + CodeTemplateRegistration, + ConfidentialOutputData, + OutputType, + TemplateType, + ValidatorNodeRegistration, + ValidatorNodeSignature, + }, }; /// Options for UTXO's @@ -131,7 +136,7 @@ impl OutputFeatures { pub fn for_template_registration(template_registration: CodeTemplateRegistration) -> OutputFeatures { OutputFeatures { output_type: OutputType::CodeTemplateRegistration, - sidechain_feature: Some(SideChainFeature::TemplateRegistration(template_registration)), + sidechain_feature: Some(SideChainFeature::CodeTemplateRegistration(template_registration)), ..Default::default() } } @@ -152,12 +157,44 @@ impl OutputFeatures { } } + pub fn for_code_template_registration( + author_public_key: PublicKey, + author_signature: Signature, + template_name: MaxSizeString<32>, + template_version: u16, + template_type: TemplateType, + build_info: BuildInfo, + binary_sha: MaxSizeBytes<32>, + binary_url: MaxSizeString<255>, + ) -> OutputFeatures { + OutputFeatures { + output_type: OutputType::CodeTemplateRegistration, + sidechain_feature: Some(SideChainFeature::CodeTemplateRegistration(CodeTemplateRegistration { + author_public_key, + author_signature, + template_name, + template_version, + template_type, + build_info, + binary_sha, + binary_url, + })), + ..Default::default() + } + } + pub fn validator_node_registration(&self) -> Option<&ValidatorNodeRegistration> { self.sidechain_feature .as_ref() .and_then(|s| s.validator_node_registration()) } + pub fn code_template_registration(&self) -> Option<&CodeTemplateRegistration> { + self.sidechain_feature + .as_ref() + .and_then(|s| s.code_template_registration()) + } + pub fn is_coinbase(&self) -> bool { matches!(self.output_type, OutputType::Coinbase) } diff --git a/base_layer/core/src/transactions/transaction_components/side_chain/sidechain_feature.rs b/base_layer/core/src/transactions/transaction_components/side_chain/sidechain_feature.rs index 4ceb8edba4..6d41c7d1ff 100644 --- a/base_layer/core/src/transactions/transaction_components/side_chain/sidechain_feature.rs +++ b/base_layer/core/src/transactions/transaction_components/side_chain/sidechain_feature.rs @@ -32,14 +32,14 @@ use crate::transactions::transaction_components::{ #[derive(Debug, Clone, Hash, PartialEq, Deserialize, Serialize, Eq, BorshSerialize, BorshDeserialize)] pub enum SideChainFeature { ValidatorNodeRegistration(ValidatorNodeRegistration), - TemplateRegistration(CodeTemplateRegistration), + CodeTemplateRegistration(CodeTemplateRegistration), ConfidentialOutput(ConfidentialOutputData), } impl SideChainFeature { - pub fn template_registration(&self) -> Option<&CodeTemplateRegistration> { + pub fn code_template_registration(&self) -> Option<&CodeTemplateRegistration> { match self { - Self::TemplateRegistration(v) => Some(v), + Self::CodeTemplateRegistration(v) => Some(v), _ => None, } } diff --git a/base_layer/wallet/src/transaction_service/handle.rs b/base_layer/wallet/src/transaction_service/handle.rs index 9bae444bdb..c2ba3a7d1e 100644 --- a/base_layer/wallet/src/transaction_service/handle.rs +++ b/base_layer/wallet/src/transaction_service/handle.rs @@ -36,11 +36,19 @@ use tari_common_types::{ }; use tari_comms::types::CommsPublicKey; use tari_core::{ + consensus::{MaxSizeBytes, MaxSizeString}, mempool::FeePerGramStat, proto, transactions::{ tari_amount::MicroTari, - transaction_components::{OutputFeatures, Transaction, TransactionOutput}, + transaction_components::{ + BuildInfo, + CodeTemplateRegistration, + OutputFeatures, + TemplateType, + Transaction, + TransactionOutput, + }, }, }; use tari_service_framework::reply_channel::SenderService; @@ -97,6 +105,17 @@ pub enum TransactionServiceRequest { fee_per_gram: MicroTari, message: String, }, + RegisterCodeTemplate { + author_public_key: PublicKey, + author_signature: Signature, + template_name: MaxSizeString<32>, + template_version: u16, + template_type: TemplateType, + build_info: BuildInfo, + binary_sha: MaxSizeBytes<32>, + binary_url: MaxSizeString<255>, + fee_per_gram: MicroTari, + }, SendOneSidedTransaction { destination: TariAddress, amount: MicroTari, @@ -229,6 +248,9 @@ impl fmt::Display for TransactionServiceRequest { Self::GetFeePerGramStatsPerBlock { count } => { write!(f, "GetFeePerGramEstimatesPerBlock(count: {})", count,) }, + TransactionServiceRequest::RegisterCodeTemplate { template_name, .. } => { + write!(f, "RegisterCodeTemplate: {}", template_name) + }, } } } @@ -237,7 +259,14 @@ impl fmt::Display for TransactionServiceRequest { #[derive(Debug)] pub enum TransactionServiceResponse { TransactionSent(TxId), - BurntTransactionSent { tx_id: TxId, proof: Box }, + BurntTransactionSent { + tx_id: TxId, + proof: Box, + }, + TemplateRegistrationTransactionSent { + tx_id: TxId, + template_registration: Box, + }, TransactionCancelled, PendingInboundTransactions(HashMap), PendingOutboundTransactions(HashMap), @@ -489,6 +518,38 @@ impl TransactionServiceHandle { } } + pub async fn register_code_template( + &mut self, + author_public_key: PublicKey, + author_signature: Signature, + template_name: MaxSizeString<32>, + template_version: u16, + template_type: TemplateType, + build_info: BuildInfo, + binary_sha: MaxSizeBytes<32>, + binary_url: MaxSizeString<255>, + fee_per_gram: MicroTari, + ) -> Result { + match self + .handle + .call(TransactionServiceRequest::RegisterCodeTemplate { + author_public_key, + author_signature, + template_name, + template_version, + template_type, + build_info, + binary_sha, + binary_url, + fee_per_gram, + }) + .await?? + { + TransactionServiceResponse::TransactionSent(tx_id) => Ok(tx_id), + _ => Err(TransactionServiceError::UnexpectedApiResponse), + } + } + pub async fn send_one_sided_transaction( &mut self, destination: TariAddress, diff --git a/base_layer/wallet/src/transaction_service/service.rs b/base_layer/wallet/src/transaction_service/service.rs index 0abc99e924..462d114353 100644 --- a/base_layer/wallet/src/transaction_service/service.rs +++ b/base_layer/wallet/src/transaction_service/service.rs @@ -49,6 +49,7 @@ use tari_core::{ transactions::{ tari_amount::MicroTari, transaction_components::{ + CodeTemplateRegistration, EncryptedData, KernelFeatures, OutputFeatures, @@ -688,6 +689,39 @@ where .await?; return Ok(()); }, + TransactionServiceRequest::RegisterCodeTemplate { + author_public_key, + author_signature, + template_name, + template_version, + template_type, + build_info, + binary_sha, + binary_url, + fee_per_gram, + } => { + self.register_code_template( + fee_per_gram, + CodeTemplateRegistration { + author_public_key, + author_signature, + template_name: template_name.clone(), + template_version, + template_type, + build_info, + binary_sha, + binary_url, + }, + UtxoSelectionCriteria::default(), + format!("Template Registration: {}", template_name), + send_transaction_join_handles, + transaction_broadcast_join_handles, + reply_channel.take().expect("Reply channel is not set"), + ) + .await?; + + return Ok(()); + }, TransactionServiceRequest::SendShaAtomicSwapTransaction( destination, amount, @@ -1559,6 +1593,35 @@ where .await } + pub async fn register_code_template( + &mut self, + fee_per_gram: MicroTari, + template_registration: CodeTemplateRegistration, + selection_criteria: UtxoSelectionCriteria, + message: String, + join_handles: &mut FuturesUnordered< + JoinHandle>>, + >, + transaction_broadcast_join_handles: &mut FuturesUnordered< + JoinHandle>>, + >, + reply_channel: oneshot::Sender>, + ) -> Result<(), TransactionServiceError> { + self.send_transaction( + self.resources.wallet_identity.address.clone(), + 0.into(), + selection_criteria, + OutputFeatures::for_template_registration(template_registration), + fee_per_gram, + message, + TransactionMetadata::default(), + join_handles, + transaction_broadcast_join_handles, + reply_channel, + ) + .await + } + /// Sends a one side payment transaction to a recipient /// # Arguments /// 'dest_pubkey': The Comms pubkey of the recipient node @@ -2026,7 +2089,7 @@ where trace!( target: LOG_TARGET, "Transaction (TxId: {}) has already been received, this is probably a repeated message, Trace: - {}.", + {}.", data.tx_id, traced_message_tag );