2025-11-27 22:07:53 +03:00
|
|
|
use std::{path::PathBuf, str::FromStr, sync::Arc};
|
2025-08-07 14:07:34 +03:00
|
|
|
|
2025-08-13 13:42:00 +03:00
|
|
|
use anyhow::Result;
|
2025-11-26 00:27:20 +03:00
|
|
|
use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
|
2025-11-27 22:07:53 +03:00
|
|
|
use common::{
|
|
|
|
|
block::HashableBlockData, sequencer_client::SequencerClient, transaction::NSSATransaction,
|
|
|
|
|
};
|
|
|
|
|
use key_protocol::{
|
|
|
|
|
key_management::key_tree::traits::KeyNode as _, key_protocol_core::NSSAUserData,
|
|
|
|
|
};
|
|
|
|
|
use nssa::{Account, privacy_preserving_transaction::message::EncryptedAccountData};
|
2025-11-26 00:27:20 +03:00
|
|
|
use nssa_core::account::Nonce;
|
|
|
|
|
use rand::{RngCore, rngs::OsRng};
|
2025-09-05 22:50:10 -03:00
|
|
|
use serde::Serialize;
|
2025-11-26 00:27:20 +03:00
|
|
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
2025-08-07 14:07:34 +03:00
|
|
|
|
2025-08-19 14:14:09 +03:00
|
|
|
use crate::{
|
2025-11-27 22:07:53 +03:00
|
|
|
HOME_DIR_ENV_VAR, WalletCore,
|
2025-09-11 18:32:46 +03:00
|
|
|
config::{
|
2025-11-11 12:15:20 +02:00
|
|
|
InitialAccountData, InitialAccountDataPrivate, InitialAccountDataPublic,
|
2025-10-28 16:02:30 +02:00
|
|
|
PersistentAccountDataPrivate, PersistentAccountDataPublic, PersistentStorage, WalletConfig,
|
2025-09-11 18:32:46 +03:00
|
|
|
},
|
2025-08-19 14:14:09 +03:00
|
|
|
};
|
2025-08-07 14:07:34 +03:00
|
|
|
|
2025-09-05 22:50:10 -03:00
|
|
|
/// Get home dir for wallet. Env var `NSSA_WALLET_HOME_DIR` must be set before execution to succeed.
|
2025-10-30 15:33:58 +02:00
|
|
|
pub fn get_home_nssa_var() -> Result<PathBuf> {
|
2025-08-07 14:07:34 +03:00
|
|
|
Ok(PathBuf::from_str(&std::env::var(HOME_DIR_ENV_VAR)?)?)
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-30 15:33:58 +02:00
|
|
|
/// Get home dir for wallet. Env var `HOME` must be set before execution to succeed.
|
|
|
|
|
pub fn get_home_default_path() -> Result<PathBuf> {
|
|
|
|
|
std::env::home_dir()
|
|
|
|
|
.map(|path| path.join(".nssa").join("wallet"))
|
|
|
|
|
.ok_or(anyhow::anyhow!("Failed to get HOME"))
|
2025-10-29 13:23:07 +02:00
|
|
|
}
|
|
|
|
|
|
2025-10-30 15:33:58 +02:00
|
|
|
/// Get home dir for wallet.
|
|
|
|
|
pub fn get_home() -> Result<PathBuf> {
|
|
|
|
|
if let Ok(home) = get_home_nssa_var() {
|
|
|
|
|
Ok(home)
|
|
|
|
|
} else {
|
|
|
|
|
get_home_default_path()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Fetch config from default home
|
|
|
|
|
pub async fn fetch_config() -> Result<WalletConfig> {
|
|
|
|
|
let config_home = get_home()?;
|
2025-10-29 13:23:07 +02:00
|
|
|
let mut config_needs_setup = false;
|
|
|
|
|
|
|
|
|
|
let config = match tokio::fs::OpenOptions::new()
|
|
|
|
|
.read(true)
|
|
|
|
|
.open(config_home.join("wallet_config.json"))
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
Ok(mut file) => {
|
|
|
|
|
let mut config_contents = vec![];
|
|
|
|
|
file.read_to_end(&mut config_contents).await?;
|
|
|
|
|
|
|
|
|
|
serde_json::from_slice(&config_contents)?
|
|
|
|
|
}
|
|
|
|
|
Err(err) => match err.kind() {
|
|
|
|
|
std::io::ErrorKind::NotFound => {
|
|
|
|
|
config_needs_setup = true;
|
|
|
|
|
|
|
|
|
|
println!("Config not found, setting up default config");
|
|
|
|
|
|
|
|
|
|
WalletConfig::default()
|
|
|
|
|
}
|
|
|
|
|
_ => anyhow::bail!("IO error {err:#?}"),
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if config_needs_setup {
|
|
|
|
|
tokio::fs::create_dir_all(&config_home).await?;
|
|
|
|
|
|
|
|
|
|
println!("Created configs dir at path {config_home:#?}");
|
|
|
|
|
|
|
|
|
|
let mut file = tokio::fs::OpenOptions::new()
|
|
|
|
|
.write(true)
|
|
|
|
|
.create(true)
|
2025-10-30 17:23:50 +02:00
|
|
|
.truncate(true)
|
2025-10-29 13:23:07 +02:00
|
|
|
.open(config_home.join("wallet_config.json"))
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
let default_config_serialized =
|
|
|
|
|
serde_json::to_vec_pretty(&WalletConfig::default()).unwrap();
|
|
|
|
|
|
|
|
|
|
file.write_all(&default_config_serialized).await?;
|
|
|
|
|
|
|
|
|
|
println!("Configs setted up");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(config)
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-30 15:33:58 +02:00
|
|
|
/// Fetch data stored at home
|
2025-08-08 15:22:04 +03:00
|
|
|
///
|
2025-11-26 07:32:35 +02:00
|
|
|
/// File must be created through setup beforehand.
|
2025-10-28 16:02:30 +02:00
|
|
|
pub async fn fetch_persistent_storage() -> Result<PersistentStorage> {
|
2025-08-08 15:22:04 +03:00
|
|
|
let home = get_home()?;
|
2025-10-28 16:02:30 +02:00
|
|
|
let accs_path = home.join("storage.json");
|
|
|
|
|
let mut storage_content = vec![];
|
2025-08-08 15:22:04 +03:00
|
|
|
|
2025-10-16 15:58:35 +03:00
|
|
|
match tokio::fs::File::open(accs_path).await {
|
|
|
|
|
Ok(mut file) => {
|
2025-10-28 16:02:30 +02:00
|
|
|
file.read_to_end(&mut storage_content).await?;
|
|
|
|
|
Ok(serde_json::from_slice(&storage_content)?)
|
2025-08-08 15:22:04 +03:00
|
|
|
}
|
|
|
|
|
Err(err) => match err.kind() {
|
2025-11-11 12:15:20 +02:00
|
|
|
std::io::ErrorKind::NotFound => {
|
|
|
|
|
anyhow::bail!("Not found, please setup roots from config command beforehand");
|
|
|
|
|
}
|
2025-08-08 15:22:04 +03:00
|
|
|
_ => {
|
|
|
|
|
anyhow::bail!("IO error {err:#?}");
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-20 17:16:51 +03:00
|
|
|
|
2025-10-28 16:02:30 +02:00
|
|
|
/// Produces data for storage
|
|
|
|
|
pub fn produce_data_for_storage(
|
|
|
|
|
user_data: &NSSAUserData,
|
|
|
|
|
last_synced_block: u64,
|
|
|
|
|
) -> PersistentStorage {
|
2025-08-20 17:16:51 +03:00
|
|
|
let mut vec_for_storage = vec![];
|
|
|
|
|
|
2025-11-27 13:46:35 +02:00
|
|
|
for (account_id, key) in &user_data.public_key_tree.account_id_map {
|
2025-11-10 16:29:33 +02:00
|
|
|
if let Some(data) = user_data.public_key_tree.key_map.get(key) {
|
2025-11-11 12:15:20 +02:00
|
|
|
vec_for_storage.push(
|
|
|
|
|
PersistentAccountDataPublic {
|
2025-11-27 13:46:35 +02:00
|
|
|
account_id: *account_id,
|
2025-11-11 12:15:20 +02:00
|
|
|
chain_index: key.clone(),
|
|
|
|
|
data: data.clone(),
|
|
|
|
|
}
|
|
|
|
|
.into(),
|
|
|
|
|
);
|
2025-11-10 16:29:33 +02:00
|
|
|
}
|
2025-09-11 18:32:46 +03:00
|
|
|
}
|
|
|
|
|
|
2025-11-27 13:46:35 +02:00
|
|
|
for (account_id, key) in &user_data.private_key_tree.account_id_map {
|
2025-11-10 16:29:33 +02:00
|
|
|
if let Some(data) = user_data.private_key_tree.key_map.get(key) {
|
2025-11-11 12:15:20 +02:00
|
|
|
vec_for_storage.push(
|
|
|
|
|
PersistentAccountDataPrivate {
|
2025-11-27 13:46:35 +02:00
|
|
|
account_id: *account_id,
|
2025-11-11 12:15:20 +02:00
|
|
|
chain_index: key.clone(),
|
|
|
|
|
data: data.clone(),
|
|
|
|
|
}
|
|
|
|
|
.into(),
|
|
|
|
|
);
|
2025-11-10 16:29:33 +02:00
|
|
|
}
|
2025-08-20 17:16:51 +03:00
|
|
|
}
|
|
|
|
|
|
2025-11-27 13:46:35 +02:00
|
|
|
for (account_id, key) in &user_data.default_pub_account_signing_keys {
|
2025-11-11 12:15:20 +02:00
|
|
|
vec_for_storage.push(
|
|
|
|
|
InitialAccountData::Public(InitialAccountDataPublic {
|
2025-11-27 13:46:35 +02:00
|
|
|
account_id: account_id.to_string(),
|
2025-11-11 12:15:20 +02:00
|
|
|
pub_sign_key: key.clone(),
|
|
|
|
|
})
|
|
|
|
|
.into(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-27 13:46:35 +02:00
|
|
|
for (account_id, (key_chain, account)) in &user_data.default_user_private_accounts {
|
2025-11-11 12:15:20 +02:00
|
|
|
vec_for_storage.push(
|
|
|
|
|
InitialAccountData::Private(InitialAccountDataPrivate {
|
2025-11-27 13:46:35 +02:00
|
|
|
account_id: account_id.to_string(),
|
2025-11-11 12:15:20 +02:00
|
|
|
account: account.clone(),
|
|
|
|
|
key_chain: key_chain.clone(),
|
|
|
|
|
})
|
|
|
|
|
.into(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-28 16:02:30 +02:00
|
|
|
PersistentStorage {
|
|
|
|
|
accounts: vec_for_storage,
|
|
|
|
|
last_synced_block,
|
|
|
|
|
}
|
2025-08-20 17:16:51 +03:00
|
|
|
}
|
2025-08-26 09:16:46 +03:00
|
|
|
|
2025-10-03 01:30:40 -03:00
|
|
|
pub(crate) fn produce_random_nonces(size: usize) -> Vec<Nonce> {
|
|
|
|
|
let mut result = vec![[0; 16]; size];
|
|
|
|
|
result.iter_mut().for_each(|bytes| OsRng.fill_bytes(bytes));
|
2025-10-03 08:08:54 -03:00
|
|
|
result.into_iter().map(Nonce::from_le_bytes).collect()
|
2025-10-03 01:30:40 -03:00
|
|
|
}
|
|
|
|
|
|
2025-10-24 15:26:30 +03:00
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
2025-11-24 17:09:30 +03:00
|
|
|
pub enum AccountPrivacyKind {
|
2025-10-24 15:26:30 +03:00
|
|
|
Public,
|
|
|
|
|
Private,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) fn parse_addr_with_privacy_prefix(
|
2025-11-24 17:09:30 +03:00
|
|
|
account_base58: &str,
|
|
|
|
|
) -> Result<(String, AccountPrivacyKind)> {
|
|
|
|
|
if account_base58.starts_with("Public/") {
|
2025-10-24 15:26:30 +03:00
|
|
|
Ok((
|
2025-11-24 17:09:30 +03:00
|
|
|
account_base58.strip_prefix("Public/").unwrap().to_string(),
|
|
|
|
|
AccountPrivacyKind::Public,
|
2025-10-24 15:26:30 +03:00
|
|
|
))
|
2025-11-24 17:09:30 +03:00
|
|
|
} else if account_base58.starts_with("Private/") {
|
2025-10-24 15:26:30 +03:00
|
|
|
Ok((
|
2025-11-24 17:09:30 +03:00
|
|
|
account_base58.strip_prefix("Private/").unwrap().to_string(),
|
|
|
|
|
AccountPrivacyKind::Private,
|
2025-10-24 15:26:30 +03:00
|
|
|
))
|
|
|
|
|
} else {
|
|
|
|
|
anyhow::bail!("Unsupported privacy kind, available variants is Public/ and Private/");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-05 22:50:10 -03:00
|
|
|
/// Human-readable representation of an account.
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
|
pub(crate) struct HumanReadableAccount {
|
|
|
|
|
balance: u128,
|
|
|
|
|
program_owner_b64: String,
|
|
|
|
|
data_b64: String,
|
|
|
|
|
nonce: u128,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<Account> for HumanReadableAccount {
|
|
|
|
|
fn from(account: Account) -> Self {
|
|
|
|
|
let program_owner_b64 = BASE64.encode(bytemuck::cast_slice(&account.program_owner));
|
|
|
|
|
let data_b64 = BASE64.encode(account.data);
|
|
|
|
|
Self {
|
|
|
|
|
balance: account.balance,
|
|
|
|
|
program_owner_b64,
|
|
|
|
|
data_b64,
|
|
|
|
|
nonce: account.nonce,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-27 22:07:53 +03:00
|
|
|
pub async fn parse_block_range(
|
|
|
|
|
start: u64,
|
|
|
|
|
stop: u64,
|
|
|
|
|
seq_client: Arc<SequencerClient>,
|
|
|
|
|
wallet_core: &mut WalletCore,
|
|
|
|
|
) -> Result<()> {
|
|
|
|
|
for block_id in start..(stop + 1) {
|
|
|
|
|
let block =
|
|
|
|
|
borsh::from_slice::<HashableBlockData>(&seq_client.get_block(block_id).await?.block)?;
|
|
|
|
|
|
|
|
|
|
for tx in block.transactions {
|
|
|
|
|
let nssa_tx = NSSATransaction::try_from(&tx)?;
|
|
|
|
|
|
|
|
|
|
if let NSSATransaction::PrivacyPreserving(tx) = nssa_tx {
|
|
|
|
|
let mut affected_accounts = vec![];
|
|
|
|
|
|
|
|
|
|
for (acc_account_id, (key_chain, _)) in
|
|
|
|
|
&wallet_core.storage.user_data.default_user_private_accounts
|
|
|
|
|
{
|
|
|
|
|
let view_tag = EncryptedAccountData::compute_view_tag(
|
|
|
|
|
key_chain.nullifer_public_key.clone(),
|
|
|
|
|
key_chain.incoming_viewing_public_key.clone(),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
for (ciph_id, encrypted_data) in tx
|
|
|
|
|
.message()
|
|
|
|
|
.encrypted_private_post_states
|
|
|
|
|
.iter()
|
|
|
|
|
.enumerate()
|
|
|
|
|
{
|
|
|
|
|
if encrypted_data.view_tag == view_tag {
|
|
|
|
|
let ciphertext = &encrypted_data.ciphertext;
|
|
|
|
|
let commitment = &tx.message.new_commitments[ciph_id];
|
|
|
|
|
let shared_secret = key_chain
|
|
|
|
|
.calculate_shared_secret_receiver(encrypted_data.epk.clone());
|
|
|
|
|
|
|
|
|
|
let res_acc = nssa_core::EncryptionScheme::decrypt(
|
|
|
|
|
ciphertext,
|
|
|
|
|
&shared_secret,
|
|
|
|
|
commitment,
|
|
|
|
|
ciph_id as u32,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if let Some(res_acc) = res_acc {
|
|
|
|
|
println!(
|
|
|
|
|
"Received new account for account_id {acc_account_id:#?} with account object {res_acc:#?}"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
affected_accounts.push((*acc_account_id, res_acc));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for keys_node in wallet_core
|
|
|
|
|
.storage
|
|
|
|
|
.user_data
|
|
|
|
|
.private_key_tree
|
|
|
|
|
.key_map
|
|
|
|
|
.values()
|
|
|
|
|
{
|
|
|
|
|
let acc_account_id = keys_node.account_id();
|
|
|
|
|
let key_chain = &keys_node.value.0;
|
|
|
|
|
|
|
|
|
|
let view_tag = EncryptedAccountData::compute_view_tag(
|
|
|
|
|
key_chain.nullifer_public_key.clone(),
|
|
|
|
|
key_chain.incoming_viewing_public_key.clone(),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
for (ciph_id, encrypted_data) in tx
|
|
|
|
|
.message()
|
|
|
|
|
.encrypted_private_post_states
|
|
|
|
|
.iter()
|
|
|
|
|
.enumerate()
|
|
|
|
|
{
|
|
|
|
|
if encrypted_data.view_tag == view_tag {
|
|
|
|
|
let ciphertext = &encrypted_data.ciphertext;
|
|
|
|
|
let commitment = &tx.message.new_commitments[ciph_id];
|
|
|
|
|
let shared_secret = key_chain
|
|
|
|
|
.calculate_shared_secret_receiver(encrypted_data.epk.clone());
|
|
|
|
|
|
|
|
|
|
let res_acc = nssa_core::EncryptionScheme::decrypt(
|
|
|
|
|
ciphertext,
|
|
|
|
|
&shared_secret,
|
|
|
|
|
commitment,
|
|
|
|
|
ciph_id as u32,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if let Some(res_acc) = res_acc {
|
|
|
|
|
println!(
|
|
|
|
|
"Received new account for account_id {acc_account_id:#?} with account object {res_acc:#?}"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
affected_accounts.push((acc_account_id, res_acc));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (affected_account_id, new_acc) in affected_accounts {
|
|
|
|
|
wallet_core
|
|
|
|
|
.storage
|
|
|
|
|
.insert_private_account_data(affected_account_id, new_acc);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
wallet_core.last_synced_block = block_id;
|
|
|
|
|
wallet_core.store_persistent_data().await?;
|
|
|
|
|
|
|
|
|
|
println!(
|
|
|
|
|
"Block at id {block_id} with timestamp {} parsed",
|
|
|
|
|
block.timestamp
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-26 09:16:46 +03:00
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_get_home_get_env_var() {
|
2025-09-04 14:38:41 +03:00
|
|
|
unsafe {
|
|
|
|
|
std::env::set_var(HOME_DIR_ENV_VAR, "/path/to/configs");
|
|
|
|
|
}
|
2025-08-26 09:16:46 +03:00
|
|
|
|
|
|
|
|
let home = get_home().unwrap();
|
|
|
|
|
|
|
|
|
|
assert_eq!(PathBuf::from_str("/path/to/configs").unwrap(), home);
|
|
|
|
|
|
2025-09-04 14:38:41 +03:00
|
|
|
unsafe {
|
|
|
|
|
std::env::remove_var(HOME_DIR_ENV_VAR);
|
|
|
|
|
}
|
2025-08-26 09:16:46 +03:00
|
|
|
}
|
2025-10-24 15:26:30 +03:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_addr_parse_with_privacy() {
|
|
|
|
|
let addr_base58 = "Public/BLgCRDXYdQPMMWVHYRFGQZbgeHx9frkipa8GtpG2Syqy";
|
|
|
|
|
let (_, addr_kind) = parse_addr_with_privacy_prefix(addr_base58).unwrap();
|
|
|
|
|
|
2025-11-24 17:09:30 +03:00
|
|
|
assert_eq!(addr_kind, AccountPrivacyKind::Public);
|
2025-10-24 15:26:30 +03:00
|
|
|
|
|
|
|
|
let addr_base58 = "Private/BLgCRDXYdQPMMWVHYRFGQZbgeHx9frkipa8GtpG2Syqy";
|
|
|
|
|
let (_, addr_kind) = parse_addr_with_privacy_prefix(addr_base58).unwrap();
|
|
|
|
|
|
2025-11-24 17:09:30 +03:00
|
|
|
assert_eq!(addr_kind, AccountPrivacyKind::Private);
|
2025-10-24 15:26:30 +03:00
|
|
|
|
|
|
|
|
let addr_base58 = "asdsada/BLgCRDXYdQPMMWVHYRFGQZbgeHx9frkipa8GtpG2Syqy";
|
|
|
|
|
assert!(parse_addr_with_privacy_prefix(addr_base58).is_err());
|
|
|
|
|
}
|
2025-08-26 09:16:46 +03:00
|
|
|
}
|