diff --git a/applications/tari_console_wallet/src/ui/components/burn_tab.rs b/applications/tari_console_wallet/src/ui/components/burn_tab.rs index c53dd8cd0dc..1f064a994f0 100644 --- a/applications/tari_console_wallet/src/ui/components/burn_tab.rs +++ b/applications/tari_console_wallet/src/ui/components/burn_tab.rs @@ -1,8 +1,12 @@ // Copyright 2022 The Tari Project // SPDX-License-Identifier: BSD-3-Clause +use std::{fs, str::from_utf8}; + use log::*; +use tari_common_types::types::PublicKey; use tari_core::transactions::tari_amount::MicroTari; +use tari_utilities::{hex::Hex, ByteArray}; use tari_wallet::output_manager_service::UtxoSelectionCriteria; use tokio::{runtime::Handle, sync::watch}; use tui::{ @@ -18,7 +22,7 @@ use unicode_width::UnicodeWidthStr; use crate::{ ui::{ components::{balance::Balance, contacts_tab::ContactsTab, Component, KeyHandled}, - state::{AppState, UiTransactionBurnStatus}, + state::{AppState, BurntProofBase64, UiTransactionBurnStatus}, types::UiBurntProof, widgets::{draw_dialog, MultiColumnList, WindowedListState}, MAX_WIDTH, @@ -63,7 +67,7 @@ impl BurnTab { burn_result_watch: None, confirmation_dialog: None, table_state: TableState::default(), - show_proofs: false, + show_proofs: true, } } @@ -225,9 +229,11 @@ impl BurnTab { let instructions = Paragraph::new(Spans::from(vec![ Span::raw(" Use "), Span::styled("Up↑/Down↓ Keys", Style::default().add_modifier(Modifier::BOLD)), - Span::raw(" to choose a contact, "), - Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)), - Span::raw(" to select."), + Span::raw(" to choose a proof, "), + Span::styled("O", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to save proof to a file (named after proof ID), "), + Span::styled("D", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to delete the selected proof."), ])) .wrap(Wrap { trim: true }); f.render_widget(instructions, list_areas[0]); @@ -249,23 +255,20 @@ impl BurnTab { pub fn create_column_view(windowed_view: &[UiBurntProof]) -> MultiColumnList> { let mut column0_items = Vec::new(); let mut column1_items = Vec::new(); - // let mut column2_items = Vec::new(); for item in windowed_view.iter() { - column0_items.push(ListItem::new(Span::raw(item.id.to_string()))); - column1_items.push(ListItem::new(Span::raw(item.proof.clone()))); - // column2_items.push(ListItem::new(Span::raw(item.last_seen.clone()))); + column0_items.push(ListItem::new(Span::raw(item.reciprocal_claim_public_key.clone()))); + column1_items.push(ListItem::new(Span::raw(item.burned_at.to_string().clone()))); } let column_list = MultiColumnList::new() .highlight_style(Style::default().add_modifier(Modifier::BOLD).fg(Color::Magenta)) .heading_style(Style::default().fg(Color::Magenta)) .max_width(MAX_WIDTH) - .add_column(Some("ID"), Some(23), column0_items) .add_column(None, Some(1), Vec::new()) - .add_column(Some("Reciprocal Public Key"), Some(66), column1_items); - // .add_column(None, Some(1), Vec::new()) - // .add_column(Some("Last Seen"), Some(11), column2_items); + .add_column(Some("Reciprocal Claim Public Key"), Some(66), column0_items) + .add_column(None, Some(1), Vec::new()) + .add_column(Some("Burned At"), Some(11), column1_items); column_list } @@ -277,73 +280,87 @@ impl BurnTab { self.confirmation_dialog = None; return KeyHandled::Handled; } else if 'y' == c { - match self.confirmation_dialog { - None => (), - Some(BurnConfirmationDialogType::Normal) => { - if 'y' == c { - let amount = self.amount_field.parse::().unwrap_or(MicroTari::from(0)); - - let fee_per_gram = if let Ok(v) = self.fee_field.parse::() { - v - } else { - self.error_message = - Some("Fee-per-gram should be an integer\nPress Enter to continue.".to_string()); - return KeyHandled::Handled; - }; - - let burn_proof_filepath = if self.burnt_proof_filepath_field.is_empty() { - None - } else { - Some(self.burnt_proof_filepath_field.clone()) - }; - - let claim_public_key = if self.claim_public_key_field.is_empty() { - None - } else { - Some(self.claim_public_key_field.clone()) - }; - - let (tx, rx) = watch::channel(UiTransactionBurnStatus::Initiated); - - let mut reset_fields = false; - match self.confirmation_dialog { - Some(BurnConfirmationDialogType::Normal) => { - match Handle::current().block_on(app_state.send_burn_transaction( - burn_proof_filepath, - claim_public_key, - amount.into(), - UtxoSelectionCriteria::default(), - fee_per_gram, - self.message_field.clone(), - tx, - )) { - Err(e) => { - self.error_message = Some(format!( - "Error sending burn transaction (with a claim public key \ - provided):\n{}\nPress Enter to continue.", - e - )) - }, - Ok(_) => reset_fields = true, - } + if 'y' == c { + let amount = self.amount_field.parse::().unwrap_or(MicroTari::from(0)); + + let fee_per_gram = if let Ok(v) = self.fee_field.parse::() { + v + } else { + self.error_message = + Some("Fee-per-gram should be an integer\nPress Enter to continue.".to_string()); + return KeyHandled::Handled; + }; + + let burn_proof_filepath = if self.burnt_proof_filepath_field.is_empty() { + None + } else { + Some(self.burnt_proof_filepath_field.clone()) + }; + + let claim_public_key = if self.claim_public_key_field.is_empty() { + None + } else { + Some(self.claim_public_key_field.clone()) + }; + + let (tx, rx) = watch::channel(UiTransactionBurnStatus::Initiated); + + let mut reset_fields = false; + match self.confirmation_dialog { + Some(BurnConfirmationDialogType::Normal) => { + match Handle::current().block_on(app_state.send_burn_transaction( + burn_proof_filepath, + claim_public_key, + amount.into(), + UtxoSelectionCriteria::default(), + fee_per_gram, + self.message_field.clone(), + tx, + )) { + Err(e) => { + self.error_message = Some(format!( + "Error sending burn transaction (with a claim public key \ + provided):\n{}\nPress Enter to continue.", + e + )) }, - None => {}, - } + Ok(_) => { + Handle::current().block_on(app_state.update_cache()); + &app_state.refresh_burnt_proofs_state(); - if reset_fields { - self.burnt_proof_filepath_field = "".to_string(); - self.claim_public_key_field = "".to_string(); - self.amount_field = "".to_string(); - self.fee_field = app_state.get_default_fee_per_gram().as_u64().to_string(); - self.message_field = "".to_string(); - self.burn_input_mode = BurnInputMode::None; - self.burn_result_watch = Some(rx); + reset_fields = true + }, } - - self.confirmation_dialog = None; - return KeyHandled::Handled; - } - }, + }, + None => {}, + Some(BurnConfirmationDialogType::DeleteBurntProof(proof_id)) => { + match Handle::current().block_on(app_state.delete_burnt_proof(proof_id)) { + Err(e) => { + self.error_message = Some(format!( + "Failed to delete burnt proof (id={}):\n{}\nPress Enter to continue.", + proof_id, e + )) + }, + Ok(_) => { + &app_state.refresh_burnt_proofs_state(); + Handle::current().block_on(app_state.update_cache()); + }, + } + }, + } + + if reset_fields { + self.burnt_proof_filepath_field = "".to_string(); + self.claim_public_key_field = "".to_string(); + self.amount_field = "".to_string(); + self.fee_field = app_state.get_default_fee_per_gram().as_u64().to_string(); + self.message_field = "".to_string(); + self.burn_input_mode = BurnInputMode::None; + self.burn_result_watch = Some(rx); + } + + self.confirmation_dialog = None; + return KeyHandled::Handled; } } else { } @@ -402,16 +419,47 @@ impl BurnTab { } fn on_key_show_proofs(&mut self, c: char, app_state: &mut AppState) -> KeyHandled { - if self.show_proofs && c == '\n' { - if let Some(c) = self - .proofs_list_state - .selected() - .and_then(|i| app_state.get_burnt_proof_by_index(i)) - .cloned() - { - self.show_proofs = false; - } - return KeyHandled::Handled; + match (self.show_proofs, c) { + (true, 'd') => { + if let Some(proof) = self + .proofs_list_state + .selected() + .and_then(|i| app_state.get_burnt_proof_by_index(i)) + .cloned() + { + if self.proofs_list_state.selected().is_none() { + return KeyHandled::NotHandled; + } + + self.confirmation_dialog = Some(BurnConfirmationDialogType::DeleteBurntProof(proof.id)); + } + + return KeyHandled::Handled; + }, + + (true, 'o') => { + if let Some(proof) = self + .proofs_list_state + .selected() + .and_then(|i| app_state.get_burnt_proof_by_index(i)) + .cloned() + { + if self.proofs_list_state.selected().is_none() { + return KeyHandled::NotHandled; + } + + if let Err(e) = fs::write(format!("{}.json", proof.id), proof.payload) { + self.error_message = Some(format!( + "Failed to save burnt proof payload to file {}.json: {}, Press Enter to continue.", + proof.id, e + )); + } + } + + return KeyHandled::Handled; + }, + + _ => {}, } KeyHandled::NotHandled @@ -464,7 +512,12 @@ impl Component for BurnTab { ); return; }, - UiTransactionBurnStatus::TransactionComplete((proof_id, serialized_proof)) => { + UiTransactionBurnStatus::TransactionComplete(( + _proof_id, + _reciprocal_claim_public_key, + _serialized_proof, + _burned_at, + )) => { self.success_message = Some("Transaction completed successfully!\nPlease press Enter to continue".to_string()); return; @@ -510,6 +563,18 @@ impl Component for BurnTab { 9, ); }, + + Some(BurnConfirmationDialogType::DeleteBurntProof(proof_id)) => { + draw_dialog( + f, + area, + "Confirm Delete".to_string(), + "Are you sure you want to delete this burnt proof?\n(Y)es / (N)o".to_string(), + Color::Red, + 120, + 9, + ); + }, } } @@ -587,19 +652,23 @@ impl Component for BurnTab { } } - fn on_up(&mut self, _app_state: &mut AppState) { - let index = self.table_state.selected().unwrap_or_default(); - if index == 0 { - self.table_state.select(None); - } + fn on_up(&mut self, app_state: &mut AppState) { + self.proofs_list_state.set_num_items(app_state.get_burnt_proofs().len()); + self.proofs_list_state.previous(); } - fn on_down(&mut self, _app_state: &mut AppState) { - self.table_state.select(None); + fn on_down(&mut self, app_state: &mut AppState) { + self.proofs_list_state.set_num_items(app_state.get_burnt_proofs().len()); + self.proofs_list_state.next(); } fn on_esc(&mut self, _: &mut AppState) { + if self.confirmation_dialog.is_some() { + return; + } + self.burn_input_mode = BurnInputMode::None; + self.proofs_list_state.select(None); } fn on_backspace(&mut self, _app_state: &mut AppState) { @@ -637,4 +706,5 @@ pub enum BurnInputMode { #[derive(PartialEq, Debug)] pub enum BurnConfirmationDialogType { Normal, + DeleteBurntProof(i32), } diff --git a/applications/tari_console_wallet/src/ui/mod.rs b/applications/tari_console_wallet/src/ui/mod.rs index cfc9182f74f..11155fecf5a 100644 --- a/applications/tari_console_wallet/src/ui/mod.rs +++ b/applications/tari_console_wallet/src/ui/mod.rs @@ -57,6 +57,8 @@ pub fn run(app: App>) -> Result<(), ExitError> { app.app_state.refresh_transaction_state().await?; trace!(target: LOG_TARGET, "Refreshing contacts state"); app.app_state.refresh_contacts_state().await?; + trace!(target: LOG_TARGET, "Refreshing burnt proofs state"); + app.app_state.refresh_burnt_proofs_state().await?; trace!(target: LOG_TARGET, "Refreshing connected peers state"); app.app_state.refresh_connected_peers_state().await?; trace!(target: LOG_TARGET, "Checking connectivity"); 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 4aa0811cb4c..fe21a678119 100644 --- a/applications/tari_console_wallet/src/ui/state/app_state.rs +++ b/applications/tari_console_wallet/src/ui/state/app_state.rs @@ -50,7 +50,10 @@ use tari_core::transactions::{ weight::TransactionWeight, }; use tari_shutdown::ShutdownSignal; -use tari_utilities::hex::{from_hex, Hex}; +use tari_utilities::{ + hex::{from_hex, Hex}, + ByteArray, +}; use tari_wallet::{ base_node_service::{handle::BaseNodeEventReceiver, service::BaseNodeState}, connectivity_service::{OnlineStatus, WalletConnectivityHandle, WalletConnectivityInterface}, @@ -64,7 +67,7 @@ use tari_wallet::{ WalletSqlite, }; use tokio::{ - sync::{broadcast, watch, RwLock}, + sync::{broadcast, watch, RwLock, RwLockMappedWriteGuard}, task, }; @@ -268,6 +271,18 @@ impl AppState { Ok(()) } + pub async fn delete_burnt_proof(&mut self, proof_id: i32) -> Result<(), UiError> { + let mut inner = self.inner.write().await; + + inner.wallet.db.remove_burnt_proof(proof_id)?; + + inner.refresh_burnt_proofs_state().await?; + drop(inner); + self.update_cache().await; + + Ok(()) + } + pub async fn send_transaction( &mut self, address: String, @@ -412,6 +427,7 @@ impl AppState { message, fee_per_gram, tx_service_handle, + inner.wallet.db.clone(), result_tx, )); @@ -860,16 +876,17 @@ impl AppStateInner { for proof in db_burnt_proofs { ui_proofs.push(UiBurntProof { id: proof.0, - proof: proof.1, + reciprocal_claim_public_key: proof.1, + payload: proof.2, + burned_at: proof.3, }); } - // TODO: sort by timestamp when added - // ui_proofs.sort_by(|a, b| { - // a.alias - // .partial_cmp(&b.alias) - // .expect("Should be able to compare contact aliases") - // }); + ui_proofs.sort_by(|a, b| { + a.burned_at + .partial_cmp(&b.burned_at) + .expect("Should be able to compare burn timestamps") + }); self.data.burnt_proofs = ui_proofs; self.updated = true; @@ -1275,7 +1292,7 @@ pub enum UiTransactionBurnStatus { Initiated, Queued, SentDirect, - TransactionComplete((i32, String)), + TransactionComplete((i32, String, String, NaiveDateTime)), DiscoveryInProgress, SentViaSaf, Error(String), diff --git a/applications/tari_console_wallet/src/ui/state/mod.rs b/applications/tari_console_wallet/src/ui/state/mod.rs index 9b247ab7db1..f6891902a8f 100644 --- a/applications/tari_console_wallet/src/ui/state/mod.rs +++ b/applications/tari_console_wallet/src/ui/state/mod.rs @@ -25,7 +25,7 @@ mod debouncer; mod tasks; mod wallet_event_monitor; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use tari_common_types::serde_with; pub use self::app_state::*; @@ -33,7 +33,7 @@ pub use self::app_state::*; // ---------------------------------------------------------------------------- // TODO: re-implement in a clean way -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] pub struct CommitmentSignatureBase64 { #[serde(with = "serde_with::base64")] pub public_nonce: Vec, @@ -43,7 +43,7 @@ pub struct CommitmentSignatureBase64 { pub v: Vec, } -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] pub struct BurntProofBase64 { #[serde(with = "serde_with::base64")] pub reciprocal_claim_public_key: Vec, diff --git a/applications/tari_console_wallet/src/ui/state/tasks.rs b/applications/tari_console_wallet/src/ui/state/tasks.rs index af04d015d85..6b931b27d63 100644 --- a/applications/tari_console_wallet/src/ui/state/tasks.rs +++ b/applications/tari_console_wallet/src/ui/state/tasks.rs @@ -22,13 +22,17 @@ use std::path::PathBuf; +use chrono::{NaiveDateTime, Utc}; +use log::error; use rand::random; use tari_common_types::{tari_address::TariAddress, types::PublicKey}; use tari_core::transactions::{tari_amount::MicroTari, transaction_components::OutputFeatures}; -use tari_utilities::ByteArray; +use tari_utilities::{hex::Hex, ByteArray}; use tari_wallet::{ output_manager_service::UtxoSelectionCriteria, + storage::{database::WalletDatabase, sqlite_db::wallet::WalletSqliteDatabase}, transaction_service::handle::{TransactionEvent, TransactionSendStatus, TransactionServiceHandle}, + WalletSqlite, }; use tokio::sync::{broadcast, watch}; @@ -231,6 +235,7 @@ pub async fn send_burn_transaction_task( message: String, fee_per_gram: MicroTari, mut transaction_service_handle: TransactionServiceHandle, + db: WalletDatabase, result_tx: watch::Sender, ) { let _ = result_tx.send(UiTransactionBurnStatus::Initiated); @@ -243,21 +248,23 @@ pub async fn send_burn_transaction_task( Err(e) => { let _ = result_tx.send(UiTransactionBurnStatus::Error(UiError::from(e).to_string())); }, - Ok((burn_tx_id, proof)) => { + Ok((burn_tx_id, original_proof)) => { loop { match event_stream.recv().await { Ok(event) => { if let TransactionEvent::TransactionCompletedImmediately(completed_tx_id) = &*event { if burn_tx_id == *completed_tx_id { let wrapped_proof = BurntProofBase64 { - reciprocal_claim_public_key: proof.reciprocal_claim_public_key.to_vec(), - commitment: proof.commitment.to_vec(), - ownership_proof: proof.ownership_proof.map(|x| CommitmentSignatureBase64 { - public_nonce: x.public_nonce().to_vec(), - u: x.u().to_vec(), - v: x.v().to_vec(), + reciprocal_claim_public_key: original_proof.reciprocal_claim_public_key.to_vec(), + commitment: original_proof.commitment.to_vec(), + ownership_proof: original_proof.ownership_proof.map(|x| { + CommitmentSignatureBase64 { + public_nonce: x.public_nonce().to_vec(), + u: x.u().to_vec(), + v: x.v().to_vec(), + } }), - range_proof: proof.range_proof.0, + range_proof: original_proof.range_proof.0, }; let serialized_proof = @@ -270,9 +277,24 @@ pub async fn send_burn_transaction_task( std::fs::write(filepath, serialized_proof.as_bytes()) .expect("failed to save burn proof"); + let proof_id = random::().abs(); + let ts = Utc::now().naive_utc(); + + if let Err(e) = db.insert_burnt_proof( + proof_id, + original_proof.reciprocal_claim_public_key.to_hex(), + serialized_proof.clone(), + ts, + ) { + error!("failed to save burnt proof to the database"); + return; + } + let _ = result_tx.send(UiTransactionBurnStatus::TransactionComplete(( - random::(), + proof_id, + original_proof.reciprocal_claim_public_key.to_hex(), serialized_proof, + ts, ))); return; diff --git a/applications/tari_console_wallet/src/ui/types.rs b/applications/tari_console_wallet/src/ui/types.rs index d6d0051b655..5f3bd155603 100644 --- a/applications/tari_console_wallet/src/ui/types.rs +++ b/applications/tari_console_wallet/src/ui/types.rs @@ -1,10 +1,12 @@ -use chrono::{DateTime, Local}; -use tari_contacts::contacts_service::types::Contact; +use chrono::{DateTime, Local, NaiveDateTime}; +use tari_contacts::contacts_service::storage::database::Contact; #[derive(Debug, Clone)] pub struct UiBurntProof { pub id: i32, - pub proof: String, + pub reciprocal_claim_public_key: String, + pub payload: String, + pub burned_at: NaiveDateTime, } #[derive(Debug, Clone)] diff --git a/base_layer/wallet/migrations/2022-08-08-134037_initial/up.sql b/base_layer/wallet/migrations/2022-08-08-134037_initial/up.sql index 9b2e28e5cb8..dffd8167afe 100644 --- a/base_layer/wallet/migrations/2022-08-08-134037_initial/up.sql +++ b/base_layer/wallet/migrations/2022-08-08-134037_initial/up.sql @@ -1,14 +1,16 @@ -CREATE TABLE client_key_values ( +CREATE TABLE client_key_values +( key TEXT PRIMARY KEY NOT NULL, value TEXT NOT NULL ); -CREATE TABLE completed_transactions ( +CREATE TABLE completed_transactions +( tx_id BIGINT PRIMARY KEY NOT NULL, source_address BLOB NOT NULL, destination_address BLOB NOT NULL, amount BIGINT NOT NULL, - fee BIGINT NOT NULL, + fee BIGINT NOT NULL, transaction_protocol TEXT NOT NULL, status INTEGER NOT NULL, message TEXT NOT NULL, @@ -26,7 +28,8 @@ CREATE TABLE completed_transactions ( transaction_signature_key BLOB DEFAULT 0 NOT NULL ); -CREATE TABLE inbound_transactions ( +CREATE TABLE inbound_transactions +( tx_id BIGINT PRIMARY KEY NOT NULL, source_address BLOB NOT NULL, amount BIGINT NOT NULL, @@ -39,7 +42,8 @@ CREATE TABLE inbound_transactions ( last_send_timestamp DATETIME NULL ); -CREATE TABLE known_one_sided_payment_scripts ( +CREATE TABLE known_one_sided_payment_scripts +( script_hash BLOB PRIMARY KEY NOT NULL, private_key BLOB NOT NULL, script BLOB NOT NULL, @@ -47,59 +51,62 @@ CREATE TABLE known_one_sided_payment_scripts ( script_lock_height UNSIGNED BIGINT NOT NULL DEFAULT 0 ); -CREATE TABLE outbound_transactions ( - tx_id BIGINT PRIMARY KEY NOT NULL, - destination_address BLOB NOT NULL, - amount BIGINT NOT NULL, - fee BIGINT NOT NULL, - sender_protocol TEXT NOT NULL, - message TEXT NOT NULL, - timestamp DATETIME NOT NULL, - cancelled INTEGER DEFAULT 0 NOT NULL, - direct_send_success INTEGER DEFAULT 0 NOT NULL, - send_count INTEGER DEFAULT 0 NOT NULL, - last_send_timestamp DATETIME NULL +CREATE TABLE outbound_transactions +( + tx_id BIGINT PRIMARY KEY NOT NULL, + destination_address BLOB NOT NULL, + amount BIGINT NOT NULL, + fee BIGINT NOT NULL, + sender_protocol TEXT NOT NULL, + message TEXT NOT NULL, + timestamp DATETIME NOT NULL, + cancelled INTEGER DEFAULT 0 NOT NULL, + direct_send_success INTEGER DEFAULT 0 NOT NULL, + send_count INTEGER DEFAULT 0 NOT NULL, + last_send_timestamp DATETIME NULL ); -CREATE TABLE outputs ( - id INTEGER PRIMARY KEY NOT NULL, - commitment BLOB NULL, - spending_key BLOB NOT NULL, - value BIGINT NOT NULL, - output_type INTEGER NOT NULL, - maturity BIGINT NOT NULL, - status INTEGER NOT NULL, - hash BLOB NULL, - script BLOB NOT NULL, - input_data BLOB NOT NULL, - script_private_key BLOB NOT NULL, - script_lock_height UNSIGNED BIGINT NOT NULL DEFAULT 0, - sender_offset_public_key BLOB NOT NULL, - metadata_signature_ephemeral_commitment BLOB NOT NULL, - metadata_signature_ephemeral_pubkey BLOB NOT NULL, - metadata_signature_u_a BLOB NOT NULL, - metadata_signature_u_x BLOB NOT NULL, - metadata_signature_u_y BLOB NOT NULL, - mined_height UNSIGNED BIGINT NULL, - mined_in_block BLOB NULL, - mined_mmr_position BIGINT NULL, - marked_deleted_at_height BIGINT NULL, - marked_deleted_in_block BLOB NULL, - received_in_tx_id BIGINT NULL, - spent_in_tx_id BIGINT NULL, - coinbase_block_height UNSIGNED BIGINT NULL, - metadata BLOB NULL, - features_json TEXT NOT NULL DEFAULT '{}', - spending_priority UNSIGNED INTEGER NOT NULL DEFAULT 500, - covenant BLOB NOT NULL, - mined_timestamp DATETIME NULL, - encrypted_value BLOB NOT NULL, - minimum_value_promise BIGINT NOT NULL, - source INTEGER NOT NULL DEFAULT 0, +CREATE TABLE outputs +( + id INTEGER PRIMARY KEY NOT NULL, + commitment BLOB NULL, + spending_key BLOB NOT NULL, + value BIGINT NOT NULL, + output_type INTEGER NOT NULL, + maturity BIGINT NOT NULL, + status INTEGER NOT NULL, + hash BLOB NULL, + script BLOB NOT NULL, + input_data BLOB NOT NULL, + script_private_key BLOB NOT NULL, + script_lock_height UNSIGNED BIGINT NOT NULL DEFAULT 0, + sender_offset_public_key BLOB NOT NULL, + metadata_signature_ephemeral_commitment BLOB NOT NULL, + metadata_signature_ephemeral_pubkey BLOB NOT NULL, + metadata_signature_u_a BLOB NOT NULL, + metadata_signature_u_x BLOB NOT NULL, + metadata_signature_u_y BLOB NOT NULL, + mined_height UNSIGNED BIGINT NULL, + mined_in_block BLOB NULL, + mined_mmr_position BIGINT NULL, + marked_deleted_at_height BIGINT NULL, + marked_deleted_in_block BLOB NULL, + received_in_tx_id BIGINT NULL, + spent_in_tx_id BIGINT NULL, + coinbase_block_height UNSIGNED BIGINT NULL, + metadata BLOB NULL, + features_json TEXT NOT NULL DEFAULT '{}', + spending_priority UNSIGNED INTEGER NOT NULL DEFAULT 500, + covenant BLOB NOT NULL, + mined_timestamp DATETIME NULL, + encrypted_value BLOB NOT NULL, + minimum_value_promise BIGINT NOT NULL, + source INTEGER NOT NULL DEFAULT 0, CONSTRAINT unique_commitment UNIQUE (commitment) ); -CREATE TABLE scanned_blocks ( +CREATE TABLE scanned_blocks +( header_hash BLOB PRIMARY KEY NOT NULL, height BIGINT NOT NULL, num_outputs BIGINT NULL, @@ -107,7 +114,16 @@ CREATE TABLE scanned_blocks ( timestamp DATETIME NOT NULL ); -CREATE TABLE wallet_settings ( +CREATE TABLE wallet_settings +( key TEXT PRIMARY KEY NOT NULL, value TEXT NOT NULL ); + +CREATE TABLE burnt_proofs +( + id INTEGER PRIMARY KEY NOT NULL, + reciprocal_claim_public_key TEXT NOT NULL, + payload TEXT NOT NULL, + burned_at DATETIME NOT NULL +); diff --git a/base_layer/wallet/src/schema.rs b/base_layer/wallet/src/schema.rs index 99b677963b6..972330190de 100644 --- a/base_layer/wallet/src/schema.rs +++ b/base_layer/wallet/src/schema.rs @@ -133,7 +133,9 @@ diesel::table! { diesel::table! { burnt_proofs (id) { id -> Integer, - value -> Text, + reciprocal_claim_public_key -> Text, + payload -> Text, + burned_at -> Timestamp, } } diff --git a/base_layer/wallet/src/storage/database.rs b/base_layer/wallet/src/storage/database.rs index 00b0e496e79..d63ccc22e62 100644 --- a/base_layer/wallet/src/storage/database.rs +++ b/base_layer/wallet/src/storage/database.rs @@ -25,6 +25,7 @@ use std::{ sync::Arc, }; +use chrono::NaiveDateTime; use log::*; use tari_common_types::chain_metadata::ChainMetadata; use tari_comms::{ @@ -122,8 +123,8 @@ pub enum DbValue { WalletBirthday(String), LastAccessedNetwork(String), LastAccessedVersion(String), - BurntProofs(Vec<(i32, String)>), - BurntProof((i32, String)), + BurntProofs(Vec<(i32, String, String, NaiveDateTime)>), + BurntProof((i32, String, String, NaiveDateTime)), } #[derive(Clone)] @@ -136,7 +137,7 @@ pub enum DbKeyValuePair { CommsFeatures(PeerFeatures), CommsIdentitySignature(Box), NetworkAndVersion((String, String)), - BurntProof((i32, String)), + BurntProof((i32, String, String, NaiveDateTime)), } pub enum WriteOperation { @@ -362,9 +363,19 @@ where T: WalletBackend + 'static // ---------------------------------------------------------------------------- // burnt proofs - pub fn insert_burn_proof(&self, id: i32, proof: String) -> Result<(), WalletStorageError> { - self.db - .write(WriteOperation::Insert(DbKeyValuePair::BurntProof((id, proof))))?; + pub fn insert_burnt_proof( + &self, + id: i32, + reciprocal_claim_public_key: String, + proof: String, + burned_at: NaiveDateTime, + ) -> Result<(), WalletStorageError> { + self.db.write(WriteOperation::Insert(DbKeyValuePair::BurntProof(( + id, + reciprocal_claim_public_key, + proof, + burned_at, + ))))?; Ok(()) } @@ -373,7 +384,7 @@ where T: WalletBackend + 'static Ok(()) } - pub fn get_burnt_proofs(&self) -> Result, WalletStorageError> { + pub fn get_burnt_proofs(&self) -> Result, WalletStorageError> { let proofs = match self.db.fetch(&DbKey::ListBurntProofs) { Ok(None) => Ok(vec![]), Ok(Some(DbValue::BurntProofs(proofs))) => Ok(proofs), @@ -404,7 +415,7 @@ impl Display for DbValue { DbValue::LastAccessedNetwork(network) => f.write_str(&format!("LastAccessedNetwork: {}", network)), DbValue::LastAccessedVersion(version) => f.write_str(&format!("LastAccessedVersion: {}", version)), DbValue::BurntProofs(proofs) => f.write_str(&format!("BurntProofs: {:?}", proofs)), - DbValue::BurntProof((id, _)) => f.write_str(&format!("BurntProof: {}", id)), + DbValue::BurntProof((id, _, _, _)) => f.write_str(&format!("BurntProof: {}", id)), } } } diff --git a/base_layer/wallet/src/storage/sqlite_db/wallet.rs b/base_layer/wallet/src/storage/sqlite_db/wallet.rs index 1df3bf5ea73..15d90da582c 100644 --- a/base_layer/wallet/src/storage/sqlite_db/wallet.rs +++ b/base_layer/wallet/src/storage/sqlite_db/wallet.rs @@ -32,6 +32,7 @@ use argon2::password_hash::{ SaltString, }; use chacha20poly1305::{Key, KeyInit, XChaCha20Poly1305}; +use chrono::NaiveDateTime; use diesel::{prelude::*, result::Error, SqliteConnection}; use digest::{generic_array::GenericArray, FixedOutput}; use itertools::Itertools; @@ -424,11 +425,11 @@ impl WalletSqliteDatabase { WalletSettingSql::new(DbKey::LastAccessedNetwork, network).set(&mut conn)?; WalletSettingSql::new(DbKey::LastAccessedVersion, version).set(&mut conn)?; }, - DbKeyValuePair::BurntProof((id, proof)) => { + DbKeyValuePair::BurntProof((id, reciprocal_claim_public_key, proof, burned_at)) => { kvp_text = "BurntProof"; let cipher = acquire_read_lock!(self.cipher); - BurntProofSql::new(id, proof, &cipher)?.insert(&mut conn)?; + BurntProofSql::new(id, reciprocal_claim_public_key, proof, burned_at, &cipher)?.insert(&mut conn)?; }, } @@ -461,6 +462,10 @@ impl WalletSqliteDatabase { DbKey::TorId => { let _ = WalletSettingSql::clear(&DbKey::TorId, &mut conn)?; }, + DbKey::BurntProofId(proof_id) => { + BurntProofSql::delete(proof_id, &mut conn)?; + return Ok(None); + }, DbKey::CommsFeatures | DbKey::CommsAddress | DbKey::BaseNodeChainMetadata | @@ -472,7 +477,6 @@ impl WalletSqliteDatabase { DbKey::CommsIdentitySignature | DbKey::LastAccessedNetwork | DbKey::LastAccessedVersion | - DbKey::BurntProofId(_) | DbKey::ListBurntProofs => { return Err(WalletStorageError::OperationNotSupported); }, @@ -527,11 +531,31 @@ impl WalletBackend for WalletSqliteDatabase { .and_then(|bytes| IdentitySignature::from_bytes(&bytes).ok()) .map(Box::new) .map(DbValue::CommsIdentitySignature), - DbKey::BurntProofId(id) => { - BurntProofSql::get(*id, &mut conn)?.map(|BurntProofSql { id, value }| DbValue::BurntProof((id, value))) - }, + DbKey::BurntProofId(id) => BurntProofSql::get(*id, &mut conn)?.map( + |BurntProofSql { + id, + reciprocal_claim_public_key, + payload, + burned_at, + }| DbValue::BurntProof((id, reciprocal_claim_public_key, payload, burned_at)), + ), DbKey::ListBurntProofs => BurntProofSql::index(&mut conn) - .map(|proofs| DbValue::BurntProofs(proofs.into_iter().map(|x| (x.id, x.value)).collect_vec())) + .map(|proofs| { + DbValue::BurntProofs( + proofs + .into_iter() + .filter_map(|entry| match self.decrypt_value(entry) { + Ok(decrypted) => Some(( + decrypted.id, + decrypted.reciprocal_claim_public_key, + decrypted.payload, + decrypted.burned_at, + )), + Err(_) => None, + }) + .collect_vec(), + ) + }) .ok(), }; if start.elapsed().as_millis() > 0 { @@ -908,13 +932,26 @@ impl Encryptable for ClientKeyValueSql { #[diesel(table_name = burnt_proofs)] struct BurntProofSql { id: i32, - value: String, + reciprocal_claim_public_key: String, + payload: String, + burned_at: NaiveDateTime, } impl BurntProofSql { - pub fn new(id: i32, value: String, cipher: &XChaCha20Poly1305) -> Result { - let client_kv = Self { id, value }; - client_kv.encrypt(cipher).map_err(WalletStorageError::AeadError) + pub fn new( + id: i32, + reciprocal_claim_public_key: String, + payload: String, + burned_at: NaiveDateTime, + cipher: &XChaCha20Poly1305, + ) -> Result { + let entry = Self { + id, + reciprocal_claim_public_key, + payload, + burned_at, + }; + entry.encrypt(cipher).map_err(WalletStorageError::AeadError) } #[allow(dead_code)] @@ -923,7 +960,7 @@ impl BurntProofSql { } pub fn insert(&self, conn: &mut SqliteConnection) -> Result<(), WalletStorageError> { - diesel::replace_into(burnt_proofs::table).values(self).execute(conn)?; + diesel::insert_into(burnt_proofs::table).values(self).execute(conn)?; Ok(()) } @@ -959,10 +996,10 @@ impl Encryptable for BurntProofSql { #[allow(unused_assignments)] fn encrypt(mut self, cipher: &XChaCha20Poly1305) -> Result { - self.value = encrypt_bytes_integral_nonce( + self.payload = encrypt_bytes_integral_nonce( cipher, self.domain("value"), - Hidden::hide(self.value.as_bytes().to_vec()), + Hidden::hide(self.payload.as_bytes().to_vec()), )? .to_hex(); @@ -974,10 +1011,10 @@ impl Encryptable for BurntProofSql { let mut decrypted_value = decrypt_bytes_integral_nonce( cipher, self.domain("value"), - &from_hex(self.value.as_str()).map_err(|e| e.to_string())?, + &from_hex(self.payload.as_str()).map_err(|e| e.to_string())?, )?; - self.value = from_utf8(decrypted_value.as_slice()) + self.payload = from_utf8(decrypted_value.as_slice()) .map_err(|e| e.to_string())? .to_string();