mirror of
https://github.com/logos-blockchain/lssa.git
synced 2026-04-17 00:23:08 +00:00
316 lines
11 KiB
Rust
316 lines
11 KiB
Rust
use std::collections::{BTreeMap, HashMap, btree_map::Entry};
|
|
|
|
use anyhow::Result;
|
|
use bip39::Mnemonic;
|
|
use key_protocol::{
|
|
key_management::{
|
|
key_tree::{KeyTreePrivate, KeyTreePublic, chain_index::ChainIndex},
|
|
secret_holders::SeedHolder,
|
|
},
|
|
key_protocol_core::NSSAUserData,
|
|
};
|
|
use log::debug;
|
|
use nssa::program::Program;
|
|
|
|
use crate::config::{InitialAccountData, Label, PersistentAccountData, WalletConfig};
|
|
|
|
pub struct WalletChainStore {
|
|
pub user_data: NSSAUserData,
|
|
pub wallet_config: WalletConfig,
|
|
pub labels: HashMap<String, Label>,
|
|
}
|
|
|
|
impl WalletChainStore {
|
|
#[expect(
|
|
clippy::wildcard_enum_match_arm,
|
|
reason = "We perform search for specific variants only"
|
|
)]
|
|
pub fn new(
|
|
config: WalletConfig,
|
|
persistent_accounts: Vec<PersistentAccountData>,
|
|
labels: HashMap<String, Label>,
|
|
) -> Result<Self> {
|
|
if persistent_accounts.is_empty() {
|
|
anyhow::bail!("Roots not found; please run setup beforehand");
|
|
}
|
|
|
|
let mut public_init_acc_map = BTreeMap::new();
|
|
let mut private_init_acc_map = BTreeMap::new();
|
|
|
|
let public_root = persistent_accounts
|
|
.iter()
|
|
.find(|data| match data {
|
|
&PersistentAccountData::Public(data) => data.chain_index == ChainIndex::root(),
|
|
_ => false,
|
|
})
|
|
.cloned()
|
|
.expect("Malformed persistent account data, must have public root");
|
|
|
|
let private_root = persistent_accounts
|
|
.iter()
|
|
.find(|data| match data {
|
|
&PersistentAccountData::Private(data) => data.chain_index == ChainIndex::root(),
|
|
_ => false,
|
|
})
|
|
.cloned()
|
|
.expect("Malformed persistent account data, must have private root");
|
|
|
|
let mut public_tree = KeyTreePublic::new_from_root(match public_root {
|
|
PersistentAccountData::Public(data) => data.data,
|
|
_ => unreachable!(),
|
|
});
|
|
let mut private_tree = KeyTreePrivate::new_from_root(match private_root {
|
|
PersistentAccountData::Private(data) => data.data,
|
|
_ => unreachable!(),
|
|
});
|
|
|
|
for pers_acc_data in persistent_accounts {
|
|
match pers_acc_data {
|
|
PersistentAccountData::Public(data) => {
|
|
public_tree.insert(data.account_id, data.chain_index, data.data);
|
|
}
|
|
PersistentAccountData::Private(data) => {
|
|
private_tree.insert(data.account_id, data.chain_index, data.data);
|
|
}
|
|
PersistentAccountData::Preconfigured(acc_data) => match acc_data {
|
|
InitialAccountData::Public(data) => {
|
|
public_init_acc_map.insert(data.account_id, data.pub_sign_key);
|
|
}
|
|
InitialAccountData::Private(data) => {
|
|
private_init_acc_map.insert(
|
|
data.account_id,
|
|
(data.key_chain, vec![(data.identifier, data.account)]),
|
|
);
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
Ok(Self {
|
|
user_data: NSSAUserData::new_with_accounts(
|
|
public_init_acc_map,
|
|
private_init_acc_map,
|
|
public_tree,
|
|
private_tree,
|
|
)?,
|
|
wallet_config: config,
|
|
labels,
|
|
})
|
|
}
|
|
|
|
pub fn new_storage(config: WalletConfig, password: &str) -> Result<(Self, Mnemonic)> {
|
|
let mut public_init_acc_map = BTreeMap::new();
|
|
let mut private_init_acc_map = BTreeMap::new();
|
|
|
|
let initial_accounts = config
|
|
.initial_accounts
|
|
.clone()
|
|
.unwrap_or_else(InitialAccountData::create_initial_accounts_data);
|
|
|
|
for init_acc_data in initial_accounts {
|
|
match init_acc_data {
|
|
InitialAccountData::Public(data) => {
|
|
public_init_acc_map.insert(data.account_id, data.pub_sign_key);
|
|
}
|
|
InitialAccountData::Private(data) => {
|
|
let mut account = data.account;
|
|
// TODO: Program owner is only known after code is compiled and can't be set
|
|
// in the config. Therefore we overwrite it here on
|
|
// startup. Fix this when program id can be fetched
|
|
// from the node and queried from the wallet.
|
|
account.program_owner = Program::authenticated_transfer_program().id();
|
|
private_init_acc_map.insert(
|
|
data.account_id,
|
|
(data.key_chain, vec![(data.identifier, account)]),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: Use password for storage encryption
|
|
let _ = password;
|
|
let (seed_holder, mnemonic) = SeedHolder::new_mnemonic("");
|
|
let public_tree = KeyTreePublic::new(&seed_holder);
|
|
let private_tree = KeyTreePrivate::new(&seed_holder);
|
|
|
|
Ok((
|
|
Self {
|
|
user_data: NSSAUserData::new_with_accounts(
|
|
public_init_acc_map,
|
|
private_init_acc_map,
|
|
public_tree,
|
|
private_tree,
|
|
)?,
|
|
wallet_config: config,
|
|
labels: HashMap::new(),
|
|
},
|
|
mnemonic,
|
|
))
|
|
}
|
|
|
|
/// Restore storage from an existing mnemonic phrase.
|
|
pub fn restore_storage(
|
|
config: WalletConfig,
|
|
mnemonic: &Mnemonic,
|
|
password: &str,
|
|
) -> Result<Self> {
|
|
// TODO: Use password for storage encryption
|
|
let _ = password;
|
|
let seed_holder = SeedHolder::from_mnemonic(mnemonic, "");
|
|
let public_tree = KeyTreePublic::new(&seed_holder);
|
|
let private_tree = KeyTreePrivate::new(&seed_holder);
|
|
|
|
Ok(Self {
|
|
user_data: NSSAUserData::new_with_accounts(
|
|
BTreeMap::new(),
|
|
BTreeMap::new(),
|
|
public_tree,
|
|
private_tree,
|
|
)?,
|
|
wallet_config: config,
|
|
labels: HashMap::new(),
|
|
})
|
|
}
|
|
|
|
pub fn insert_private_account_data(
|
|
&mut self,
|
|
account_id: nssa::AccountId,
|
|
account: nssa_core::account::Account,
|
|
) {
|
|
debug!("inserting at address {account_id}, this account {account:?}");
|
|
|
|
// Update default accounts if present
|
|
if let Entry::Occupied(mut entry) =
|
|
self.user_data.default_user_private_accounts.entry(account_id)
|
|
{
|
|
let (key_chain, entries) = entry.get_mut();
|
|
let identifier = entries
|
|
.iter()
|
|
.find_map(|(id, _)| {
|
|
if nssa::AccountId::from((&key_chain.nullifier_public_key, *id)) == account_id {
|
|
Some(*id)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.unwrap_or(0);
|
|
// Update existing entry or insert new one
|
|
if let Some((_, acc)) = entries.iter_mut().find(|(id, _)| *id == identifier) {
|
|
*acc = account;
|
|
} else {
|
|
entries.push((identifier, account));
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Otherwise update the private key tree
|
|
// Identifier is hardcoded to 0 until ciphertexts carry the identifier
|
|
let identifier: nssa_core::Identifier = 0;
|
|
|
|
// Find the node by iterating all tree nodes for this account_id
|
|
let chain_index = self
|
|
.user_data
|
|
.private_key_tree
|
|
.account_id_map
|
|
.get(&account_id)
|
|
.cloned();
|
|
|
|
if let Some(chain_index) = chain_index {
|
|
// Node already in account_id_map — update its entry
|
|
if let Some(node) = self
|
|
.user_data
|
|
.private_key_tree
|
|
.key_map
|
|
.get_mut(&chain_index)
|
|
{
|
|
if let Some((_, acc)) =
|
|
node.value.1.iter_mut().find(|(id, _)| *id == identifier)
|
|
{
|
|
*acc = account;
|
|
} else {
|
|
node.value.1.push((identifier, account));
|
|
}
|
|
}
|
|
} else {
|
|
// Node not yet in account_id_map — find it by checking all nodes
|
|
for (ci, node) in self
|
|
.user_data
|
|
.private_key_tree
|
|
.key_map
|
|
.iter_mut()
|
|
{
|
|
let expected_id = nssa::AccountId::from((
|
|
&node.value.0.nullifier_public_key,
|
|
identifier,
|
|
));
|
|
if expected_id == account_id {
|
|
if let Some((_, acc)) =
|
|
node.value.1.iter_mut().find(|(id, _)| *id == identifier)
|
|
{
|
|
*acc = account;
|
|
} else {
|
|
node.value.1.push((identifier, account));
|
|
}
|
|
// Register in account_id_map
|
|
self.user_data
|
|
.private_key_tree
|
|
.account_id_map
|
|
.insert(account_id, ci.clone());
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use key_protocol::key_management::key_tree::{
|
|
keys_private::ChildKeysPrivate, keys_public::ChildKeysPublic,
|
|
};
|
|
|
|
use super::*;
|
|
use crate::config::{PersistentAccountDataPrivate, PersistentAccountDataPublic};
|
|
|
|
fn create_sample_wallet_config() -> WalletConfig {
|
|
WalletConfig {
|
|
sequencer_addr: "http://127.0.0.1".parse().unwrap(),
|
|
seq_poll_timeout: std::time::Duration::from_secs(12),
|
|
seq_tx_poll_max_blocks: 5,
|
|
seq_poll_max_retries: 10,
|
|
seq_block_poll_max_amount: 100,
|
|
basic_auth: None,
|
|
initial_accounts: None,
|
|
}
|
|
}
|
|
|
|
fn create_sample_persistent_accounts() -> Vec<PersistentAccountData> {
|
|
let public_data = ChildKeysPublic::root([42; 64]);
|
|
let private_data = ChildKeysPrivate::root([47; 64]);
|
|
|
|
vec![
|
|
PersistentAccountData::Public(PersistentAccountDataPublic {
|
|
account_id: public_data.account_id(),
|
|
chain_index: ChainIndex::root(),
|
|
data: public_data,
|
|
}),
|
|
PersistentAccountData::Private(Box::new(PersistentAccountDataPrivate {
|
|
account_id: nssa::AccountId::from((
|
|
&private_data.value.0.nullifier_public_key,
|
|
0_u128,
|
|
)),
|
|
chain_index: ChainIndex::root(),
|
|
data: private_data,
|
|
})),
|
|
]
|
|
}
|
|
|
|
#[test]
|
|
fn new_initializes_correctly() {
|
|
let config = create_sample_wallet_config();
|
|
let accs = create_sample_persistent_accounts();
|
|
|
|
let _ = WalletChainStore::new(config, accs, HashMap::new()).unwrap();
|
|
}
|
|
}
|