lssa/wallet/src/lib.rs

804 lines
27 KiB
Rust
Raw Normal View History

2026-03-04 18:42:33 +03:00
#![expect(
clippy::print_stdout,
reason = "This is a CLI application, printing to stdout and stderr is expected and convenient"
)]
#![expect(
clippy::shadow_unrelated,
reason = "Most of the shadows come from args parsing which is ok"
)]
use std::path::PathBuf;
2024-12-05 13:05:58 +02:00
2026-03-04 18:42:33 +03:00
use anyhow::{Context as _, Result};
fix(wallet): use cryptographically secure entropy for mnemonic generation The mnemonic/wallet generation was using a constant zero-byte array for entropy ([0u8; 32]), making all wallets deterministic based solely on the password. This commit introduces proper random entropy using OsRng and enables users to back up their recovery phrase. Changes: - SeedHolder::new_mnemonic() now uses OsRng for 256-bit random entropy and returns the generated mnemonic - Added SeedHolder::from_mnemonic() to recover a wallet from an existing mnemonic phrase - WalletChainStore::new_storage() returns the mnemonic for user backup - Added WalletChainStore::restore_storage() for recovery from a mnemonic - WalletCore::new_init_storage() now returns the mnemonic - Renamed reset_storage to restore_storage, which accepts a mnemonic for recovery - CLI displays the recovery phrase when a new wallet is created - RestoreKeys command now prompts for the mnemonic phrase via read_mnemonic_from_stdin() Note: The password parameter is retained for future storage encryption but is no longer used in seed derivation (empty passphrase is used instead). This means the mnemonic alone is sufficient to recover accounts. Usage: On first wallet initialization, users will see: IMPORTANT: Write down your recovery phrase and store it securely. This is the only way to recover your wallet if you lose access. Recovery phrase: word1 word2 word3 ... word24 To restore keys: wallet restore-keys --depth 5 Input recovery phrase: <24 words> Input password: <password>
2026-01-20 16:47:34 +01:00
use bip39::Mnemonic;
use common::{HashType, transaction::NSSATransaction};
2025-08-11 08:55:08 +03:00
use config::WalletConfig;
2026-04-15 23:34:49 -03:00
use key_protocol::key_management::key_tree::chain_index::ChainIndex;
2025-08-06 14:56:58 +03:00
use log::info;
use nssa::{
2025-12-03 00:17:12 +03:00
Account, AccountId, PrivacyPreservingTransaction,
privacy_preserving_transaction::{
circuit::ProgramWithDependencies, message::EncryptedAccountData,
},
};
use nssa_core::{
Commitment, MembershipProof, SharedSecretKey, account::Nonce, program::InstructionData,
};
pub use privacy_preserving_tx::PrivacyPreservingAccount;
use sequencer_service_rpc::{RpcClient as _, SequencerClient, SequencerClientBuilder};
use storage::Storage;
2026-03-04 18:42:33 +03:00
use tokio::io::AsyncWriteExt as _;
2025-08-06 14:56:58 +03:00
2025-08-21 15:58:31 +03:00
use crate::{
account::{AccountIdWithPrivacy, Label},
config::WalletConfigOverrides,
2025-08-21 15:58:31 +03:00
poller::TxPoller,
storage::key_chain::SharedAccountEntry,
2025-08-08 15:22:04 +03:00
};
2025-08-07 14:07:34 +03:00
pub mod account;
2025-10-13 17:25:36 +03:00
pub mod cli;
2024-12-03 09:32:35 +02:00
pub mod config;
2025-08-07 14:07:34 +03:00
pub mod helperfunctions;
2025-08-21 15:58:31 +03:00
pub mod poller;
mod privacy_preserving_tx;
pub mod program_facades;
pub mod storage;
2024-12-22 16:14:52 +02:00
2026-03-04 18:42:33 +03:00
pub const HOME_DIR_ENV_VAR: &str = "NSSA_WALLET_HOME_DIR";
2025-12-18 11:44:38 +02:00
pub enum AccDecodeData {
Skip,
Decode(nssa_core::SharedSecretKey, AccountId),
}
/// Info returned when creating a shared account.
pub struct SharedAccountInfo {
pub account_id: AccountId,
pub npk: nssa_core::NullifierPublicKey,
pub vpk: nssa_core::encryption::ViewingPublicKey,
}
#[derive(Debug, thiserror::Error)]
pub enum ExecutionFailureKind {
#[error("Failed to get data from sequencer")]
SequencerError(#[source] anyhow::Error),
#[error("Inputs amounts does not match outputs")]
AmountMismatchError,
#[error("Accounts key not found")]
KeyNotFoundError,
#[error("Sequencer client error")]
SequencerClientError(#[from] sequencer_service_rpc::ClientError),
#[error("Can not pay for operation")]
InsufficientFundsError,
#[error("Account {0} data is invalid")]
AccountDataError(AccountId),
#[error("Failed to build transaction: {0}")]
TransactionBuildError(#[from] nssa::error::NssaError),
}
2026-03-04 18:42:33 +03:00
#[expect(clippy::partial_pub_fields, reason = "TODO: make all fields private")]
2025-08-11 08:55:08 +03:00
pub struct WalletCore {
config_path: PathBuf,
2026-02-03 19:04:41 -03:00
config_overrides: Option<WalletConfigOverrides>,
config: WalletConfig,
storage: Storage,
storage_path: PathBuf,
poller: TxPoller,
pub sequencer_client: SequencerClient,
2024-12-05 13:05:58 +02:00
}
2025-08-11 08:55:08 +03:00
impl WalletCore {
/// Construct wallet using [`HOME_DIR_ENV_VAR`] env var for paths or user home dir if not set.
pub fn from_env() -> Result<Self> {
let config_path = helperfunctions::fetch_config_path()?;
let storage_path = helperfunctions::fetch_persistent_storage_path()?;
2024-12-05 13:05:58 +02:00
Self::new_update_chain(config_path, storage_path, None)
}
pub fn new_update_chain(
config_path: PathBuf,
storage_path: PathBuf,
config_overrides: Option<WalletConfigOverrides>,
) -> Result<Self> {
let storage = Storage::from_path(&storage_path)
.with_context(|| format!("Failed to load storage from {}", storage_path.display()))?;
Self::new(config_path, storage_path, config_overrides, storage)
2024-12-05 13:05:58 +02:00
}
2024-12-22 16:14:52 +02:00
pub fn new_init_storage(
config_path: PathBuf,
storage_path: PathBuf,
config_overrides: Option<WalletConfigOverrides>,
fix(wallet): use cryptographically secure entropy for mnemonic generation The mnemonic/wallet generation was using a constant zero-byte array for entropy ([0u8; 32]), making all wallets deterministic based solely on the password. This commit introduces proper random entropy using OsRng and enables users to back up their recovery phrase. Changes: - SeedHolder::new_mnemonic() now uses OsRng for 256-bit random entropy and returns the generated mnemonic - Added SeedHolder::from_mnemonic() to recover a wallet from an existing mnemonic phrase - WalletChainStore::new_storage() returns the mnemonic for user backup - Added WalletChainStore::restore_storage() for recovery from a mnemonic - WalletCore::new_init_storage() now returns the mnemonic - Renamed reset_storage to restore_storage, which accepts a mnemonic for recovery - CLI displays the recovery phrase when a new wallet is created - RestoreKeys command now prompts for the mnemonic phrase via read_mnemonic_from_stdin() Note: The password parameter is retained for future storage encryption but is no longer used in seed derivation (empty passphrase is used instead). This means the mnemonic alone is sufficient to recover accounts. Usage: On first wallet initialization, users will see: IMPORTANT: Write down your recovery phrase and store it securely. This is the only way to recover your wallet if you lose access. Recovery phrase: word1 word2 word3 ... word24 To restore keys: wallet restore-keys --depth 5 Input recovery phrase: <24 words> Input password: <password>
2026-01-20 16:47:34 +01:00
password: &str,
) -> Result<(Self, Mnemonic)> {
let (storage, mnemonic) = Storage::new(password).context("Failed to create storage")?;
let wallet = Self::new(config_path, storage_path, config_overrides, storage)?;
Ok((wallet, mnemonic))
}
fn new(
config_path: PathBuf,
storage_path: PathBuf,
config_overrides: Option<WalletConfigOverrides>,
storage: Storage,
) -> Result<Self> {
2026-03-03 23:21:08 +03:00
let mut config =
WalletConfig::from_path_or_initialize_default(&config_path).with_context(|| {
format!(
"Failed to deserialize wallet config at {}",
config_path.display()
)
})?;
2026-02-03 19:04:41 -03:00
if let Some(config_overrides) = config_overrides.clone() {
config.apply_overrides(config_overrides);
}
let sequencer_client = {
let mut builder = SequencerClientBuilder::default();
if let Some(basic_auth) = &config.basic_auth {
builder = builder.set_headers(
2026-03-13 22:56:14 +03:00
std::iter::once((
"Authorization".parse().expect("Header name is valid"),
2026-03-13 22:56:14 +03:00
format!("Basic {basic_auth}")
.parse()
.context("Invalid basic auth format")?,
2026-03-13 22:56:14 +03:00
))
.collect(),
);
}
builder
.build(config.sequencer_addr.clone())
.context("Failed to create sequencer client")?
};
let tx_poller = TxPoller::new(&config, sequencer_client.clone());
2025-11-11 12:15:20 +02:00
Ok(Self {
config_path,
config_overrides,
config,
storage_path,
2025-11-11 12:15:20 +02:00
storage,
poller: tx_poller,
sequencer_client,
2025-11-11 12:15:20 +02:00
})
}
2026-03-10 00:17:43 +03:00
/// Get configuration with applied overrides.
2026-03-03 23:21:08 +03:00
#[must_use]
2026-03-09 18:27:56 +03:00
pub const fn config(&self) -> &WalletConfig {
&self.config
}
pub fn set_config(&mut self, config: WalletConfig) {
self.config = config;
}
2026-03-10 00:17:43 +03:00
/// Get storage.
2026-03-03 23:21:08 +03:00
#[must_use]
pub const fn storage(&self) -> &Storage {
&self.storage
}
2025-08-22 15:58:43 +03:00
/// Get mutable reference to storage.
#[must_use]
pub const fn storage_mut(&mut self) -> &mut Storage {
&mut self.storage
}
fix(wallet): use cryptographically secure entropy for mnemonic generation The mnemonic/wallet generation was using a constant zero-byte array for entropy ([0u8; 32]), making all wallets deterministic based solely on the password. This commit introduces proper random entropy using OsRng and enables users to back up their recovery phrase. Changes: - SeedHolder::new_mnemonic() now uses OsRng for 256-bit random entropy and returns the generated mnemonic - Added SeedHolder::from_mnemonic() to recover a wallet from an existing mnemonic phrase - WalletChainStore::new_storage() returns the mnemonic for user backup - Added WalletChainStore::restore_storage() for recovery from a mnemonic - WalletCore::new_init_storage() now returns the mnemonic - Renamed reset_storage to restore_storage, which accepts a mnemonic for recovery - CLI displays the recovery phrase when a new wallet is created - RestoreKeys command now prompts for the mnemonic phrase via read_mnemonic_from_stdin() Note: The password parameter is retained for future storage encryption but is no longer used in seed derivation (empty passphrase is used instead). This means the mnemonic alone is sufficient to recover accounts. Usage: On first wallet initialization, users will see: IMPORTANT: Write down your recovery phrase and store it securely. This is the only way to recover your wallet if you lose access. Recovery phrase: word1 word2 word3 ... word24 To restore keys: wallet restore-keys --depth 5 Input recovery phrase: <24 words> Input password: <password>
2026-01-20 16:47:34 +01:00
/// Restore storage from an existing mnemonic phrase.
pub fn restore_storage(&mut self, mnemonic: &Mnemonic, password: &str) -> Result<()> {
self.storage.restore(mnemonic, password)
}
2026-03-10 00:17:43 +03:00
/// Store persistent data at home.
pub fn store_persistent_data(&self) -> Result<()> {
self.storage
.save_to_path(&self.storage_path)
.with_context(|| {
format!(
"Failed to store persistent accounts at {}",
self.storage_path.display()
)
})?;
2025-08-20 17:16:51 +03:00
2026-03-03 23:21:08 +03:00
println!(
"Stored persistent accounts at {}",
self.storage_path.display()
);
2025-08-20 17:16:51 +03:00
Ok(())
2025-08-20 17:16:51 +03:00
}
2026-03-10 00:17:43 +03:00
/// Store persistent data at home.
pub async fn store_config_changes(&self) -> Result<()> {
let config = serde_json::to_vec_pretty(&self.config)?;
2025-11-03 15:45:50 +02:00
let mut config_file = tokio::fs::File::create(&self.config_path).await?;
2025-11-03 15:45:50 +02:00
config_file.write_all(&config).await?;
// Ensure data is flushed to disk before returning to prevent race conditions
config_file.sync_all().await?;
2025-11-03 15:45:50 +02:00
2026-03-03 23:21:08 +03:00
info!("Stored data at {}", self.config_path.display());
2025-11-03 15:45:50 +02:00
Ok(())
2025-11-03 15:45:50 +02:00
}
2025-12-03 13:10:07 +02:00
pub fn create_new_account_public(
&mut self,
chain_index: Option<ChainIndex>,
) -> (AccountId, ChainIndex) {
2025-09-08 14:48:58 +03:00
self.storage
.key_chain_mut()
2025-11-10 16:29:33 +02:00
.generate_new_public_transaction_private_key(chain_index)
2024-12-25 09:50:54 +02:00
}
2026-04-19 23:13:51 -03:00
pub fn create_private_accounts_key(&mut self, chain_index: Option<ChainIndex>) -> ChainIndex {
2025-09-11 18:32:46 +03:00
self.storage
.key_chain_mut()
2026-04-17 19:45:30 -03:00
.create_private_accounts_key(chain_index)
2025-09-02 07:32:39 +03:00
}
pub fn create_new_account_private(
&mut self,
chain_index: Option<ChainIndex>,
) -> (AccountId, ChainIndex) {
self.storage
.key_chain_mut()
.generate_new_privacy_preserving_transaction_key_chain(chain_index)
}
/// Insert a group key holder into storage.
pub fn insert_group_key_holder(
&mut self,
name: Label,
holder: key_protocol::key_management::group_key_holder::GroupKeyHolder,
) {
self.storage
.key_chain_mut()
.insert_group_key_holder(name, holder);
}
/// Set the wallet's dedicated sealing secret key.
pub const fn set_sealing_secret_key(&mut self, key: nssa_core::encryption::Scalar) {
self.storage.key_chain_mut().set_sealing_secret_key(key);
}
/// Resolve an `AccountId` to the appropriate `PrivacyPreservingAccount` variant.
/// Checks the key tree first, then shared private accounts.
#[must_use]
pub fn resolve_private_account(
&self,
account_id: nssa::AccountId,
) -> Option<PrivacyPreservingAccount> {
// Check key tree first
if self
.storage
.key_chain()
.private_account(account_id)
.is_some()
{
return Some(PrivacyPreservingAccount::PrivateOwned(account_id));
}
// Check shared private accounts
let entry = self
.storage
.key_chain()
.shared_private_account(account_id)?;
let holder = self
.storage
.key_chain()
.group_key_holder(&entry.group_label)?;
2026-05-11 20:26:03 -03:00
if let (Some(pda_seed), Some(program_id)) = (entry.pda_seed, entry.pda_program_id) {
let keys = holder.derive_keys_for_pda(&program_id, &pda_seed);
Some(PrivacyPreservingAccount::PrivatePdaShared {
account_id,
nsk: keys.nullifier_secret_key,
npk: keys.generate_nullifier_public_key(),
vpk: keys.generate_viewing_public_key(),
identifier: entry.identifier,
})
} else {
let derivation_seed = {
use sha2::Digest as _;
let mut hasher = sha2::Sha256::new();
hasher.update(b"/LEE/v0.3/SharedAccountTag/\x00\x00\x00\x00\x00");
hasher.update(entry.identifier.to_le_bytes());
let result: [u8; 32] = hasher.finalize().into();
result
};
let keys = holder.derive_keys_for_shared_account(&derivation_seed);
Some(PrivacyPreservingAccount::PrivateShared {
nsk: keys.nullifier_secret_key,
npk: keys.generate_nullifier_public_key(),
vpk: keys.generate_viewing_public_key(),
identifier: entry.identifier,
})
}
}
/// Remove a group key holder from storage. Returns the removed holder if it existed.
pub fn remove_group_key_holder(
&mut self,
name: &Label,
) -> Option<key_protocol::key_management::group_key_holder::GroupKeyHolder> {
self.storage.key_chain_mut().remove_group_key_holder(name)
}
/// Register a shared account in storage for sync tracking.
fn register_shared_account(
&mut self,
account_id: AccountId,
group_label: Label,
identifier: nssa_core::Identifier,
pda_seed: Option<nssa_core::program::PdaSeed>,
pda_program_id: Option<nssa_core::program::ProgramId>,
) {
self.storage.key_chain_mut().insert_shared_private_account(
account_id,
SharedAccountEntry {
group_label,
identifier,
pda_seed,
pda_program_id,
account: Account::default(),
},
);
}
/// Create a shared PDA account from a group's GMS. Returns the `AccountId` and derived keys.
pub fn create_shared_pda_account(
&mut self,
group_name: Label,
pda_seed: nssa_core::program::PdaSeed,
program_id: nssa_core::program::ProgramId,
2026-05-12 01:29:24 -03:00
identifier: nssa_core::Identifier,
) -> Result<SharedAccountInfo> {
let holder = self
.storage
.key_chain()
.group_key_holder(&group_name)
.context(format!("Group '{group_name}' not found"))?;
let keys = holder.derive_keys_for_pda(&program_id, &pda_seed);
let npk = keys.generate_nullifier_public_key();
let vpk = keys.generate_viewing_public_key();
2026-05-12 01:29:24 -03:00
let account_id = AccountId::for_private_pda(&program_id, &pda_seed, &npk, identifier);
self.register_shared_account(
account_id,
group_name,
2026-05-12 01:29:24 -03:00
identifier,
Some(pda_seed),
Some(program_id),
);
Ok(SharedAccountInfo {
account_id,
npk,
vpk,
})
}
/// Create a shared regular private account from a group's GMS. Returns the `AccountId` and
/// derived keys. The derivation seed is computed deterministically from a random identifier.
pub fn create_shared_regular_account(
&mut self,
group_name: Label,
) -> Result<SharedAccountInfo> {
let identifier: nssa_core::Identifier = rand::random();
let derivation_seed = {
use sha2::Digest as _;
let mut hasher = sha2::Sha256::new();
hasher.update(b"/LEE/v0.3/SharedAccountTag/\x00\x00\x00\x00\x00");
hasher.update(identifier.to_le_bytes());
let result: [u8; 32] = hasher.finalize().into();
result
};
let holder = self
.storage
.key_chain()
.group_key_holder(&group_name)
.context(format!("Group '{group_name}' not found"))?;
let keys = holder.derive_keys_for_shared_account(&derivation_seed);
let npk = keys.generate_nullifier_public_key();
let vpk = keys.generate_viewing_public_key();
let account_id = AccountId::from((&npk, identifier));
self.register_shared_account(account_id, group_name, identifier, None, None);
Ok(SharedAccountInfo {
account_id,
npk,
vpk,
})
}
2026-03-10 00:17:43 +03:00
/// Get account balance.
pub async fn get_account_balance(&self, acc: AccountId) -> Result<u128> {
Ok(self.sequencer_client.get_account_balance(acc).await?)
2025-09-02 09:01:33 +03:00
}
2026-03-10 00:17:43 +03:00
/// Get accounts nonces.
pub async fn get_accounts_nonces(&self, accs: Vec<AccountId>) -> Result<Vec<Nonce>> {
Ok(self.sequencer_client.get_accounts_nonces(accs).await?)
2025-09-02 09:01:33 +03:00
}
pub async fn get_account(&self, account_id: AccountIdWithPrivacy) -> Result<Account> {
match account_id {
AccountIdWithPrivacy::Public(acc_id) => self.get_account_public(acc_id).await,
AccountIdWithPrivacy::Private(acc_id) => {
if let Some(account) = self.get_account_private(acc_id) {
Ok(account)
} else {
anyhow::bail!("Private account with id {acc_id} not found in storage")
}
}
}
}
/// Get public account.
pub async fn get_account_public(&self, account_id: AccountId) -> Result<Account> {
Ok(self.sequencer_client.get_account(account_id).await?)
}
2026-03-03 23:21:08 +03:00
#[must_use]
pub fn get_account_public_signing_key(
&self,
account_id: AccountId,
) -> Option<&nssa::PrivateKey> {
self.storage.key_chain().pub_account_signing_key(account_id)
}
2026-03-03 23:21:08 +03:00
#[must_use]
pub fn get_account_private(&self, account_id: AccountId) -> Option<Account> {
2025-10-03 15:59:27 -03:00
self.storage
.key_chain()
.private_account(account_id)
.map(|acc| acc.account.clone())
2025-10-03 15:59:27 -03:00
}
2026-03-03 23:21:08 +03:00
#[must_use]
pub fn get_private_account_commitment(&self, account_id: AccountId) -> Option<Commitment> {
2026-05-12 17:17:58 -03:00
let account = self
.storage
.key_chain()
.private_account(account_id)
.map(|acc| acc.account)
2026-05-12 17:17:58 -03:00
.or_else(|| {
self.storage
.key_chain()
.shared_private_account(account_id)
.map(|entry| &entry.account)
2026-05-12 17:17:58 -03:00
})?;
Some(Commitment::new(&account_id, account))
2025-10-03 15:59:27 -03:00
}
2026-03-10 00:17:43 +03:00
/// Poll transactions.
pub async fn poll_native_token_transfer(&self, hash: HashType) -> Result<NSSATransaction> {
self.poller.poll_tx(hash).await
2025-08-21 15:58:31 +03:00
}
2025-10-14 08:33:20 +03:00
2025-10-15 15:25:36 +03:00
pub async fn check_private_account_initialized(
&self,
account_id: AccountId,
2025-10-15 15:25:36 +03:00
) -> Result<Option<MembershipProof>> {
if let Some(acc_comm) = self.get_private_account_commitment(account_id) {
2025-10-15 15:25:36 +03:00
self.sequencer_client
.get_proof_for_commitment(acc_comm)
.await
2026-03-14 03:20:37 +03:00
.map_err(Into::into)
2025-10-14 08:33:20 +03:00
} else {
2025-10-15 15:25:36 +03:00
Ok(None)
2025-10-14 08:33:20 +03:00
}
}
pub fn decode_insert_privacy_preserving_transaction_results(
&mut self,
2026-03-03 23:21:08 +03:00
tx: &nssa::privacy_preserving_transaction::PrivacyPreservingTransaction,
2025-12-18 11:44:38 +02:00
acc_decode_mask: &[AccDecodeData],
2025-10-14 08:33:20 +03:00
) -> Result<()> {
2025-12-18 11:44:38 +02:00
for (output_index, acc_decode_data) in acc_decode_mask.iter().enumerate() {
match acc_decode_data {
AccDecodeData::Decode(secret, acc_account_id) => {
let acc_ead = tx.message.encrypted_private_post_states[output_index].clone();
let acc_comm = tx.message.new_commitments[output_index].clone();
let (kind, res_acc) = nssa_core::EncryptionScheme::decrypt(
2025-12-18 11:44:38 +02:00
&acc_ead.ciphertext,
secret,
&acc_comm,
2026-03-03 23:21:08 +03:00
output_index
.try_into()
.expect("Output index is expected to fit in u32"),
2025-12-18 11:44:38 +02:00
)
.unwrap();
println!("Received new acc {res_acc:#?}");
self.storage
.key_chain_mut()
.insert_private_account(*acc_account_id, kind, res_acc)
.expect("Account Id should exist");
2025-12-18 11:44:38 +02:00
}
AccDecodeData::Skip => {}
}
2025-10-14 08:33:20 +03:00
}
println!("Transaction data is {:?}", tx.message);
Ok(())
}
2025-01-10 03:00:32 +02:00
pub async fn send_privacy_preserving_tx(
&self,
accounts: Vec<PrivacyPreservingAccount>,
instruction_data: InstructionData,
program: &ProgramWithDependencies,
) -> Result<(HashType, Vec<SharedSecretKey>), ExecutionFailureKind> {
self.send_privacy_preserving_tx_with_pre_check(accounts, instruction_data, program, |_| {
Ok(())
})
.await
}
2025-08-06 14:56:58 +03:00
pub async fn send_privacy_preserving_tx_with_pre_check(
&self,
accounts: Vec<PrivacyPreservingAccount>,
instruction_data: InstructionData,
program: &ProgramWithDependencies,
tx_pre_check: impl FnOnce(&[&Account]) -> Result<(), ExecutionFailureKind>,
) -> Result<(HashType, Vec<SharedSecretKey>), ExecutionFailureKind> {
let acc_manager = privacy_preserving_tx::AccountManager::new(self, accounts).await?;
let pre_states = acc_manager.pre_states();
tx_pre_check(
&pre_states
.iter()
.map(|pre| &pre.account)
.collect::<Vec<_>>(),
)?;
let private_account_keys = acc_manager.private_account_keys();
let (output, proof) = nssa::privacy_preserving_transaction::circuit::execute_and_prove(
pre_states,
instruction_data,
acc_manager.account_identities(),
&program.to_owned(),
)
.unwrap();
let message =
nssa::privacy_preserving_transaction::message::Message::try_from_circuit_output(
acc_manager.public_account_ids(),
Vec::from_iter(acc_manager.public_account_nonces()),
private_account_keys
.iter()
2026-04-16 19:07:27 +02:00
.map(|keys| (keys.npk, keys.vpk.clone(), keys.epk.clone()))
.collect(),
output,
)
.unwrap();
2025-08-06 14:56:58 +03:00
let witness_set =
nssa::privacy_preserving_transaction::witness_set::WitnessSet::for_message(
&message,
proof,
2025-11-14 01:28:34 -03:00
&acc_manager.public_account_auth(),
);
let tx = PrivacyPreservingTransaction::new(message, witness_set);
2026-03-05 12:35:18 +02:00
let shared_secrets: Vec<_> = private_account_keys
.into_iter()
.map(|keys| keys.ssk)
.collect();
Ok((
self.sequencer_client
.send_transaction(NSSATransaction::PrivacyPreserving(tx))
.await?,
shared_secrets,
))
}
2025-09-25 05:54:32 +03:00
pub async fn sync_to_latest_block(&mut self) -> Result<u64> {
let latest_block_id = self.sequencer_client.get_last_block_id().await?;
println!("Latest block is {latest_block_id}");
self.sync_to_block(latest_block_id).await?;
Ok(latest_block_id)
}
2025-12-03 00:17:12 +03:00
pub async fn sync_to_block(&mut self, block_id: u64) -> Result<()> {
use futures::TryStreamExt as _;
2025-08-20 17:16:51 +03:00
let last_synced_block = self.storage.last_synced_block();
if last_synced_block >= block_id {
2025-12-03 00:17:12 +03:00
return Ok(());
2025-10-14 15:29:18 +03:00
}
2025-10-10 17:44:42 -03:00
let before_polling = std::time::Instant::now();
let num_of_blocks = block_id.saturating_sub(last_synced_block);
2026-03-04 18:42:33 +03:00
if num_of_blocks == 0 {
return Ok(());
}
println!("Syncing to block {block_id}. Blocks to sync: {num_of_blocks}");
2025-08-06 14:56:58 +03:00
2025-12-03 00:17:12 +03:00
let poller = self.poller.clone();
let mut blocks =
std::pin::pin!(poller.poll_block_range(last_synced_block.saturating_add(1)..=block_id));
2025-10-15 15:17:30 +03:00
let bar = indicatif::ProgressBar::new(num_of_blocks);
2025-12-03 00:17:12 +03:00
while let Some(block) = blocks.try_next().await? {
for tx in block.body.transactions {
self.sync_private_accounts_with_tx(tx);
2025-10-15 15:17:30 +03:00
}
self.storage.set_last_synced_block(block.header.block_id);
self.store_persistent_data()?;
bar.inc(1);
2025-12-03 00:17:12 +03:00
}
bar.finish();
2025-10-15 15:17:30 +03:00
2025-10-28 16:02:30 +02:00
println!(
"Synced to block {block_id} in {:?}",
before_polling.elapsed()
2025-10-28 16:02:30 +02:00
);
2025-10-15 15:17:30 +03:00
2025-12-03 00:17:12 +03:00
Ok(())
}
2025-10-15 15:17:30 +03:00
2025-12-03 00:17:12 +03:00
fn sync_private_accounts_with_tx(&mut self, tx: NSSATransaction) {
let NSSATransaction::PrivacyPreserving(tx) = tx else {
return;
};
2025-10-15 15:17:30 +03:00
let affected_accounts = self
2025-12-03 00:17:12 +03:00
.storage
.key_chain()
.private_account_key_chains()
.flat_map(|(_account_id, key_chain, index)| {
2025-12-03 00:17:12 +03:00
let view_tag = EncryptedAccountData::compute_view_tag(
2026-03-18 16:53:07 -04:00
&key_chain.nullifier_public_key,
2026-03-03 23:21:08 +03:00
&key_chain.viewing_public_key,
2025-12-03 00:17:12 +03:00
);
let new_commitments = &tx.message.new_commitments;
2025-12-03 00:17:12 +03:00
tx.message()
.encrypted_private_post_states
.iter()
.enumerate()
.filter(move |(_, encrypted_data)| encrypted_data.view_tag == view_tag)
.filter_map(move |(ciph_id, encrypted_data)| {
2025-12-03 00:17:12 +03:00
let ciphertext = &encrypted_data.ciphertext;
let commitment = &new_commitments[ciph_id];
let shared_secret = key_chain.calculate_shared_secret_receiver(
&encrypted_data.epk,
index.and_then(ChainIndex::index),
);
2025-12-03 00:17:12 +03:00
nssa_core::EncryptionScheme::decrypt(
2025-12-03 00:17:12 +03:00
ciphertext,
&shared_secret,
commitment,
2026-03-03 23:21:08 +03:00
ciph_id
.try_into()
.expect("Ciphertext ID is expected to fit in u32"),
)
.map(|(kind, res_acc)| {
let npk = &key_chain.nullifier_public_key;
2026-05-08 21:41:48 -03:00
let account_id = nssa::AccountId::for_private_account(npk, &kind);
(account_id, kind, res_acc)
})
2025-12-03 00:17:12 +03:00
})
2026-03-05 12:35:18 +02:00
.collect::<Vec<_>>()
2025-12-03 00:17:12 +03:00
})
.collect::<Vec<_>>();
for (affected_account_id, kind, new_acc) in affected_accounts {
info!(
2025-12-03 00:17:12 +03:00
"Received new account for account_id {affected_account_id:#?} with account object {new_acc:#?}"
);
self.storage
.key_chain_mut()
.insert_private_account(affected_account_id, kind, new_acc)
.expect("Account Id should exist");
2025-12-03 00:17:12 +03:00
}
// Scan for updates to shared accounts (GMS-derived).
self.sync_shared_private_accounts_with_tx(&tx);
}
fn sync_shared_private_accounts_with_tx(&mut self, tx: &PrivacyPreservingTransaction) {
let shared_keys: Vec<_> = self
.storage
.key_chain()
2026-05-07 22:48:32 +02:00
.shared_private_accounts_iter()
.filter_map(|(&account_id, entry)| {
let holder = self
.storage
.key_chain()
2026-05-07 22:48:32 +02:00
.group_key_holder(&entry.group_label)?;
let keys = match (&entry.pda_seed, &entry.pda_program_id) {
(Some(pda_seed), Some(program_id)) => {
holder.derive_keys_for_pda(program_id, pda_seed)
}
(Some(_), None) => return None, // PDA without program_id, skip
_ => {
let derivation_seed = {
use sha2::Digest as _;
let mut hasher = sha2::Sha256::new();
hasher.update(b"/LEE/v0.3/SharedAccountTag/\x00\x00\x00\x00\x00");
hasher.update(entry.identifier.to_le_bytes());
let result: [u8; 32] = hasher.finalize().into();
result
};
holder.derive_keys_for_shared_account(&derivation_seed)
}
};
let npk = keys.generate_nullifier_public_key();
let vpk = keys.generate_viewing_public_key();
let vsk = keys.viewing_secret_key;
Some((account_id, npk, vpk, vsk))
})
.collect();
for (account_id, npk, vpk, vsk) in shared_keys {
let view_tag = EncryptedAccountData::compute_view_tag(&npk, &vpk);
for (ciph_id, encrypted_data) in tx
.message()
.encrypted_private_post_states
.iter()
.enumerate()
{
if encrypted_data.view_tag != view_tag {
continue;
}
let shared_secret = SharedSecretKey::new(vsk, &encrypted_data.epk);
let commitment = &tx.message.new_commitments[ciph_id];
2026-05-12 01:29:24 -03:00
if let Some((_kind, new_acc)) = nssa_core::EncryptionScheme::decrypt(
&encrypted_data.ciphertext,
&shared_secret,
commitment,
ciph_id
.try_into()
.expect("Ciphertext ID is expected to fit in u32"),
) {
info!("Synced shared account {account_id:#?} with new state {new_acc:#?}");
2026-05-07 22:48:32 +02:00
self.storage
.key_chain_mut()
2026-05-07 22:48:32 +02:00
.update_shared_private_account_state(&account_id, new_acc);
}
}
}
2025-10-15 15:17:30 +03:00
}
2026-03-03 23:21:08 +03:00
#[must_use]
2026-03-09 18:27:56 +03:00
pub const fn config_path(&self) -> &PathBuf {
2026-02-03 19:04:41 -03:00
&self.config_path
}
2026-03-03 23:21:08 +03:00
#[must_use]
2026-03-09 18:27:56 +03:00
pub const fn storage_path(&self) -> &PathBuf {
2026-02-03 19:04:41 -03:00
&self.storage_path
}
2026-03-03 23:21:08 +03:00
#[must_use]
2026-03-09 18:27:56 +03:00
pub const fn config_overrides(&self) -> &Option<WalletConfigOverrides> {
2026-02-03 19:04:41 -03:00
&self.config_overrides
}
2025-10-15 15:17:30 +03:00
}