use crate::{
base_node_service::{config::BaseNodeServiceConfig, handle::BaseNodeServiceHandle, BaseNodeServiceInitializer},
contacts_service::{handle::ContactsServiceHandle, storage::database::ContactsBackend, ContactsServiceInitializer},
error::WalletError,
output_manager_service::{
config::OutputManagerServiceConfig,
handle::OutputManagerHandle,
protocols::txo_validation_protocol::{TxoValidationRetry, TxoValidationType},
storage::database::OutputManagerBackend,
OutputManagerServiceInitializer,
TxId,
},
storage::database::{WalletBackend, WalletDatabase},
transaction_service::{
config::TransactionServiceConfig,
handle::TransactionServiceHandle,
storage::database::TransactionBackend,
TransactionServiceInitializer,
},
};
use aes_gcm::{
aead::{generic_array::GenericArray, NewAead},
Aes256Gcm,
};
use digest::Digest;
use log::*;
use std::{marker::PhantomData, sync::Arc};
use tari_comms::{
multiaddr::Multiaddr,
peer_manager::{NodeId, Peer, PeerFeatures, PeerFlags},
types::CommsPublicKey,
CommsNode,
UnspawnedCommsNode,
};
use tari_comms_dht::{store_forward::StoreAndForwardRequester, Dht};
use tari_core::{
consensus::Network,
transactions::{
tari_amount::MicroTari,
transaction::{OutputFeatures, UnblindedOutput},
types::{CryptoFactories, PrivateKey},
},
};
use tari_crypto::{
common::Blake256,
ristretto::{RistrettoPublicKey, RistrettoSchnorr, RistrettoSecretKey},
signatures::{SchnorrSignature, SchnorrSignatureError},
tari_utilities::hex::Hex,
};
use tari_p2p::{
comms_connector::pubsub_connector,
initialization,
initialization::{CommsConfig, P2pInitializer},
};
use tari_service_framework::StackBuilder;
use tari_shutdown::ShutdownSignal;
use tokio::runtime;
const LOG_TARGET: &str = "wallet";
#[derive(Clone)]
pub struct WalletConfig {
pub comms_config: CommsConfig,
pub factories: CryptoFactories,
pub transaction_service_config: Option<TransactionServiceConfig>,
pub output_manager_service_config: Option<OutputManagerServiceConfig>,
pub buffer_size: usize,
pub rate_limit: usize,
pub network: Network,
pub base_node_service_config: Option<BaseNodeServiceConfig>,
}
impl WalletConfig {
#[allow(clippy::too_many_arguments)]
pub fn new(
comms_config: CommsConfig,
factories: CryptoFactories,
transaction_service_config: Option<TransactionServiceConfig>,
output_manager_service_config: Option<OutputManagerServiceConfig>,
network: Network,
base_node_service_config: Option<BaseNodeServiceConfig>,
buffer_size: Option<usize>,
rate_limit: Option<usize>,
) -> Self
{
Self {
comms_config,
factories,
transaction_service_config,
output_manager_service_config,
buffer_size: buffer_size.unwrap_or_else(|| 1500),
rate_limit: rate_limit.unwrap_or_else(|| 50),
network,
base_node_service_config,
}
}
}
#[derive(Clone)]
pub struct Wallet<T, U, V, W>
where
T: WalletBackend + 'static,
U: TransactionBackend + 'static,
V: OutputManagerBackend + 'static,
W: ContactsBackend + 'static,
{
pub comms: CommsNode,
pub dht_service: Dht,
pub store_and_forward_requester: StoreAndForwardRequester,
pub output_manager_service: OutputManagerHandle,
pub transaction_service: TransactionServiceHandle,
pub contacts_service: ContactsServiceHandle,
pub base_node_service: Option<BaseNodeServiceHandle>,
pub db: WalletDatabase<T>,
pub factories: CryptoFactories,
#[cfg(feature = "test_harness")]
pub transaction_backend: U,
_u: PhantomData<U>,
_v: PhantomData<V>,
_w: PhantomData<W>,
}
impl<T, U, V, W> Wallet<T, U, V, W>
where
T: WalletBackend + 'static,
U: TransactionBackend + 'static,
V: OutputManagerBackend + 'static,
W: ContactsBackend + 'static,
{
pub async fn new(
config: WalletConfig,
wallet_backend: T,
transaction_backend: U,
output_manager_backend: V,
contacts_backend: W,
shutdown_signal: ShutdownSignal,
) -> Result<Wallet<T, U, V, W>, WalletError>
{
let db = WalletDatabase::new(wallet_backend);
db.set_comms_secret_key(config.comms_config.node_identity.secret_key().clone())
.await?;
#[cfg(feature = "test_harness")]
let transaction_backend_handle = transaction_backend.clone();
let factories = config.factories;
let (publisher, subscription_factory) =
pubsub_connector(runtime::Handle::current(), config.buffer_size, config.rate_limit);
let peer_message_subscription_factory = Arc::new(subscription_factory);
let transport_type = config.comms_config.transport_type.clone();
let node_identity = config.comms_config.node_identity.clone();
debug!(target: LOG_TARGET, "Wallet Initializing");
let mut stack = StackBuilder::new(shutdown_signal)
.add_initializer(P2pInitializer::new(config.comms_config, publisher))
.add_initializer(OutputManagerServiceInitializer::new(
config.output_manager_service_config.unwrap_or_default(),
peer_message_subscription_factory.clone(),
output_manager_backend,
factories.clone(),
config.network,
))
.add_initializer(TransactionServiceInitializer::new(
config.transaction_service_config.unwrap_or_default(),
peer_message_subscription_factory.clone(),
transaction_backend,
node_identity.clone(),
factories.clone(),
))
.add_initializer(ContactsServiceInitializer::new(contacts_backend));
let mut base_node_service_enabled = false;
if let Some(base_node_service_config) = config.base_node_service_config {
debug!(target: LOG_TARGET, "Base Node Service enabled by config. Initializing.");
stack = stack.add_initializer(BaseNodeServiceInitializer::new(
base_node_service_config,
peer_message_subscription_factory,
));
base_node_service_enabled = true;
} else {
debug!(target: LOG_TARGET, "Wallet Base Node Service is not enabled.");
}
let mut handles = stack.build().await?;
let comms = handles
.take_handle::<UnspawnedCommsNode>()
.expect("P2pInitializer was not added to the stack");
let comms = initialization::spawn_comms_using_transport(comms, transport_type).await?;
let output_manager_handle = handles.expect_handle::<OutputManagerHandle>();
let transaction_service_handle = handles.expect_handle::<TransactionServiceHandle>();
let contacts_handle = handles.expect_handle::<ContactsServiceHandle>();
let dht = handles.expect_handle::<Dht>();
let store_and_forward_requester = dht.store_and_forward_requester();
let base_node_service_handle = if base_node_service_enabled {
Some(handles.expect_handle::<BaseNodeServiceHandle>())
} else {
None
};
Ok(Wallet {
comms,
dht_service: dht,
store_and_forward_requester,
output_manager_service: output_manager_handle,
transaction_service: transaction_service_handle,
contacts_service: contacts_handle,
base_node_service: base_node_service_handle,
db,
factories,
#[cfg(feature = "test_harness")]
transaction_backend: transaction_backend_handle,
_u: PhantomData,
_v: PhantomData,
_w: PhantomData,
})
}
pub async fn wait_until_shutdown(self) {
self.comms.clone().wait_until_shutdown().await;
}
pub async fn set_base_node_peer(
&mut self,
public_key: CommsPublicKey,
net_address: String,
) -> Result<(), WalletError>
{
info!(
"Wallet setting base node peer, public key: {}, net address: {}.",
public_key, net_address
);
let address = net_address.parse::<Multiaddr>()?;
let peer = Peer::new(
public_key.clone(),
NodeId::from_key(&public_key).unwrap(),
vec![address].into(),
PeerFlags::empty(),
PeerFeatures::COMMUNICATION_NODE,
Default::default(),
String::new(),
);
self.comms.peer_manager().add_peer(peer.clone()).await?;
self.comms
.connectivity()
.add_managed_peers(vec![peer.node_id.clone()])
.await?;
self.transaction_service
.set_base_node_public_key(peer.public_key.clone())
.await?;
self.output_manager_service
.set_base_node_public_key(peer.public_key.clone())
.await?;
if let Some(mut base_node_service) = self.base_node_service.clone() {
base_node_service.set_base_node_peer(peer).await?;
}
Ok(())
}
pub async fn import_utxo(
&mut self,
amount: MicroTari,
spending_key: &PrivateKey,
source_public_key: &CommsPublicKey,
message: String,
) -> Result<TxId, WalletError>
{
let unblinded_output = UnblindedOutput::new(amount, spending_key.clone(), None);
self.output_manager_service.add_output(unblinded_output.clone()).await?;
let tx_id = self
.transaction_service
.import_utxo(amount, source_public_key.clone(), message)
.await?;
info!(
target: LOG_TARGET,
"UTXO (Commitment: {}) imported into wallet",
unblinded_output
.as_transaction_input(&self.factories.commitment, OutputFeatures::default())
.commitment
.to_hex()
);
Ok(tx_id)
}
pub fn sign_message(
&mut self,
secret: RistrettoSecretKey,
nonce: RistrettoSecretKey,
message: &str,
) -> Result<SchnorrSignature<RistrettoPublicKey, RistrettoSecretKey>, SchnorrSignatureError>
{
let challenge = Blake256::digest(message.as_bytes());
RistrettoSchnorr::sign(secret, nonce, challenge.clone().as_slice())
}
pub fn verify_message_signature(
&mut self,
public_key: RistrettoPublicKey,
public_nonce: RistrettoPublicKey,
signature: RistrettoSecretKey,
message: String,
) -> bool
{
let signature = RistrettoSchnorr::new(public_nonce, signature);
let challenge = Blake256::digest(message.as_bytes());
signature.verify_challenge(&public_key, challenge.clone().as_slice())
}
pub async fn validate_utxos(&mut self, retries: TxoValidationRetry) -> Result<u64, WalletError> {
self.store_and_forward_requester
.request_saf_messages_from_neighbours()
.await?;
let request_key = self
.output_manager_service
.validate_txos(TxoValidationType::Unspent, retries)
.await?;
Ok(request_key)
}
pub async fn coin_split(
&mut self,
amount_per_split: MicroTari,
split_count: usize,
fee_per_gram: MicroTari,
message: String,
lock_height: Option<u64>,
) -> Result<TxId, WalletError>
{
let coin_split_tx = self
.output_manager_service
.create_coin_split(amount_per_split, split_count, fee_per_gram, lock_height)
.await;
match coin_split_tx {
Ok((tx_id, split_tx, amount, fee)) => {
let coin_tx = self
.transaction_service
.submit_transaction(tx_id, split_tx, fee, amount, message)
.await;
match coin_tx {
Ok(_) => Ok(tx_id),
Err(e) => Err(WalletError::TransactionServiceError(e)),
}
},
Err(e) => Err(WalletError::OutputManagerError(e)),
}
}
pub async fn apply_encryption(&mut self, passphrase: String) -> Result<(), WalletError> {
debug!(target: LOG_TARGET, "Applying wallet encryption.");
let passphrase_hash = Blake256::new().chain(passphrase.as_bytes()).result().to_vec();
let key = GenericArray::from_slice(passphrase_hash.as_slice());
let cipher = Aes256Gcm::new(key);
self.db.apply_encryption(cipher.clone()).await?;
self.output_manager_service.apply_encryption(cipher.clone()).await?;
self.transaction_service.apply_encryption(cipher).await?;
Ok(())
}
pub async fn remove_encryption(&mut self) -> Result<(), WalletError> {
self.db.remove_encryption().await?;
self.output_manager_service.remove_encryption().await?;
self.transaction_service.remove_encryption().await?;
Ok(())
}
}