From 77e5ca92596f8309033913e9f1a372dbbee9d4c4 Mon Sep 17 00:00:00 2001 From: stringhandler Date: Mon, 5 Aug 2024 14:59:23 +0200 Subject: [PATCH] feat(wallet): add view key commands (#6426) Description --- Adds commands for exporting a view key and spend key for an existing wallet, and also commands for creating a read-only wallet that only has access to the view key. Note this is only possible to the great work by @SWvheerden who enabled this functionality in the wallet. Motivation and Context --- This allows you to keep an offline cold wallet that has spend access, and at the same time listen for received funds and act on them. This enables ecommerce and exchange support without the danger of running a hot spending wallet. How Has This Been Tested? --- Tested locally What process can a PR reviewer use to test or verify this change? --- You can follow the steps in the [exchange guide](https://github.com/tari-project/tari-dot-com/pull/231) to use all of the additions in this PR Breaking Changes --- - [x] None - [ ] Requires data directory on base node to be deleted - [ ] Requires hard fork - [ ] Other - Please specify --- .../minotari_app_grpc/proto/wallet.proto | 380 +++++++++--------- .../src/automation/commands.rs | 29 ++ .../minotari_console_wallet/src/cli.rs | 12 + .../minotari_console_wallet/src/grpc/mod.rs | 3 + .../src/grpc/wallet_grpc_server.rs | 5 + .../minotari_console_wallet/src/init/mod.rs | 94 ++++- .../minotari_console_wallet/src/lib.rs | 10 +- .../src/wallet_modes.rs | 1 + .../transaction_components/encrypted_data.rs | 4 +- .../transaction_service/storage/sqlite_db.rs | 2 +- base_layer/wallet_ffi/src/lib.rs | 2 +- integration_tests/src/wallet_process.rs | 2 + 12 files changed, 346 insertions(+), 198 deletions(-) diff --git a/applications/minotari_app_grpc/proto/wallet.proto b/applications/minotari_app_grpc/proto/wallet.proto index 25d660c3dc..cf20a908a0 100644 --- a/applications/minotari_app_grpc/proto/wallet.proto +++ b/applications/minotari_app_grpc/proto/wallet.proto @@ -31,96 +31,96 @@ import "network.proto"; // The gRPC interface for interacting with the wallet. service Wallet { - // This returns the current version - rpc GetVersion (GetVersionRequest) returns (GetVersionResponse); - // This checks if the wallet is healthy and running - rpc CheckConnectivity(GetConnectivityRequest) returns (CheckConnectivityResponse); - // Check for new updates - rpc CheckForUpdates (Empty) returns (SoftwareUpdate); - // This returns the identity information - rpc Identify (GetIdentityRequest) returns (GetIdentityResponse); - // This returns the tari address - rpc GetAddress (Empty) returns (GetAddressResponse); - // Send Minotari to a number of recipients - rpc Transfer (TransferRequest) returns (TransferResponse); - // Returns the transaction details for the given transaction IDs - rpc GetTransactionInfo (GetTransactionInfoRequest) returns (GetTransactionInfoResponse); - // Returns all transactions' details - rpc GetCompletedTransactions (GetCompletedTransactionsRequest) returns (stream GetCompletedTransactionsResponse); - // Returns the balance - rpc GetBalance (GetBalanceRequest) returns (GetBalanceResponse); - // Returns unspent amounts - rpc GetUnspentAmounts (Empty) returns (GetUnspentAmountsResponse); - // Request the wallet perform a coinsplit - rpc CoinSplit (CoinSplitRequest) returns (CoinSplitResponse); - // Import Utxo to wallet - rpc ImportUtxos (ImportUtxosRequest) returns (ImportUtxosResponse); - // Get Base Node network connectivity status - rpc GetNetworkStatus(Empty) returns (NetworkStatusResponse); - // List currently connected peers - rpc ListConnectedPeers(Empty) returns (ListConnectedPeersResponse); - // Cancel pending transaction - rpc CancelTransaction (CancelTransactionRequest) returns (CancelTransactionResponse); - // Will trigger a complete revalidation of all wallet outputs. - rpc RevalidateAllTransactions (RevalidateRequest) returns (RevalidateResponse); - // Will trigger a validation of all wallet outputs. - rpc ValidateAllTransactions (ValidateRequest) returns (ValidateResponse); - // This will send a XTR SHA Atomic swap transaction - rpc SendShaAtomicSwapTransaction(SendShaAtomicSwapRequest) returns (SendShaAtomicSwapResponse); - // This will create a burn transaction - rpc CreateBurnTransaction(CreateBurnTransactionRequest) returns (CreateBurnTransactionResponse); - // This will claim a XTR SHA Atomic swap transaction - rpc ClaimShaAtomicSwapTransaction(ClaimShaAtomicSwapRequest) returns (ClaimShaAtomicSwapResponse); - // This will claim a HTLC refund transaction - rpc ClaimHtlcRefundTransaction(ClaimHtlcRefundRequest) returns (ClaimHtlcRefundResponse); - // Creates a transaction with a template registration output - rpc CreateTemplateRegistration(CreateTemplateRegistrationRequest) returns (CreateTemplateRegistrationResponse); - rpc SetBaseNode(SetBaseNodeRequest) returns (SetBaseNodeResponse); - - rpc StreamTransactionEvents(TransactionEventRequest) returns (stream TransactionEventResponse); - - rpc RegisterValidatorNode(RegisterValidatorNodeRequest) returns (RegisterValidatorNodeResponse); -} - -message GetVersionRequest { } + // This returns the current version + rpc GetVersion (GetVersionRequest) returns (GetVersionResponse); + // This checks if the wallet is healthy and running + rpc CheckConnectivity(GetConnectivityRequest) returns (CheckConnectivityResponse); + // Check for new updates + rpc CheckForUpdates (Empty) returns (SoftwareUpdate); + // This returns the identity information + rpc Identify (GetIdentityRequest) returns (GetIdentityResponse); + // This returns the tari address + rpc GetAddress (Empty) returns (GetAddressResponse); + // Send Minotari to a number of recipients + rpc Transfer (TransferRequest) returns (TransferResponse); + // Returns the transaction details for the given transaction IDs + rpc GetTransactionInfo (GetTransactionInfoRequest) returns (GetTransactionInfoResponse); + // Returns all transactions' details + rpc GetCompletedTransactions (GetCompletedTransactionsRequest) returns (stream GetCompletedTransactionsResponse); + // Returns the balance + rpc GetBalance (GetBalanceRequest) returns (GetBalanceResponse); + // Returns unspent amounts + rpc GetUnspentAmounts (Empty) returns (GetUnspentAmountsResponse); + // Request the wallet perform a coinsplit + rpc CoinSplit (CoinSplitRequest) returns (CoinSplitResponse); + // Import Utxo to wallet + rpc ImportUtxos (ImportUtxosRequest) returns (ImportUtxosResponse); + // Get Base Node network connectivity status + rpc GetNetworkStatus(Empty) returns (NetworkStatusResponse); + // List currently connected peers + rpc ListConnectedPeers(Empty) returns (ListConnectedPeersResponse); + // Cancel pending transaction + rpc CancelTransaction (CancelTransactionRequest) returns (CancelTransactionResponse); + // Will trigger a complete revalidation of all wallet outputs. + rpc RevalidateAllTransactions (RevalidateRequest) returns (RevalidateResponse); + // Will trigger a validation of all wallet outputs. + rpc ValidateAllTransactions (ValidateRequest) returns (ValidateResponse); + // This will send a XTR SHA Atomic swap transaction + rpc SendShaAtomicSwapTransaction(SendShaAtomicSwapRequest) returns (SendShaAtomicSwapResponse); + // This will create a burn transaction + rpc CreateBurnTransaction(CreateBurnTransactionRequest) returns (CreateBurnTransactionResponse); + // This will claim a XTR SHA Atomic swap transaction + rpc ClaimShaAtomicSwapTransaction(ClaimShaAtomicSwapRequest) returns (ClaimShaAtomicSwapResponse); + // This will claim a HTLC refund transaction + rpc ClaimHtlcRefundTransaction(ClaimHtlcRefundRequest) returns (ClaimHtlcRefundResponse); + // Creates a transaction with a template registration output + rpc CreateTemplateRegistration(CreateTemplateRegistrationRequest) returns (CreateTemplateRegistrationResponse); + rpc SetBaseNode(SetBaseNodeRequest) returns (SetBaseNodeResponse); + + rpc StreamTransactionEvents(TransactionEventRequest) returns (stream TransactionEventResponse); + + rpc RegisterValidatorNode(RegisterValidatorNodeRequest) returns (RegisterValidatorNodeResponse); +} + +message GetVersionRequest {} message GetVersionResponse { - string version = 1; + string version = 1; } message GetAddressResponse { - bytes address = 1; + bytes address = 1; } message TransferRequest { - repeated PaymentRecipient recipients = 1; + repeated PaymentRecipient recipients = 1; } message SendShaAtomicSwapRequest { - PaymentRecipient recipient = 1; + PaymentRecipient recipient = 1; } message CreateBurnTransactionRequest{ - uint64 amount = 1; - uint64 fee_per_gram = 2; - string message = 3; - bytes claim_public_key = 4; + uint64 amount = 1; + uint64 fee_per_gram = 2; + string message = 3; + bytes claim_public_key = 4; } message PaymentRecipient { - string address = 1; - uint64 amount = 2; - uint64 fee_per_gram = 3; - string message = 4; - enum PaymentType { - STANDARD_MIMBLEWIMBLE = 0; - ONE_SIDED = 1; - ONE_SIDED_TO_STEALTH_ADDRESS = 2; - } - PaymentType payment_type = 5; - bytes payment_id = 6; + string address = 1; + uint64 amount = 2; + uint64 fee_per_gram = 3; + string message = 4; + enum PaymentType { + STANDARD_MIMBLEWIMBLE = 0; + ONE_SIDED = 1; + ONE_SIDED_TO_STEALTH_ADDRESS = 2; + } + PaymentType payment_type = 5; + bytes payment_id = 6; } message TransferResponse { @@ -128,166 +128,167 @@ message TransferResponse { } message SendShaAtomicSwapResponse { - uint64 transaction_id = 1; - string pre_image = 2; - string output_hash = 3; - bool is_success = 4; - string failure_message = 5; + uint64 transaction_id = 1; + string pre_image = 2; + string output_hash = 3; + bool is_success = 4; + string failure_message = 5; } message CreateBurnTransactionResponse{ - uint64 transaction_id = 1; - bool is_success = 2; - string failure_message = 3; - bytes commitment = 4; - CommitmentSignature ownership_proof = 5; - bytes range_proof = 6; - bytes reciprocal_claim_public_key = 7; + uint64 transaction_id = 1; + bool is_success = 2; + string failure_message = 3; + bytes commitment = 4; + CommitmentSignature ownership_proof = 5; + bytes range_proof = 6; + bytes reciprocal_claim_public_key = 7; } message TransferResult { - string address = 1; - uint64 transaction_id = 2; - bool is_success = 3; - string failure_message = 4; + string address = 1; + uint64 transaction_id = 2; + bool is_success = 3; + string failure_message = 4; } message ClaimShaAtomicSwapRequest{ - string output = 1; - string pre_image = 2; - uint64 fee_per_gram = 3; + string output = 1; + string pre_image = 2; + uint64 fee_per_gram = 3; } message ClaimShaAtomicSwapResponse { - TransferResult results = 1; + TransferResult results = 1; } message ClaimHtlcRefundRequest{ - string output_hash = 1; - uint64 fee_per_gram = 2; + string output_hash = 1; + uint64 fee_per_gram = 2; } message ClaimHtlcRefundResponse { - TransferResult results = 1; + TransferResult results = 1; } message GetTransactionInfoRequest { - repeated uint64 transaction_ids = 1; + repeated uint64 transaction_ids = 1; } message GetTransactionInfoResponse { - repeated TransactionInfo transactions = 1; + repeated TransactionInfo transactions = 1; } message TransactionInfo { - uint64 tx_id = 1; - bytes source_address = 2; - bytes dest_address = 3; - TransactionStatus status = 4; - TransactionDirection direction = 5; - uint64 amount = 6; - uint64 fee = 7; - bool is_cancelled = 8; - bytes excess_sig = 9; - uint64 timestamp = 10; - string message = 11; + uint64 tx_id = 1; + bytes source_address = 2; + bytes dest_address = 3; + TransactionStatus status = 4; + TransactionDirection direction = 5; + uint64 amount = 6; + uint64 fee = 7; + bool is_cancelled = 8; + bytes excess_sig = 9; + uint64 timestamp = 10; + string message = 11; + bytes payment_id = 12; } enum TransactionDirection { - TRANSACTION_DIRECTION_UNKNOWN = 0; - TRANSACTION_DIRECTION_INBOUND = 1; - TRANSACTION_DIRECTION_OUTBOUND = 2; + TRANSACTION_DIRECTION_UNKNOWN = 0; + TRANSACTION_DIRECTION_INBOUND = 1; + TRANSACTION_DIRECTION_OUTBOUND = 2; } enum TransactionStatus { - // This transaction has been completed between the parties but has not been broadcast to the base layer network. - TRANSACTION_STATUS_COMPLETED = 0; - // This transaction has been broadcast to the base layer network and is currently in one or more base node mempools. - TRANSACTION_STATUS_BROADCAST = 1; - // This transaction has been mined and included in a block. - TRANSACTION_STATUS_MINED_UNCONFIRMED = 2; - // This transaction was generated as part of importing a spendable UTXO - TRANSACTION_STATUS_IMPORTED = 3; - // This transaction is still being negotiated by the parties - TRANSACTION_STATUS_PENDING = 4; - // This is a created Coinbase Transaction - TRANSACTION_STATUS_COINBASE = 5; - // This transaction is mined and confirmed at the current base node's height - TRANSACTION_STATUS_MINED_CONFIRMED = 6; - // The transaction was rejected by the mempool - TRANSACTION_STATUS_REJECTED = 7; - // This is faux transaction mainly for one-sided transaction outputs or wallet recovery outputs have been found - TRANSACTION_STATUS_ONE_SIDED_UNCONFIRMED = 8; - // All Imported and FauxUnconfirmed transactions will end up with this status when the outputs have been confirmed - TRANSACTION_STATUS_ONE_SIDED_CONFIRMED = 9; - // This transaction is still being queued for sending - TRANSACTION_STATUS_QUEUED = 10; - // The transaction was not found by the wallet its in transaction database - TRANSACTION_STATUS_NOT_FOUND = 11; - // This is Coinbase transaction that is detected from chain - TRANSACTION_STATUS_COINBASE_UNCONFIRMED = 12; - // This is Coinbase transaction that is detected from chain - TRANSACTION_STATUS_COINBASE_CONFIRMED = 13; - // This is Coinbase transaction that is not currently detected as mined - TRANSACTION_STATUS_COINBASE_NOT_IN_BLOCK_CHAIN = 14; -} - -message GetCompletedTransactionsRequest { } + // This transaction has been completed between the parties but has not been broadcast to the base layer network. + TRANSACTION_STATUS_COMPLETED = 0; + // This transaction has been broadcast to the base layer network and is currently in one or more base node mempools. + TRANSACTION_STATUS_BROADCAST = 1; + // This transaction has been mined and included in a block. + TRANSACTION_STATUS_MINED_UNCONFIRMED = 2; + // This transaction was generated as part of importing a spendable UTXO + TRANSACTION_STATUS_IMPORTED = 3; + // This transaction is still being negotiated by the parties + TRANSACTION_STATUS_PENDING = 4; + // This is a created Coinbase Transaction + TRANSACTION_STATUS_COINBASE = 5; + // This transaction is mined and confirmed at the current base node's height + TRANSACTION_STATUS_MINED_CONFIRMED = 6; + // The transaction was rejected by the mempool + TRANSACTION_STATUS_REJECTED = 7; + // This is faux transaction mainly for one-sided transaction outputs or wallet recovery outputs have been found + TRANSACTION_STATUS_ONE_SIDED_UNCONFIRMED = 8; + // All Imported and FauxUnconfirmed transactions will end up with this status when the outputs have been confirmed + TRANSACTION_STATUS_ONE_SIDED_CONFIRMED = 9; + // This transaction is still being queued for sending + TRANSACTION_STATUS_QUEUED = 10; + // The transaction was not found by the wallet its in transaction database + TRANSACTION_STATUS_NOT_FOUND = 11; + // This is Coinbase transaction that is detected from chain + TRANSACTION_STATUS_COINBASE_UNCONFIRMED = 12; + // This is Coinbase transaction that is detected from chain + TRANSACTION_STATUS_COINBASE_CONFIRMED = 13; + // This is Coinbase transaction that is not currently detected as mined + TRANSACTION_STATUS_COINBASE_NOT_IN_BLOCK_CHAIN = 14; +} + +message GetCompletedTransactionsRequest {} message GetCompletedTransactionsResponse { - TransactionInfo transaction = 1; + TransactionInfo transaction = 1; } -message GetBalanceRequest { } +message GetBalanceRequest {} message GetBalanceResponse { - uint64 available_balance = 1; - uint64 pending_incoming_balance = 2; - uint64 pending_outgoing_balance = 3; - uint64 timelocked_balance = 4; + uint64 available_balance = 1; + uint64 pending_incoming_balance = 2; + uint64 pending_outgoing_balance = 3; + uint64 timelocked_balance = 4; } message GetUnspentAmountsResponse { - repeated uint64 amount = 1; + repeated uint64 amount = 1; } message CoinSplitRequest { - uint64 amount_per_split = 1; - uint64 split_count = 2; - uint64 fee_per_gram = 3; - string message = 4; - uint64 lock_height = 5; + uint64 amount_per_split = 1; + uint64 split_count = 2; + uint64 fee_per_gram = 3; + string message = 4; + uint64 lock_height = 5; } message CoinSplitResponse { - uint64 tx_id = 1; + uint64 tx_id = 1; } message ImportUtxosRequest { - repeated UnblindedOutput outputs = 1; + repeated UnblindedOutput outputs = 1; } message ImportUtxosResponse { - repeated uint64 tx_ids = 1; + repeated uint64 tx_ids = 1; } message CreateTemplateRegistrationRequest { - TemplateRegistration template_registration = 1; - uint64 fee_per_gram = 2; + TemplateRegistration template_registration = 1; + uint64 fee_per_gram = 2; } message CreateTemplateRegistrationResponse { - uint64 tx_id = 1; - bytes template_address = 2; + uint64 tx_id = 1; + bytes template_address = 2; } message CancelTransactionRequest { - uint64 tx_id = 1; + uint64 tx_id = 1; } message CancelTransactionResponse { - bool is_success = 1; - string failure_message = 2; + bool is_success = 1; + string failure_message = 2; } message RevalidateRequest{} @@ -299,8 +300,8 @@ message ValidateRequest{} message ValidateResponse{} message SetBaseNodeRequest { - string public_key_hex = 1; - string net_address = 2; + string public_key_hex = 1; + string net_address = 2; } message SetBaseNodeResponse{} @@ -308,12 +309,12 @@ message SetBaseNodeResponse{} message GetConnectivityRequest{} message CheckConnectivityResponse{ - enum OnlineStatus { - Connecting = 0; - Online = 1; - Offline = 2; - } - OnlineStatus status = 1; + enum OnlineStatus { + Connecting = 0; + Online = 1; + Offline = 2; + } + OnlineStatus status = 1; } message TransactionEventRequest{ @@ -321,29 +322,30 @@ message TransactionEventRequest{ } message TransactionEvent { - string event = 1; - string tx_id = 2; - bytes source_address = 3; - bytes dest_address = 4; - string status = 5; - string direction = 6; - uint64 amount = 7; - string message = 8; + string event = 1; + string tx_id = 2; + bytes source_address = 3; + bytes dest_address = 4; + string status = 5; + string direction = 6; + uint64 amount = 7; + string message = 8; + bytes payment_id = 9; } message TransactionEventResponse { - TransactionEvent transaction = 1; + TransactionEvent transaction = 1; } message RegisterValidatorNodeRequest { - bytes validator_node_public_key = 1; - Signature validator_node_signature = 2; - uint64 fee_per_gram = 3; - string message = 4; + bytes validator_node_public_key = 1; + Signature validator_node_signature = 2; + uint64 fee_per_gram = 3; + string message = 4; } message RegisterValidatorNodeResponse { - uint64 transaction_id = 1; - bool is_success = 2; - string failure_message = 3; + uint64 transaction_id = 1; + bool is_success = 2; + string failure_message = 3; } diff --git a/applications/minotari_console_wallet/src/automation/commands.rs b/applications/minotari_console_wallet/src/automation/commands.rs index 3e96f3abe8..332802a62c 100644 --- a/applications/minotari_console_wallet/src/automation/commands.rs +++ b/applications/minotari_console_wallet/src/automation/commands.rs @@ -2163,6 +2163,35 @@ pub async fn command_runner( Err(e) => eprintln!("GetBalance error! {}", e), } }, + ExportViewKeyAndSpendKey(args) => { + let view_key = wallet.key_manager_service.get_view_key().await?; + let spend_key = wallet.key_manager_service.get_spend_key().await?; + let view_key_hex = view_key.pub_key.to_hex(); + let private_view_key_hex = wallet.key_manager_service.get_private_view_key().await?.to_hex(); + let spend_key_hex = spend_key.pub_key.to_hex(); + let output_file = args.output_file; + #[derive(Serialize)] + struct ViewKeyFile { + view_key: String, + public_view_key: String, + spend_key: String, + } + let view_key_file = ViewKeyFile { + view_key: private_view_key_hex.clone(), + public_view_key: view_key_hex.clone(), + spend_key: spend_key_hex.clone(), + }; + let view_key_file_json = + serde_json::to_string(&view_key_file).map_err(|e| CommandError::JsonFile(e.to_string()))?; + if let Some(file) = output_file { + let file = File::create(file).map_err(|e| CommandError::JsonFile(e.to_string()))?; + let mut file = LineWriter::new(file); + writeln!(file, "{}", view_key_file_json).map_err(|e| CommandError::JsonFile(e.to_string()))?; + } else { + println!("View key: {}", private_view_key_hex); + println!("Spend key: {}", spend_key_hex); + } + }, } } diff --git a/applications/minotari_console_wallet/src/cli.rs b/applications/minotari_console_wallet/src/cli.rs index 59dc54c21d..d0531e1e19 100644 --- a/applications/minotari_console_wallet/src/cli.rs +++ b/applications/minotari_console_wallet/src/cli.rs @@ -88,6 +88,11 @@ pub struct Cli { pub command2: Option, #[clap(long, alias = "profile")] pub profile_with_tokio_console: bool, + // For read only wallets + #[clap(long)] + pub view_private_key: Option, + #[clap(long)] + pub spend_key: Option, } impl ConfigOverrideProvider for Cli { @@ -145,6 +150,7 @@ pub enum CliCommands { RegisterValidatorNode(RegisterValidatorNodeArgs), CreateTlsCerts, Sync(SyncArgs), + ExportViewKeyAndSpendKey(ExportViewKeyAndSpendKeyArgs), } #[derive(Debug, Args, Clone)] @@ -335,6 +341,12 @@ pub struct ExportTxArgs { pub output_file: Option, } +#[derive(Debug, Args, Clone)] +pub struct ExportViewKeyAndSpendKeyArgs { + #[clap(short, long)] + pub output_file: Option, +} + #[derive(Debug, Args, Clone)] pub struct ImportTxArgs { #[clap(short, long)] diff --git a/applications/minotari_console_wallet/src/grpc/mod.rs b/applications/minotari_console_wallet/src/grpc/mod.rs index b56951b623..fa2c761687 100644 --- a/applications/minotari_console_wallet/src/grpc/mod.rs +++ b/applications/minotari_console_wallet/src/grpc/mod.rs @@ -29,6 +29,7 @@ pub fn convert_to_transaction_event(event: String, source: TransactionWrapper) - direction: completed.direction.to_string(), amount: completed.amount.as_u64(), message: completed.message.to_string(), + payment_id: completed.payment_id.map(|id| id.to_bytes()).unwrap_or_default(), }, TransactionWrapper::Outbound(outbound) => TransactionEvent { event, @@ -39,6 +40,7 @@ pub fn convert_to_transaction_event(event: String, source: TransactionWrapper) - direction: "outbound".to_string(), amount: outbound.amount.as_u64(), message: outbound.message, + payment_id: vec![], }, TransactionWrapper::Inbound(inbound) => TransactionEvent { event, @@ -49,6 +51,7 @@ pub fn convert_to_transaction_event(event: String, source: TransactionWrapper) - direction: "inbound".to_string(), amount: inbound.amount.as_u64(), message: inbound.message.clone(), + payment_id: vec![], }, } } diff --git a/applications/minotari_console_wallet/src/grpc/wallet_grpc_server.rs b/applications/minotari_console_wallet/src/grpc/wallet_grpc_server.rs index bb03fbff7e..a39ee4cbd0 100644 --- a/applications/minotari_console_wallet/src/grpc/wallet_grpc_server.rs +++ b/applications/minotari_console_wallet/src/grpc/wallet_grpc_server.rs @@ -784,6 +784,7 @@ impl wallet_server::Wallet for WalletGrpcServer { .get_signature() .to_vec(), message: txn.message.clone(), + payment_id: txn.payment_id.as_ref().map(|id| id.to_bytes()).unwrap_or_default(), }), }; match sender.send(Ok(response)).await { @@ -1100,6 +1101,7 @@ fn simple_event(event: &str) -> TransactionEvent { direction: event.to_string(), amount: 0, message: String::default(), + payment_id: vec![], } } @@ -1121,6 +1123,7 @@ fn convert_wallet_transaction_into_transaction_info( excess_sig: Default::default(), timestamp: tx.timestamp.timestamp() as u64, message: tx.message, + payment_id: vec![], }, PendingOutbound(tx) => TransactionInfo { tx_id: tx.tx_id.into(), @@ -1134,6 +1137,7 @@ fn convert_wallet_transaction_into_transaction_info( excess_sig: Default::default(), timestamp: tx.timestamp.timestamp() as u64, message: tx.message, + payment_id: vec![], }, Completed(tx) => TransactionInfo { tx_id: tx.tx_id.into(), @@ -1151,6 +1155,7 @@ fn convert_wallet_transaction_into_transaction_info( .map(|s| s.get_signature().to_vec()) .unwrap_or_default(), message: tx.message, + payment_id: tx.payment_id.map(|id| id.to_bytes()).unwrap_or_default(), }, } } diff --git a/applications/minotari_console_wallet/src/init/mod.rs b/applications/minotari_console_wallet/src/init/mod.rs index 2fe925622e..f271b471cb 100644 --- a/applications/minotari_console_wallet/src/init/mod.rs +++ b/applications/minotari_console_wallet/src/init/mod.rs @@ -24,6 +24,7 @@ use std::{fs, io, path::PathBuf, str::FromStr, sync::Arc, time::Instant}; +use crossterm::terminal::{disable_raw_mode, enable_raw_mode, is_raw_mode_enabled}; use log::*; use minotari_app_utilities::{consts, identity_management::setup_node_identity}; #[cfg(feature = "ledger")] @@ -53,7 +54,7 @@ use tari_common::{ use tari_common_types::{ key_branches::TransactionKeyManagerBranch, types::{PrivateKey, PublicKey}, - wallet_types::{LedgerWallet, WalletType}, + wallet_types::{LedgerWallet, ProvidedKeysWallet, WalletType}, }; use tari_comms::{ multiaddr::Multiaddr, @@ -77,7 +78,7 @@ use tari_key_manager::{ }; use tari_p2p::{peer_seeds::SeedPeer, TransportType}; use tari_shutdown::ShutdownSignal; -use tari_utilities::{hex::Hex, ByteArray, SafePassword}; +use tari_utilities::{encoding::Base58, hex::Hex, ByteArray, SafePassword}; use zxcvbn::zxcvbn; use crate::{ @@ -98,6 +99,7 @@ pub enum WalletBoot { New, Existing, Recovery, + ViewAndSpendKey, } /// Get and confirm a passphrase from the user, with feedback @@ -758,6 +760,10 @@ fn boot(cli: &Cli, wallet_config: &WalletConfig) -> Result Result> "); match readline { Ok(line) => { @@ -793,6 +800,9 @@ fn boot(cli: &Cli, wallet_config: &WalletConfig) -> Result { + return Ok(WalletBoot::ViewAndSpendKey); + }, _ => continue, } }, @@ -833,6 +843,10 @@ pub(crate) fn boot_with_password( debug!(target: LOG_TARGET, "Prompting for passphrase for existing wallet."); prompt_password("Enter wallet passphrase: ")? }, + WalletBoot::ViewAndSpendKey => { + debug!(target: LOG_TARGET, "Prompting for passphrase for view key wallet."); + get_new_passphrase("Create wallet passphrase: ", "Confirm wallet passphrase: ")? + }, }; Ok((boot_mode, password)) @@ -842,12 +856,44 @@ pub fn prompt_wallet_type( boot_mode: WalletBoot, wallet_config: &WalletConfig, non_interactive: bool, + view_private_key: Option, + spend_key: Option, ) -> Option { - if non_interactive { + if non_interactive && !matches!(boot_mode, WalletBoot::ViewAndSpendKey) { return Some(WalletType::default()); } match boot_mode { + WalletBoot::ViewAndSpendKey => { + let view_key = if let Some(vk) = view_private_key { + match PrivateKey::from_hex(&vk) { + Ok(pk) => pk, + Err(_) => { + println!("Invalid view key provided"); + panic!("Invalid view key provided"); + }, + } + } else { + prompt_private_key("Enter view key: ").expect("View key provided was invalid") + }; + let spend_key = if let Some(sk) = spend_key { + match PublicKey::from_hex(&sk) { + Ok(pk) => pk, + Err(_) => { + println!("Invalid spend key provided"); + panic!("Invalid spend key provided"); + }, + } + } else { + prompt_public_key("Enter spend key: ").expect("Spend key provided was invalid") + }; + + Some(WalletType::ProvidedKeys(ProvidedKeysWallet { + view_key, + public_spend_key: spend_key, + private_spend_key: None, + })) + }, WalletBoot::New | WalletBoot::Recovery => { #[cfg(not(feature = "ledger"))] return Some(WalletType::default()); @@ -905,6 +951,46 @@ pub fn prompt_ledger_account(boot_mode: WalletBoot) -> Option { } } +pub fn prompt_private_key(prompt: &str) -> Option { + // see what we type, as we type it + let must_re_enable_raw_mode = is_raw_mode_enabled().expect("Could not determine raw mode status"); + disable_raw_mode().expect("Could not disable raw mode"); + + println!("{} (hex)", prompt); + let mut input = "".to_string(); + io::stdin().read_line(&mut input).unwrap(); + let input = input.trim(); + if must_re_enable_raw_mode { + enable_raw_mode().expect("Could not enable raw mode"); + } + match PrivateKey::from_canonical_bytes(&Vec::::from_hex(input).expect("Bad hex data")) { + Ok(pk) => Some(pk), + Err(e) => { + panic!("Bad private key: {}", e) + }, + } +} + +pub fn prompt_public_key(prompt: &str) -> Option { + // see what we type, as we type it + let must_re_enable_raw_mode = is_raw_mode_enabled().expect("Could not determine raw mode status"); + disable_raw_mode().expect("Could not disable raw mode"); + println!("{} (hex or base58)", prompt); + let mut input = "".to_string(); + io::stdin().read_line(&mut input).unwrap(); + if must_re_enable_raw_mode { + enable_raw_mode().expect("Could not enable raw mode"); + } + let input = input.trim(); + match PublicKey::from_hex(input) { + Ok(pk) => Some(pk), + Err(_) => match PublicKey::from_base58(input) { + Ok(pk) => Some(pk), + Err(_) => None, + }, + } +} + #[cfg(test)] mod test { use tari_utilities::SafePassword; diff --git a/applications/minotari_console_wallet/src/lib.rs b/applications/minotari_console_wallet/src/lib.rs index 1b066c8a76..a2c97bf07e 100644 --- a/applications/minotari_console_wallet/src/lib.rs +++ b/applications/minotari_console_wallet/src/lib.rs @@ -98,6 +98,8 @@ pub fn run_wallet(shutdown: &mut Shutdown, runtime: Runtime, config: &mut Applic grpc_address: None, command2: None, profile_with_tokio_console: false, + view_private_key: None, + spend_key: None, }; run_wallet_with_cli(shutdown, runtime, config, cli) @@ -128,7 +130,13 @@ pub fn run_wallet_with_cli( let recovery_seed = get_recovery_seed(boot_mode, &cli)?; - let wallet_type = prompt_wallet_type(boot_mode, &config.wallet, cli.non_interactive_mode); + let wallet_type = prompt_wallet_type( + boot_mode, + &config.wallet, + cli.non_interactive_mode, + cli.view_private_key.clone(), + cli.spend_key.clone(), + ); // get command line password if provided let seed_words_file_name = cli.seed_words_file_name.clone(); diff --git a/applications/minotari_console_wallet/src/wallet_modes.rs b/applications/minotari_console_wallet/src/wallet_modes.rs index 565eaa5c24..adf6b6e345 100644 --- a/applications/minotari_console_wallet/src/wallet_modes.rs +++ b/applications/minotari_console_wallet/src/wallet_modes.rs @@ -635,6 +635,7 @@ mod test { CliCommands::CreateTlsCerts => {}, CliCommands::PreMineSpendBackupUtxo(_) => {}, CliCommands::Sync(_) => {}, + CliCommands::ExportViewKeyAndSpendKey(_) => {}, } } assert!( diff --git a/base_layer/core/src/transactions/transaction_components/encrypted_data.rs b/base_layer/core/src/transactions/transaction_components/encrypted_data.rs index 183ecc0f4b..aa107aef68 100644 --- a/base_layer/core/src/transactions/transaction_components/encrypted_data.rs +++ b/base_layer/core/src/transactions/transaction_components/encrypted_data.rs @@ -98,7 +98,7 @@ impl PaymentId { } } - pub fn as_bytes(&self) -> Vec { + pub fn to_bytes(&self) -> Vec { match self { PaymentId::Empty => Vec::new(), PaymentId::U64(v) => (*v).to_le_bytes().to_vec(), @@ -166,7 +166,7 @@ impl EncryptedData { let mut bytes = Zeroizing::new(vec![0; SIZE_VALUE + SIZE_MASK + payment_id.get_size()]); bytes[..SIZE_VALUE].clone_from_slice(value.as_u64().to_le_bytes().as_ref()); bytes[SIZE_VALUE..SIZE_VALUE + SIZE_MASK].clone_from_slice(mask.as_bytes()); - bytes[SIZE_VALUE + SIZE_MASK..].clone_from_slice(&payment_id.as_bytes()); + bytes[SIZE_VALUE + SIZE_MASK..].clone_from_slice(&payment_id.to_bytes()); // Produce a secure random nonce let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng); diff --git a/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs b/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs index 9e4a7d28a7..8b2f37ba27 100644 --- a/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs +++ b/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs @@ -1965,7 +1965,7 @@ impl CompletedTransactionSql { let transaction_bytes = bincode::serialize(&c.transaction).map_err(|e| TransactionStorageError::BincodeSerialize(e.to_string()))?; let payment_id = match c.payment_id { - Some(id) => Some(id.as_bytes()), + Some(id) => Some(id.to_bytes()), None => Some(Vec::new()), }; let output = Self { diff --git a/base_layer/wallet_ffi/src/lib.rs b/base_layer/wallet_ffi/src/lib.rs index 7832de9ab5..e07cf9c0d4 100644 --- a/base_layer/wallet_ffi/src/lib.rs +++ b/base_layer/wallet_ffi/src/lib.rs @@ -340,7 +340,7 @@ impl From for TariUtxo { .expect("failed to obtain hex from a commitment") .into_raw(), payment_id: CString::new( - String::from_utf8(x.payment_id.as_bytes()).unwrap_or_else(|_| "Invalid".to_string()), + String::from_utf8(x.payment_id.to_bytes()).unwrap_or_else(|_| "Invalid".to_string()), ) .expect("failed to obtain string from a payment id") .into_raw(), diff --git a/integration_tests/src/wallet_process.rs b/integration_tests/src/wallet_process.rs index f79d3ab5b4..bc4fb22592 100644 --- a/integration_tests/src/wallet_process.rs +++ b/integration_tests/src/wallet_process.rs @@ -230,6 +230,8 @@ pub fn get_default_cli() -> Cli { grpc_address: None, command2: None, profile_with_tokio_console: false, + view_private_key: None, + spend_key: None, } }