From 701e3c2341d1029c2711b81a66952f3bee7d8e42 Mon Sep 17 00:00:00 2001 From: Martin Stefcek <35243812+Cifko@users.noreply.github.com> Date: Wed, 14 Jun 2023 11:33:26 +0200 Subject: [PATCH] feat: ui for template registration in console wallet (#5444) Description --- Added code template registration tab to the console wallet. The UI shows proper errors. It will not erase the fields when something is wrong. Autofill everything that it can from the binary url (template name, version, type, repo link). Computes the hash for the template. Motivation and Context --- The autofill works like this: If the link is e.g. `https://github.com/tari-project/tari/blob/development/template.12.wasm`. It will fill the repo as `https://github.com/tari-project/tari` Template name `template`, template version `12`, template type `WASM:1`. The hash is always computed as of now, because there is no way for the user to generate it manually. How Has This Been Tested? --- Manually. What process can a PR reviewer use to test or verify this change? --- Register a code template, and look into the VN UI. Breaking Changes --- - [x] None - [ ] Requires data directory on base node to be deleted - [ ] Requires hard fork - [ ] Other - Please specify --------- Co-authored-by: Andrejs Gubarevs <1062334+agubarev@users.noreply.github.com> Co-authored-by: stringhandler --- Cargo.lock | 2 + .../src/conversions/sidechain_feature.rs | 4 +- applications/tari_console_wallet/Cargo.toml | 2 + .../src/grpc/wallet_grpc_server.rs | 2 +- .../tari_console_wallet/src/ui/app.rs | 2 + .../src/ui/components/mod.rs | 1 + .../ui/components/register_template_tab.rs | 861 ++++++++++++++++++ .../src/ui/components/send_tab.rs | 2 + .../src/ui/state/app_state.rs | 44 +- .../tari_console_wallet/src/ui/state/tasks.rs | 193 +++- .../core/src/chain_storage/lmdb_db/lmdb_db.rs | 2 +- .../src/consensus/consensus_encoding/bytes.rs | 35 +- base_layer/core/src/covenants/test.rs | 2 +- .../core/src/proto/sidechain_feature.rs | 4 +- .../transaction_components/output_features.rs | 55 +- .../side_chain/sidechain_feature.rs | 6 +- .../wallet/src/transaction_service/handle.rs | 65 +- .../wallet/src/transaction_service/service.rs | 65 +- 18 files changed, 1315 insertions(+), 32 deletions(-) create mode 100644 applications/tari_console_wallet/src/ui/components/register_template_tab.rs 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 );