diff --git a/Cargo.lock b/Cargo.lock index 13ceb125d4..de95582864 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6106,6 +6106,7 @@ dependencies = [ "tari_app_grpc", "tari_common_types", "tari_crypto", + "tari_key_manager", "tari_mmr", "tari_utilities", "tauri", diff --git a/applications/tari_base_node/src/grpc/base_node_grpc_server.rs b/applications/tari_base_node/src/grpc/base_node_grpc_server.rs index a6c5cdbd57..9260d9b24b 100644 --- a/applications/tari_base_node/src/grpc/base_node_grpc_server.rs +++ b/applications/tari_base_node/src/grpc/base_node_grpc_server.rs @@ -521,7 +521,7 @@ impl tari_rpc::base_node_server::BaseNode for BaseNodeGrpcServer { })); }, }; - Err(Status::unknown("Could not find a matching arm")) + // Err(Status::unknown("Could not find a matching arm")) } else { Err(Status::not_found("Could not find any utxo")) } diff --git a/applications/tari_collectibles/src-tauri/Cargo.toml b/applications/tari_collectibles/src-tauri/Cargo.toml index eebfd923f5..9fe17342d2 100644 --- a/applications/tari_collectibles/src-tauri/Cargo.toml +++ b/applications/tari_collectibles/src-tauri/Cargo.toml @@ -18,6 +18,7 @@ tauri-build = { version = "1.0.0-beta.4" } tari_app_grpc = { path = "../../tari_app_grpc" } tari_common_types = { path = "../../../base_layer/common_types" } tari_crypto = { git = "https://github.com/tari-project/tari-crypto.git", branch = "main" } +tari_key_manager = { path = "../../../base_layer/key_manager" } tari_mmr = { path = "../../../base_layer/mmr"} tari_utilities = "*" diff --git a/applications/tari_collectibles/src-tauri/migrations/2021-11-15-124424_wallet/down.sql b/applications/tari_collectibles/src-tauri/migrations/2021-11-15-124424_wallet/down.sql new file mode 100644 index 0000000000..291a97c5ce --- /dev/null +++ b/applications/tari_collectibles/src-tauri/migrations/2021-11-15-124424_wallet/down.sql @@ -0,0 +1 @@ +-- This file should undo anything in `up.sql` \ No newline at end of file diff --git a/applications/tari_collectibles/src-tauri/migrations/2021-11-15-124424_wallet/up.sql b/applications/tari_collectibles/src-tauri/migrations/2021-11-15-124424_wallet/up.sql new file mode 100644 index 0000000000..a04d981226 --- /dev/null +++ b/applications/tari_collectibles/src-tauri/migrations/2021-11-15-124424_wallet/up.sql @@ -0,0 +1,6 @@ +-- Your SQL goes here +create table wallets ( + id blob not null primary key, + name text, + cipher_seed blob not null unique +); \ No newline at end of file diff --git a/applications/tari_collectibles/src-tauri/src/app_state.rs b/applications/tari_collectibles/src-tauri/src/app_state.rs index c0f4256565..669e844939 100644 --- a/applications/tari_collectibles/src-tauri/src/app_state.rs +++ b/applications/tari_collectibles/src-tauri/src/app_state.rs @@ -28,7 +28,6 @@ use crate::{ StorageError, }, }; -use diesel::SqliteConnection; use std::sync::Arc; use tari_common_types::types::PublicKey; use tauri::async_runtime::RwLock; @@ -36,6 +35,7 @@ use tauri::async_runtime::RwLock; pub struct AppState { config: Settings, db_factory: SqliteDbFactory, + passphrase: Option, } #[derive(Clone)] @@ -50,10 +50,19 @@ impl ConcurrentAppState { inner: Arc::new(RwLock::new(AppState { db_factory: SqliteDbFactory::new(settings.data_dir.as_path()), config: settings, + passphrase: None, })), } } + pub async fn passphrase(&self) -> Option { + self.inner.read().await.passphrase.clone() + } + + pub async fn set_passphrase(&mut self, pass: Option) { + self.inner.write().await.passphrase = pass; + } + pub async fn create_wallet_client(&self) -> WalletClient { WalletClient::new(self.inner.read().await.config.wallet_grpc_address.clone()) } diff --git a/applications/tari_collectibles/src-tauri/src/commands/accounts/mod.rs b/applications/tari_collectibles/src-tauri/src/commands/accounts/mod.rs index a7bd304fae..b5df2e7c78 100644 --- a/applications/tari_collectibles/src-tauri/src/commands/accounts/mod.rs +++ b/applications/tari_collectibles/src-tauri/src/commands/accounts/mod.rs @@ -21,8 +21,8 @@ // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. use crate::{ app_state::ConcurrentAppState, - models::{Account, NewAccount}, - storage::{AccountsTableGateway, CollectiblesStorage}, + models::{Account, NewAccount, NewWallet, Wallet}, + storage::{AccountsTableGateway, CollectiblesStorage, WalletsTableGateway}, }; use tari_common_types::types::PublicKey; use tari_utilities::hex::Hex; diff --git a/applications/tari_collectibles/src-tauri/src/commands/mod.rs b/applications/tari_collectibles/src-tauri/src/commands/mod.rs index f16bd7b166..5189f00bb3 100644 --- a/applications/tari_collectibles/src-tauri/src/commands/mod.rs +++ b/applications/tari_collectibles/src-tauri/src/commands/mod.rs @@ -20,6 +20,19 @@ // 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 crate::app_state::ConcurrentAppState; + pub mod accounts; pub mod assets; pub mod tips; +pub mod wallets; + +#[tauri::command] +pub async fn create_db(state: tauri::State<'_, ConcurrentAppState>) -> Result<(), String> { + let _db = state + .create_db() + .await + .map_err(|e| format!("Could not connect to DB: {}", e))?; + + Ok(()) +} diff --git a/applications/tari_collectibles/src-tauri/src/commands/wallets/mod.rs b/applications/tari_collectibles/src-tauri/src/commands/wallets/mod.rs new file mode 100644 index 0000000000..ca5d78f547 --- /dev/null +++ b/applications/tari_collectibles/src-tauri/src/commands/wallets/mod.rs @@ -0,0 +1,116 @@ +// Copyright 2021. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// 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 crate::{ + app_state::ConcurrentAppState, + models::{NewWallet, Wallet, WalletInfo}, + storage::{CollectiblesStorage, WalletsTableGateway}, +}; +use tari_key_manager::{ + cipher_seed::CipherSeed, + mnemonic::{Mnemonic, MnemonicLanguage}, +}; +use uuid::Uuid; + +#[tauri::command] +pub(crate) async fn wallets_create( + name: Option, + passphrase: Option, + state: tauri::State<'_, ConcurrentAppState>, +) -> Result { + let new_wallet = NewWallet { + name, + cipher_seed: CipherSeed::new(), + }; + + let result = state + .create_db() + .await + .map_err(|e| format!("Could not connect to DB: {}", e))? + .wallets() + .insert(new_wallet, passphrase) + .map_err(|e| format!("Could not save wallet: {}", e))?; + Ok(result) +} + +#[tauri::command] +pub(crate) async fn wallets_list( + state: tauri::State<'_, ConcurrentAppState>, +) -> Result, String> { + let db = state + .create_db() + .await + .map_err(|e| format!("Could not connect to DB: {}", e))?; + + let result = db + .wallets() + .list() + .map_err(|e| format!("Could list wallets from DB: {}", e))?; + Ok(result) +} + +#[tauri::command] +pub(crate) async fn wallets_find( + id: String, + passphrase: Option, + state: tauri::State<'_, ConcurrentAppState>, +) -> Result { + let db = state + .create_db() + .await + .map_err(|e| format!("Could not connect to DB: {}", e))?; + + let uuid = Uuid::parse_str(&id).map_err(|e| format!("Failed to parse UUID: {}", e))?; + + let result = db + .wallets() + .find(uuid, passphrase) + .map_err(|e| e.to_string())?; + + Ok(result) +} + +#[tauri::command] +pub(crate) async fn wallets_seed_words( + id: String, + passphrase: Option, + state: tauri::State<'_, ConcurrentAppState>, +) -> Result, String> { + let db = state + .create_db() + .await + .map_err(|e| format!("Could not connect to DB: {}", e))?; + + let uuid = Uuid::parse_str(&id).map_err(|e| format!("Failed to parse UUID: {}", e))?; + + let wallet = db + .wallets() + .find(uuid, passphrase.clone()) + .map_err(|e| e.to_string())?; + + let seed_words = wallet + .cipher_seed + .to_mnemonic(&MnemonicLanguage::English, passphrase) + .map_err(|e| format!("Failed to convert cipher seed to seed words: {}", e))?; + + Ok(seed_words) +} diff --git a/applications/tari_collectibles/src-tauri/src/main.rs b/applications/tari_collectibles/src-tauri/src/main.rs index 74a67eca37..b47985a07c 100644 --- a/applications/tari_collectibles/src-tauri/src/main.rs +++ b/applications/tari_collectibles/src-tauri/src/main.rs @@ -26,12 +26,17 @@ fn main() { tauri::Builder::default() .manage(state) .invoke_handler(tauri::generate_handler![ + commands::create_db, commands::assets::assets_create, commands::assets::assets_list_owned, commands::assets::assets_list_registered_assets, commands::assets::assets_issue_simple_tokens, commands::accounts::accounts_create, - commands::accounts::accounts_list + commands::accounts::accounts_list, + commands::wallets::wallets_create, + commands::wallets::wallets_list, + commands::wallets::wallets_find, + commands::wallets::wallets_seed_words, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/applications/tari_collectibles/src-tauri/src/models/mod.rs b/applications/tari_collectibles/src-tauri/src/models/mod.rs index ae96611dfb..051e84a3ea 100644 --- a/applications/tari_collectibles/src-tauri/src/models/mod.rs +++ b/applications/tari_collectibles/src-tauri/src/models/mod.rs @@ -26,5 +26,7 @@ mod registered_asset_info; pub use registered_asset_info::RegisteredAssetInfo; mod account; pub use account::{Account, NewAccount}; +mod wallet; +pub use wallet::{NewWallet, Wallet, WalletInfo}; mod tip002_info; pub use tip002_info::Tip002Info; diff --git a/applications/tari_collectibles/src-tauri/src/models/wallet.rs b/applications/tari_collectibles/src-tauri/src/models/wallet.rs new file mode 100644 index 0000000000..39bcc2462f --- /dev/null +++ b/applications/tari_collectibles/src-tauri/src/models/wallet.rs @@ -0,0 +1,52 @@ +// Copyright 2021. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// 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 serde::{Deserialize, Serialize}; +use tari_key_manager::cipher_seed::CipherSeed; +use uuid::Uuid; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Wallet { + pub id: Uuid, + pub name: Option, + pub cipher_seed: CipherSeed, +} + +pub struct NewWallet { + pub name: Option, + pub cipher_seed: CipherSeed, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct WalletInfo { + pub id: Uuid, + pub name: Option, +} + +impl From for WalletInfo { + fn from(wallet: Wallet) -> Self { + WalletInfo { + id: wallet.id, + name: wallet.name, + } + } +} diff --git a/applications/tari_collectibles/src-tauri/src/schema.rs b/applications/tari_collectibles/src-tauri/src/schema.rs index 4a5e97d3c6..282fa09015 100644 --- a/applications/tari_collectibles/src-tauri/src/schema.rs +++ b/applications/tari_collectibles/src-tauri/src/schema.rs @@ -9,3 +9,11 @@ table! { committee_pub_keys -> Binary, } } + +table! { + wallets (id) { + id -> Binary, + name -> Nullable, + cipher_seed -> Binary, + } +} diff --git a/applications/tari_collectibles/src-tauri/src/storage/mod.rs b/applications/tari_collectibles/src-tauri/src/storage/mod.rs index 509cf84c4e..d3d49c185e 100644 --- a/applications/tari_collectibles/src-tauri/src/storage/mod.rs +++ b/applications/tari_collectibles/src-tauri/src/storage/mod.rs @@ -20,7 +20,7 @@ // 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 crate::models::{Account, NewAccount}; +use crate::models::{Account, NewAccount, NewWallet, Wallet, WalletInfo}; pub mod sqlite; mod storage_error; pub use storage_error::StorageError; @@ -28,7 +28,10 @@ use uuid::Uuid; pub trait CollectiblesStorage { type Accounts: AccountsTableGateway; + type Wallets: WalletsTableGateway; + fn accounts(&self) -> Self::Accounts; + fn wallets(&self) -> Self::Wallets; } pub trait AccountsTableGateway { @@ -36,3 +39,11 @@ pub trait AccountsTableGateway { fn insert(&self, account: NewAccount) -> Result; fn find(&self, account_id: Uuid) -> Result; } + +pub trait WalletsTableGateway { + type Passphrase; + + fn list(&self) -> Result, StorageError>; + fn insert(&self, wallet: NewWallet, pass: Self::Passphrase) -> Result; + fn find(&self, id: Uuid, pass: Self::Passphrase) -> Result; +} diff --git a/applications/tari_collectibles/src-tauri/src/storage/sqlite/mod.rs b/applications/tari_collectibles/src-tauri/src/storage/sqlite/mod.rs index f2e34d60ea..ee340ff6d7 100644 --- a/applications/tari_collectibles/src-tauri/src/storage/sqlite/mod.rs +++ b/applications/tari_collectibles/src-tauri/src/storage/sqlite/mod.rs @@ -1,12 +1,3 @@ -use crate::{ - models::{Account, NewAccount}, - storage::{AccountsTableGateway, CollectiblesStorage, StorageError}, -}; -use diesel::{Connection, SqliteConnection}; -use std::path::Path; -use tari_utilities::ByteArray; -use uuid::Uuid; - // Copyright 2021. The Tari Project // // Redistribution and use in source and binary forms, with or without modification, are permitted provided that the @@ -28,15 +19,25 @@ use uuid::Uuid; // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, // 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. -pub mod models; -use crate::schema::{self, accounts::dsl::accounts, *}; -use diesel::prelude::*; -use std::fs; + +use crate::{ + models::{Account, NewAccount, NewWallet, Wallet, WalletInfo}, + schema::{self, *}, + storage::{AccountsTableGateway, CollectiblesStorage, StorageError, WalletsTableGateway}, +}; +use diesel::{prelude::*, Connection, SqliteConnection}; +use std::{fs, path::Path}; use tari_common_types::types::PublicKey; +use tari_key_manager::{cipher_seed::CipherSeed, error::KeyManagerError}; +use tari_utilities::ByteArray; +use uuid::Uuid; + +pub mod models; pub struct SqliteDbFactory { database_url: String, } + impl SqliteDbFactory { pub fn new(data_dir: &Path) -> Self { fs::create_dir_all(data_dir) @@ -52,7 +53,7 @@ impl SqliteDbFactory { pub fn create_db(&self) -> Result { let connection = SqliteConnection::establish(self.database_url.as_str())?; - connection.execute("PRAGMA foreign_keys = ON;"); + connection.execute("PRAGMA foreign_keys = ON;")?; // Create the db embed_migrations!("./migrations"); embedded_migrations::run(&connection)?; @@ -68,12 +69,19 @@ pub struct SqliteCollectiblesStorage { impl CollectiblesStorage for SqliteCollectiblesStorage { type Accounts = SqliteAccountsTableGateway; + type Wallets = SqliteWalletsTableGateway; fn accounts(&self) -> Self::Accounts { SqliteAccountsTableGateway { database_url: self.database_url.clone(), } } + + fn wallets(&self) -> Self::Wallets { + SqliteWalletsTableGateway { + database_url: self.database_url.clone(), + } + } } pub struct SqliteAccountsTableGateway { @@ -89,7 +97,7 @@ impl AccountsTableGateway for SqliteAccountsTableGateway { Ok( results .iter() - .map(|r| SqliteAccountsTableGateway::convert_account(r)) + .map(SqliteAccountsTableGateway::convert_account) .collect::>()?, ) } @@ -119,7 +127,6 @@ impl AccountsTableGateway for SqliteAccountsTableGateway { }; let conn = SqliteConnection::establish(self.database_url.as_str())?; - use crate::schema::accounts; diesel::insert_into(accounts::table) .values(sql_model) .execute(&conn)?; @@ -164,3 +171,80 @@ impl SqliteAccountsTableGateway { }) } } + +pub struct SqliteWalletsTableGateway { + database_url: String, +} + +impl SqliteWalletsTableGateway { + fn convert_wallet(w: &models::Wallet, pass: Option) -> Result { + let cipher_seed = match CipherSeed::from_enciphered_bytes(&w.cipher_seed, pass) { + Ok(seed) => seed, + Err(e) if matches!(e, KeyManagerError::DecryptionFailed) => { + return Err(StorageError::WrongPassword) + } + Err(e) => return Err(e.into()), + }; + + Ok(Wallet { + id: Uuid::from_slice(&w.id)?, + name: w.name.clone(), + cipher_seed, + }) + } +} + +impl WalletsTableGateway for SqliteWalletsTableGateway { + type Passphrase = Option; + + fn list(&self) -> Result, StorageError> { + let conn = SqliteConnection::establish(self.database_url.as_str())?; + let results: Vec = schema::wallets::table.load(&conn)?; + Ok( + results + .iter() + .map(|w| WalletInfo { + id: Uuid::from_slice(&w.id).unwrap(), + name: w.name.clone(), + }) + .collect(), + ) + } + + fn insert( + &self, + wallet: NewWallet, + passphrase: Self::Passphrase, + ) -> Result { + let id = Uuid::new_v4(); + + // todo: error + let sql_model = models::Wallet { + id: Vec::from(id.as_bytes().as_slice()), + name: wallet.name.clone(), + cipher_seed: wallet.cipher_seed.encipher(passphrase).unwrap(), + }; + let conn = SqliteConnection::establish(self.database_url.as_str())?; + + // use crate::schema::wallets; + diesel::insert_into(wallets::table) + .values(sql_model) + .execute(&conn)?; + + let result = Wallet { + id, + name: wallet.name, + cipher_seed: wallet.cipher_seed, + }; + Ok(result) + } + + fn find(&self, id: Uuid, passphrase: Self::Passphrase) -> Result { + let conn = SqliteConnection::establish(self.database_url.as_str())?; + let db_wallet = schema::wallets::table + .find(Vec::from(id.as_bytes().as_slice())) + .get_result(&conn)?; + + SqliteWalletsTableGateway::convert_wallet(&db_wallet, passphrase) + } +} diff --git a/applications/tari_collectibles/src-tauri/src/storage/sqlite/models/mod.rs b/applications/tari_collectibles/src-tauri/src/storage/sqlite/models/mod.rs index a13d48aa50..0811e140f7 100644 --- a/applications/tari_collectibles/src-tauri/src/storage/sqlite/models/mod.rs +++ b/applications/tari_collectibles/src-tauri/src/storage/sqlite/models/mod.rs @@ -21,4 +21,7 @@ // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. mod account; +mod wallet; + pub use account::Account; +pub use wallet::Wallet; diff --git a/applications/tari_collectibles/src-tauri/src/storage/sqlite/models/wallet.rs b/applications/tari_collectibles/src-tauri/src/storage/sqlite/models/wallet.rs new file mode 100644 index 0000000000..e6f0709382 --- /dev/null +++ b/applications/tari_collectibles/src-tauri/src/storage/sqlite/models/wallet.rs @@ -0,0 +1,31 @@ +// Copyright 2021. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// 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 crate::schema::*; +// use diesel::prelude::*; + +#[derive(Queryable, Insertable, Identifiable)] +pub struct Wallet { + pub id: Vec, + pub name: Option, + pub cipher_seed: Vec, +} diff --git a/applications/tari_collectibles/src-tauri/src/storage/storage_error.rs b/applications/tari_collectibles/src-tauri/src/storage/storage_error.rs index 97a96583c2..e422be7aaa 100644 --- a/applications/tari_collectibles/src-tauri/src/storage/storage_error.rs +++ b/applications/tari_collectibles/src-tauri/src/storage/storage_error.rs @@ -35,9 +35,21 @@ pub enum StorageError { #[from] source: diesel::result::Error, }, - #[error("Could not migrate the database")] + #[error("Could not migrate the database: {source}")] MigrationError { #[from] source: diesel_migrations::RunMigrationsError, }, + #[error("UUID error: {source}")] + UuidError { + #[from] + source: uuid::Error, + }, + #[error("KeyManager error: {source}")] + KeyManagerError { + #[from] + source: tari_key_manager::error::KeyManagerError, + }, + #[error("The password is incorrect")] + WrongPassword, } diff --git a/applications/tari_collectibles/src-tauri/temp.sqlite b/applications/tari_collectibles/src-tauri/temp.sqlite deleted file mode 100644 index a3afcb0331..0000000000 Binary files a/applications/tari_collectibles/src-tauri/temp.sqlite and /dev/null differ diff --git a/applications/tari_collectibles/web-app/public/index.html b/applications/tari_collectibles/web-app/public/index.html index 217267f640..eb88197996 100644 --- a/applications/tari_collectibles/web-app/public/index.html +++ b/applications/tari_collectibles/web-app/public/index.html @@ -24,7 +24,7 @@ Learn how to configure a non-root public URL by running `npm run build`. --> - React App + Tari Collectibles + -
- diff --git a/applications/tari_collectibles/web-app/src/App.js b/applications/tari_collectibles/web-app/src/App.js index 8db6f2ec61..b532546192 100644 --- a/applications/tari_collectibles/web-app/src/App.js +++ b/applications/tari_collectibles/web-app/src/App.js @@ -25,21 +25,26 @@ import { Route, BrowserRouter as Router, Switch, + Redirect, Link as RouterLink, } from "react-router-dom"; import { createTheme } from "@mui/material/styles"; import { AppBar, Box, - CssBaseline, Divider, - Drawer, IconButton, + CssBaseline, + Divider, + Drawer, + IconButton, List, ListItem, ListItemIcon, - ListItemText, ListSubheader, + ListItemText, + ListSubheader, Toolbar, Typography, } from "@mui/material"; +import AccountBalanceWalletIcon from "@mui/icons-material/AccountBalanceWallet"; import DashboardIcon from "@mui/icons-material/Dashboard"; import CreateIcon from "@mui/icons-material/Create"; import AddIcon from "@mui/icons-material/Add"; @@ -54,8 +59,10 @@ import Manage from "./Manage"; import AssetManager from "./AssetManager"; import AccountDashboard from "./AccountDashboard"; import NewAccount from "./NewAccount"; -import {useEffect, useState} from "react"; +import Setup, { UnlockWallet } from "./Setup"; +import { useEffect, useState } from "react"; import binding from "./binding"; +import { Spinner } from "./components"; const mdTheme = createTheme({ palette: { @@ -66,16 +73,19 @@ const mdTheme = createTheme({ }); function IconButtonLink(props) { - const { icon, to} = props; + const { icon, to } = props; const renderLink = React.useMemo( - () => React.forwardRef(function Link(itemProps, ref) { + () => + React.forwardRef(function Link(itemProps, ref) { return ; }), - [to] - ) + [to] + ); return ( - {icon} + + {icon} + ); } @@ -91,12 +101,10 @@ function ListItemLink(props) { ); return ( -
  • - - {icon ? {icon} : null} - - -
  • + + {icon ? {icon} : null} + + ); } @@ -106,15 +114,82 @@ ListItemLink.propTypes = { to: PropTypes.string.isRequired, }; -function App() { +const AccountsMenu = (props) => { const [accounts, setAccounts] = useState([]); - useEffect(async () => { - let a = await binding.command_accounts_list(); - console.log(a); - setAccounts(a); + useEffect(() => { + binding + .command_accounts_list() + .then((accounts) => { + console.log("accounts", accounts); + setAccounts(accounts); + }) + .catch((e) => { + // todo error handling + console.error("accounts_list error:", e); + }); + }, []); - }); + // todo: hide accounts when not authenticated + return ( +
    + + } + to="/accounts/new" + > + } + > + My Assets + + + + {accounts.map((item) => { + return ( + + ); + })} + +
    + ); +}; + +// only allow access to a Protected Route if the wallet is unlocked +const ProtectedRoute = ({ authenticated, path, children }) => { + if (!authenticated) return ; + + return {children}; +}; + +ProtectedRoute.propTypes = { + authenticated: PropTypes.bool.isRequired, + path: PropTypes.string.isRequired, + children: PropTypes.node.isRequired, +}; + +function App() { + const [loading, setLoading] = useState(true); + const [authenticated, setAuthenticated] = useState(false); + const [walletId, setWalletId] = useState(""); + const [password, setPassword] = useState(""); + + // todo: screen lock after x mins no activity + + // ensure db created or open before other components try to make db calls + useEffect(() => { + binding + .command_create_db() + .then((r) => setLoading(false)) + .catch((e) => console.error(e)); + }, []); + if (loading) return ; return (
    @@ -128,7 +203,11 @@ function App() { - Tari Collectibles + + + Tari Collectibles + + } /> - } to="/accounts/new"> - - }>My Assets - { accounts.map((item) => { - return (); - })} + Issued Assets } /> + + My Wallet + } + /> - + - - + + - - - - + + - - + + - - + + + + + + + + { + setWalletId(id); + setPassword(password); + setAuthenticated(true); + }} + /> - + diff --git a/applications/tari_collectibles/web-app/src/App.test.js b/applications/tari_collectibles/web-app/src/App.test.js deleted file mode 100644 index 1f03afeece..0000000000 --- a/applications/tari_collectibles/web-app/src/App.test.js +++ /dev/null @@ -1,8 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import App from './App'; - -test('renders learn react link', () => { - render(); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); -}); diff --git a/applications/tari_collectibles/web-app/src/Dashboard.js b/applications/tari_collectibles/web-app/src/Dashboard.js index b6c208f647..91c312a285 100644 --- a/applications/tari_collectibles/web-app/src/Dashboard.js +++ b/applications/tari_collectibles/web-app/src/Dashboard.js @@ -26,8 +26,7 @@ import { AssetCard, Spinner } from "./components"; import binding from "./binding"; import { toHexString } from "./helpers"; -const explorerUrl = (blockHash) => - `https:://explore.tari.com/block/${blockHash.toString("hex")}`; +// const explorerUrl = (blockHash) => `https:://explore.tari.com/block/${blockHash.toString("hex")}`; class DashboardContent extends React.Component { constructor(props) { @@ -85,7 +84,13 @@ class DashboardContent extends React.Component { ); return ( - + ); diff --git a/applications/tari_collectibles/web-app/src/NewAccount.js b/applications/tari_collectibles/web-app/src/NewAccount.js index 9484c0d6dd..ea7e865098 100644 --- a/applications/tari_collectibles/web-app/src/NewAccount.js +++ b/applications/tari_collectibles/web-app/src/NewAccount.js @@ -20,70 +20,79 @@ // 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. -import React, { useState, useMemo } from "react"; -import {withRouter} from "react-router-dom"; -import {Alert, Button, Container, Stack, TextField, Typography} from "@mui/material"; +import React from "react"; +import { withRouter } from "react-router-dom"; +import { + Alert, + Button, + Container, + Stack, + TextField, + Typography, +} from "@mui/material"; import binding from "./binding"; class NewAccount extends React.Component { - constructor(props) { - super(props); + constructor(props) { + super(props); - this.state = { - error: null, - isSaving: false, - assetPublicKey: "" - }; - } - - onAssetPublicKeyChanged = (e) => { - this.setState({ assetPublicKey: e.target.value }); + this.state = { + error: null, + isSaving: false, + assetPublicKey: "", }; + } - onSave = async (e) => { - e.preventDefault(); - console.log(this.state.assetPublicKey); - this.setState({isSaving: true, error: null}); - try{ - await binding.command_accounts_create(this.state.assetPublicKey); - let history = this.props.history; -let path =`/assets/watched/details/${this.state.assetPublicKey}`; -console.log(path); - history.push(path); - return; - }catch(e) { - this.setState({error: e}); - console.error(e); - } - this.setState({isSaving: false}); - } + onAssetPublicKeyChanged = (e) => { + this.setState({ assetPublicKey: e.target.value }); + }; - render() { - return ( - - New asset account - - - {this.state.error ? ( - {this.state.error} - ) : ( - - )} - - - - ); + onSave = async (e) => { + e.preventDefault(); + console.log(this.state.assetPublicKey); + this.setState({ isSaving: true, error: null }); + try { + await binding.command_accounts_create(this.state.assetPublicKey); + let history = this.props.history; + let path = `/assets/watched/details/${this.state.assetPublicKey}`; + console.log(path); + history.push(path); + return; + } catch (e) { + this.setState({ error: e }); + console.error(e); } + this.setState({ isSaving: false }); + }; + + render() { + return ( + + + New asset account + + + {this.state.error ? ( + {this.state.error} + ) : ( + + )} + + + + + ); + } } export default withRouter(NewAccount); diff --git a/applications/tari_collectibles/web-app/src/Setup.js b/applications/tari_collectibles/web-app/src/Setup.js new file mode 100644 index 0000000000..c9682e36b8 --- /dev/null +++ b/applications/tari_collectibles/web-app/src/Setup.js @@ -0,0 +1,233 @@ +// Copyright 2021. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// 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. + +import { + Container, + Button, + TextField, + Stack, + Typography, + FormGroup, +} from "@mui/material"; +import React, { useState, useEffect } from "react"; +import { Spinner } from "./components"; +import binding from "./binding"; +import { withRouter, useParams } from "react-router-dom"; + +const chunk = (arr, len) => { + const chunks = []; + let i = 0; + let n = arr.length; + + while (i < n) { + chunks.push(arr.slice(i, (i += len))); + } + + return chunks; +}; + +const SeedWords = ({ wallet, password, history }) => { + const [seedWords, setSeedWords] = useState([]); + useEffect(() => { + binding + .command_wallets_seed_words(wallet.id, password) + .then((words) => setSeedWords(words)) + .catch((e) => console.error("error: ", e)); + }, [wallet.id, password]); + + const display = (seedWords) => { + console.log(seedWords); + if (seedWords.length === 0) return ; + + const chunks = chunk(seedWords, 6); + return ( +
    + {chunks.map((words, i) => ( +
    {words.join(" ")}
    + ))} +
    + ); + }; + + return ( +
    + + Seed words + +

    + Save these seed words securely. This is the recovery phrase for this + wallet. +

    + {display(seedWords)} + +
    + ); +}; + +const CreateWallet = ({ history }) => { + const [password, setPassword] = useState(""); + const [password2, setPassword2] = useState(""); + const [creating, setCreating] = useState(false); + const [wallet, setWallet] = useState(undefined); + + if (wallet) + return ; + + let valid = false; + let helperText = "Passwords must match."; + if (password === password2) { + valid = true; + helperText = ""; + } + + const create = async () => { + setCreating(true); + // todo: error handle + const wallet = await binding.command_wallets_create(password, "main"); + console.log("wallet", wallet); + setWallet(wallet); + }; + + return ( +
    + + Create new wallet + + + + setPassword(e.target.value)} + > + setPassword2(e.target.value)} + > + + + +
    + ); +}; + +const OpenWallet = ({ history, setAuthenticated }) => { + const { id } = useParams(); + const [password, setPassword] = useState(""); + const [unlocking, setUnlocking] = useState(false); + const [error, setError] = useState(""); + const isError = error !== ""; + + const unlock = async () => { + setUnlocking(true); + try { + await binding.command_wallets_find(id, password); + setAuthenticated(id, password); + history.push("/dashboard"); + } catch (e) { + console.error("error: ", e); + setUnlocking(false); + setError(e); + } + }; + + return ( + + + Unlock wallet + + + + { + setPassword(e.target.value); + setError(""); + }} + > + + + + + ); +}; + +const Setup = ({ history }) => { + const [loading, setLoading] = useState(true); + + useEffect(() => { + binding + .command_wallets_list() + .then((wallets) => { + console.log("wallets", wallets); + if (wallets.length > 0) { + const wallet = wallets[0]; + history.push(`/wallets/${wallet.id}`); + } else { + setLoading(false); + } + }) + .catch((e) => { + // todo error handling + console.error("wallets_list error:", e); + }); + }, [history]); + + if (loading) return ; + + return ( + + + + ); +}; + +const UnlockWallet = withRouter(OpenWallet); +export { UnlockWallet }; +export default withRouter(Setup); diff --git a/applications/tari_collectibles/web-app/src/binding.js b/applications/tari_collectibles/web-app/src/binding.js index 3d88f78bc3..4994b7c313 100644 --- a/applications/tari_collectibles/web-app/src/binding.js +++ b/applications/tari_collectibles/web-app/src/binding.js @@ -19,6 +19,7 @@ // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, // 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. + import { invoke } from "@tauri-apps/api/tauri"; async function command_assets_create(name, description, image) { @@ -45,21 +46,46 @@ async function command_asset_issue_simple_tokens( }); } +async function command_wallets_create(passphrase, name) { + return await invoke("wallets_create", { passphrase, name }); +} + +async function command_wallets_find(id, passphrase) { + return await invoke("wallets_find", { id, passphrase }); +} + +async function command_wallets_seed_words(id, passphrase) { + return await invoke("wallets_seed_words", { id, passphrase }); +} + +async function command_wallets_list() { + return await invoke("wallets_list"); +} + async function command_accounts_create(assetPubKey) { - return await invoke("accounts_create", {assetPubKey}); + return await invoke("accounts_create", { assetPubKey }); } async function command_accounts_list() { return await invoke("accounts_list", {}); } +async function command_create_db() { + return await invoke("create_db", {}); +} + const commands = { + command_create_db, command_assets_create, command_assets_list_owned, command_assets_list_registered_assets, command_asset_issue_simple_tokens, command_accounts_create, - command_accounts_list + command_accounts_list, + command_wallets_create, + command_wallets_list, + command_wallets_find, + command_wallets_seed_words, }; export default commands;