feat(wallet)!: add group CLI commands with --for-gms account creation

BREAKING CHANGE: `NewSubcommand::Private` has new required fields (`for_gms`, `pda`, `seed`, `program_id`). Code constructing this variant must include them (use `None`/`false` for defaults). `shared_accounts` value type changed from `Account` to `SharedAccountEntry`.
This commit is contained in:
Moudy 2026-05-05 18:55:51 +02:00
parent 7be0ed926c
commit cd545819e7
6 changed files with 232 additions and 166 deletions

View File

@ -21,6 +21,15 @@ pub struct UserPrivateAccountData {
pub accounts: Vec<(Identifier, Account)>, pub accounts: Vec<(Identifier, Account)>,
} }
/// Metadata for a shared account (GMS-derived), stored alongside the cached plaintext state.
/// The group label and identifier are needed to re-derive keys during sync.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SharedAccountEntry {
pub group_label: String,
pub identifier: Identifier,
pub account: Account,
}
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct NSSAUserData { pub struct NSSAUserData {
/// Default public accounts. /// Default public accounts.
@ -37,11 +46,12 @@ pub struct NSSAUserData {
#[serde(default)] #[serde(default)]
pub group_key_holders: BTreeMap<String, GroupKeyHolder>, pub group_key_holders: BTreeMap<String, GroupKeyHolder>,
/// Cached plaintext state of shared accounts (PDAs and regular shared accounts), /// Cached plaintext state of shared accounts (PDAs and regular shared accounts),
/// keyed by `AccountId`. Updated after each transaction by decrypting the circuit output. /// keyed by `AccountId`. Each entry stores the group label and identifier needed
/// The sequencer only stores encrypted commitments, so this local cache is the /// to re-derive keys during sync.
/// only source of plaintext state for these accounts. /// Old wallet files with `pda_accounts` (plain Account values) are incompatible with
#[serde(default, alias = "group_pda_accounts", alias = "pda_accounts")] /// this type. The `default` attribute ensures they deserialize as empty rather than failing.
pub shared_accounts: BTreeMap<nssa::AccountId, nssa_core::account::Account>, #[serde(default)]
pub shared_accounts: BTreeMap<nssa::AccountId, SharedAccountEntry>,
} }
impl NSSAUserData { impl NSSAUserData {

View File

@ -83,14 +83,27 @@ pub enum NewSubcommand {
label: Option<String>, label: Option<String>,
}, },
/// Single-account convenience: creates a key node and auto-registers one account with a random /// Single-account convenience: creates a key node and auto-registers one account with a random
/// identifier. /// identifier. When `--for-gms` is provided, derives keys from the named group instead of
/// the wallet's key tree.
Private { Private {
#[arg(long)] #[arg(long)]
/// Chain index of a parent node. /// Chain index of a parent node (ignored when --for-gms is used).
cci: Option<ChainIndex>, cci: Option<ChainIndex>,
#[arg(short, long)] #[arg(short, long)]
/// Label to assign to the new account. /// Label to assign to the new account.
label: Option<String>, label: Option<String>,
#[arg(long)]
/// Derive keys from a group's GMS instead of the wallet tree.
for_gms: Option<String>,
#[arg(long, requires = "for_gms")]
/// Create a PDA account (requires --seed and --program-id).
pda: bool,
#[arg(long, requires = "pda")]
/// PDA seed as 64-character hex string.
seed: Option<String>,
#[arg(long, requires = "pda")]
/// Program ID as hex string.
program_id: Option<String>,
}, },
/// Recommended for receiving from multiple senders: creates a key node (npk + vpk) without /// Recommended for receiving from multiple senders: creates a key node (npk + vpk) without
/// registering any account. /// registering any account.
@ -144,7 +157,14 @@ impl WalletSubcommand for NewSubcommand {
Ok(SubcommandReturnValue::RegisterAccount { account_id }) Ok(SubcommandReturnValue::RegisterAccount { account_id })
} }
Self::Private { cci, label } => { Self::Private {
cci,
label,
for_gms,
pda,
seed,
program_id,
} => {
if let Some(label) = &label if let Some(label) = &label
&& wallet_core && wallet_core
.storage .storage
@ -155,36 +175,132 @@ impl WalletSubcommand for NewSubcommand {
anyhow::bail!("Label '{label}' is already in use by another account"); anyhow::bail!("Label '{label}' is already in use by another account");
} }
let (account_id, chain_index) = wallet_core.create_new_account_private(cci); if let Some(group_name) = for_gms {
// GMS-derived account
let holder = wallet_core
.storage()
.user_data
.group_key_holder(&group_name)
.context(format!("Group '{group_name}' not found"))?;
let node = wallet_core if pda {
.storage // PDA shared account
.user_data let seed_hex = seed.context("--seed is required for PDA accounts")?;
.private_key_tree let pid_hex =
.key_map program_id.context("--program-id is required for PDA accounts")?;
.get(&chain_index)
.expect("Node was just inserted");
let key = &node.value.0;
if let Some(label) = label { let seed_bytes: [u8; 32] = hex::decode(&seed_hex)
wallet_core .context("Invalid seed hex")?
.try_into()
.map_err(|_err| anyhow::anyhow!("Seed must be exactly 32 bytes"))?;
let pda_seed = nssa_core::program::PdaSeed::new(seed_bytes);
let pid_bytes = hex::decode(&pid_hex).context("Invalid program ID hex")?;
if pid_bytes.len() != 32 {
anyhow::bail!("Program ID must be exactly 32 bytes");
}
let mut pid: nssa_core::program::ProgramId = [0; 8];
for (i, chunk) in pid_bytes.chunks_exact(4).enumerate() {
pid[i] = u32::from_le_bytes(chunk.try_into().unwrap());
}
let keys = holder.derive_keys_for_pda(&pda_seed);
let npk = keys.generate_nullifier_public_key();
let vpk = keys.generate_viewing_public_key();
let account_id = nssa::AccountId::for_private_pda(&pid, &pda_seed, &npk);
if let Some(label) = label {
wallet_core
.storage
.labels
.insert(account_id.to_string(), Label::new(label));
}
wallet_core.register_shared_account(
account_id,
group_name.clone(),
u128::MAX,
);
println!("PDA shared account from group '{group_name}'");
println!("AccountId: {account_id}");
println!("NPK: {}", hex::encode(npk.0));
println!("VPK: {}", hex::encode(&vpk.0));
wallet_core.store_persistent_data().await?;
Ok(SubcommandReturnValue::RegisterAccount { account_id })
} else {
// Regular shared account. The tag is derived deterministically
// from the identifier so that keys can be re-derived without
// storing the tag separately.
let identifier: nssa_core::Identifier = rand::random();
let tag = {
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 keys = holder.derive_keys_for_shared_account(&tag);
let npk = keys.generate_nullifier_public_key();
let vpk = keys.generate_viewing_public_key();
let account_id = nssa::AccountId::from((&npk, identifier));
if let Some(label) = label {
wallet_core
.storage
.labels
.insert(account_id.to_string(), Label::new(label));
}
wallet_core.register_shared_account(
account_id,
group_name.clone(),
identifier,
);
println!("Shared account from group '{group_name}'");
println!("AccountId: Private/{account_id}");
println!("NPK: {}", hex::encode(npk.0));
println!("VPK: {}", hex::encode(&vpk.0));
wallet_core.store_persistent_data().await?;
Ok(SubcommandReturnValue::RegisterAccount { account_id })
}
} else {
// Standard wallet-tree-derived account
let (account_id, chain_index) = wallet_core.create_new_account_private(cci);
let node = wallet_core
.storage .storage
.labels .user_data
.insert(account_id.to_string(), Label::new(label)); .private_key_tree
.key_map
.get(&chain_index)
.expect("Node was just inserted");
let key = &node.value.0;
if let Some(label) = label {
wallet_core
.storage
.labels
.insert(account_id.to_string(), Label::new(label));
}
println!(
"Generated new account with account_id Private/{account_id} at path {chain_index}"
);
println!("With npk {}", hex::encode(key.nullifier_public_key.0));
println!(
"With vpk {}",
hex::encode(key.viewing_public_key.to_bytes())
);
wallet_core.store_persistent_data().await?;
Ok(SubcommandReturnValue::RegisterAccount { account_id })
} }
println!(
"Generated new account with account_id Private/{account_id} at path {chain_index}"
);
println!("With npk {}", hex::encode(key.nullifier_public_key.0));
println!(
"With vpk {}",
hex::encode(key.viewing_public_key.to_bytes())
);
wallet_core.store_persistent_data().await?;
Ok(SubcommandReturnValue::RegisterAccount { account_id })
} }
Self::PrivateAccountsKey { cci } => { Self::PrivateAccountsKey { cci } => {
let chain_index = wallet_core.create_private_accounts_key(cci); let chain_index = wallet_core.create_private_accounts_key(cci);

View File

@ -1,15 +1,13 @@
use anyhow::{Context as _, Result}; use anyhow::{Context as _, Result};
use clap::Subcommand; use clap::Subcommand;
use key_protocol::key_management::group_key_holder::GroupKeyHolder; use key_protocol::key_management::group_key_holder::GroupKeyHolder;
use nssa::AccountId;
use nssa_core::program::PdaSeed;
use crate::{ use crate::{
WalletCore, WalletCore,
cli::{SubcommandReturnValue, WalletSubcommand}, cli::{SubcommandReturnValue, WalletSubcommand},
}; };
/// Group PDA management commands. /// Group key management commands.
#[derive(Subcommand, Debug, Clone)] #[derive(Subcommand, Debug, Clone)]
pub enum GroupSubcommand { pub enum GroupSubcommand {
/// Create a new group with a fresh random GMS. /// Create a new group with a fresh random GMS.
@ -24,29 +22,15 @@ pub enum GroupSubcommand {
/// Raw GMS as 64-character hex string. /// Raw GMS as 64-character hex string.
#[arg(long)] #[arg(long)]
gms: String, gms: String,
/// Epoch (defaults to 0).
#[arg(long, default_value = "0")]
epoch: u32,
}, },
/// Export the raw GMS hex for backup or manual distribution. /// Export the raw GMS hex for backup or manual distribution.
Export { Export {
/// Group name. /// Group name.
name: String, name: String,
}, },
/// List all groups with their epochs. /// List all groups.
#[command(visible_alias = "ls")] #[command(visible_alias = "ls")]
List, List,
/// Derive keys for a PDA seed and show the resulting AccountId.
Derive {
/// Group name.
name: String,
/// PDA seed as 64-character hex string.
#[arg(long)]
seed: String,
/// Program ID as hex string (u32x8 little-endian).
#[arg(long)]
program_id: String,
},
/// Remove a group from the wallet. /// Remove a group from the wallet.
Remove { Remove {
/// Group name. /// Group name.
@ -56,9 +40,9 @@ pub enum GroupSubcommand {
Invite { Invite {
/// Group name. /// Group name.
name: String, name: String,
/// Recipient's viewing public key as hex string. /// Recipient's sealing public key as hex string.
#[arg(long)] #[arg(long)]
vpk: String, key: String,
}, },
/// Unseal a received GMS and store it (join a group). /// Unseal a received GMS and store it (join a group).
Join { Join {
@ -67,15 +51,10 @@ pub enum GroupSubcommand {
/// Sealed GMS as hex string (from the inviter). /// Sealed GMS as hex string (from the inviter).
#[arg(long)] #[arg(long)]
sealed: String, sealed: String,
/// Account label or Private/<id> whose VSK to use for decryption. /// Account ID whose viewing secret key to use for decryption.
#[arg(long)] #[arg(long)]
account: String, account: String,
}, },
/// Ratchet the GMS to exclude removed members.
Ratchet {
/// Group name.
name: String,
},
} }
impl WalletSubcommand for GroupSubcommand { impl WalletSubcommand for GroupSubcommand {
@ -88,28 +67,25 @@ impl WalletSubcommand for GroupSubcommand {
if wallet_core if wallet_core
.storage() .storage()
.user_data .user_data
.get_group_key_holder(&name) .group_key_holder(&name)
.is_some() .is_some()
{ {
anyhow::bail!("Group '{name}' already exists"); anyhow::bail!("Group '{name}' already exists");
} }
let holder = GroupKeyHolder::new(); let holder = GroupKeyHolder::new();
wallet_core wallet_core.insert_group_key_holder(name.clone(), holder);
.storage_mut()
.user_data
.insert_group_key_holder(name.clone(), holder);
wallet_core.store_persistent_data().await?; wallet_core.store_persistent_data().await?;
println!("Created group '{name}' at epoch 0"); println!("Created group '{name}'");
Ok(SubcommandReturnValue::Empty) Ok(SubcommandReturnValue::Empty)
} }
Self::Import { name, gms, epoch } => { Self::Import { name, gms } => {
if wallet_core if wallet_core
.storage() .storage()
.user_data .user_data
.get_group_key_holder(&name) .group_key_holder(&name)
.is_some() .is_some()
{ {
anyhow::bail!("Group '{name}' already exists"); anyhow::bail!("Group '{name}' already exists");
@ -118,16 +94,13 @@ impl WalletSubcommand for GroupSubcommand {
let gms_bytes: [u8; 32] = hex::decode(&gms) let gms_bytes: [u8; 32] = hex::decode(&gms)
.context("Invalid GMS hex")? .context("Invalid GMS hex")?
.try_into() .try_into()
.map_err(|_| anyhow::anyhow!("GMS must be exactly 32 bytes"))?; .map_err(|_err| anyhow::anyhow!("GMS must be exactly 32 bytes"))?;
let holder = GroupKeyHolder::from_gms_and_epoch(gms_bytes, epoch); let holder = GroupKeyHolder::from_gms(gms_bytes);
wallet_core wallet_core.insert_group_key_holder(name.clone(), holder);
.storage_mut()
.user_data
.insert_group_key_holder(name.clone(), holder);
wallet_core.store_persistent_data().await?; wallet_core.store_persistent_data().await?;
println!("Imported group '{name}' at epoch {epoch}"); println!("Imported group '{name}'");
Ok(SubcommandReturnValue::Empty) Ok(SubcommandReturnValue::Empty)
} }
@ -135,14 +108,12 @@ impl WalletSubcommand for GroupSubcommand {
let holder = wallet_core let holder = wallet_core
.storage() .storage()
.user_data .user_data
.get_group_key_holder(&name) .group_key_holder(&name)
.context(format!("Group '{name}' not found"))?; .context(format!("Group '{name}' not found"))?;
let gms_hex = hex::encode(holder.dangerous_raw_gms()); let gms_hex = hex::encode(holder.dangerous_raw_gms());
let epoch = holder.epoch();
println!("Group: {name}"); println!("Group: {name}");
println!("Epoch: {epoch}");
println!("GMS: {gms_hex}"); println!("GMS: {gms_hex}");
Ok(SubcommandReturnValue::Empty) Ok(SubcommandReturnValue::Empty)
} }
@ -152,60 +123,15 @@ impl WalletSubcommand for GroupSubcommand {
if holders.is_empty() { if holders.is_empty() {
println!("No groups found"); println!("No groups found");
} else { } else {
for (name, holder) in holders { for name in holders.keys() {
println!("{name} (epoch {})", holder.epoch()); println!("{name}");
} }
} }
Ok(SubcommandReturnValue::Empty) Ok(SubcommandReturnValue::Empty)
} }
Self::Derive {
name,
seed,
program_id,
} => {
let holder = wallet_core
.storage()
.user_data
.get_group_key_holder(&name)
.context(format!("Group '{name}' not found"))?;
let seed_bytes: [u8; 32] = hex::decode(&seed)
.context("Invalid seed hex")?
.try_into()
.map_err(|_| anyhow::anyhow!("Seed must be exactly 32 bytes"))?;
let pda_seed = PdaSeed::new(seed_bytes);
let pid_bytes =
hex::decode(&program_id).context("Invalid program ID hex")?;
if pid_bytes.len() != 32 {
anyhow::bail!("Program ID must be exactly 32 bytes");
}
let mut pid: nssa_core::program::ProgramId = [0; 8];
for (i, chunk) in pid_bytes.chunks_exact(4).enumerate() {
pid[i] = u32::from_le_bytes(chunk.try_into().unwrap());
}
let keys = holder.derive_keys_for_pda(&pda_seed);
let npk = keys.generate_nullifier_public_key();
let vpk = keys.generate_viewing_public_key();
let account_id = AccountId::for_private_pda(&pid, &pda_seed, &npk);
println!("Group: {name}");
println!("NPK: {}", hex::encode(npk.0));
println!("VPK: {}", hex::encode(&vpk.0));
println!("AccountId: {account_id}");
Ok(SubcommandReturnValue::Empty)
}
Self::Remove { name } => { Self::Remove { name } => {
if wallet_core if wallet_core.remove_group_key_holder(&name).is_none() {
.storage_mut()
.user_data
.group_key_holders
.remove(&name)
.is_none()
{
anyhow::bail!("Group '{name}' not found"); anyhow::bail!("Group '{name}' not found");
} }
@ -214,18 +140,18 @@ impl WalletSubcommand for GroupSubcommand {
Ok(SubcommandReturnValue::Empty) Ok(SubcommandReturnValue::Empty)
} }
Self::Invite { name, vpk } => { Self::Invite { name, key } => {
let holder = wallet_core let holder = wallet_core
.storage() .storage()
.user_data .user_data
.get_group_key_holder(&name) .group_key_holder(&name)
.context(format!("Group '{name}' not found"))?; .context(format!("Group '{name}' not found"))?;
let vpk_bytes = hex::decode(&vpk).context("Invalid VPK hex")?; let key_bytes = hex::decode(&key).context("Invalid key hex")?;
let recipient_vpk = let recipient_key =
nssa_core::encryption::shared_key_derivation::Secp256k1Point(vpk_bytes); nssa_core::encryption::shared_key_derivation::Secp256k1Point(key_bytes);
let sealed = holder.seal_for(&recipient_vpk); let sealed = holder.seal_for(&recipient_key);
println!("{}", hex::encode(&sealed)); println!("{}", hex::encode(&sealed));
Ok(SubcommandReturnValue::Empty) Ok(SubcommandReturnValue::Empty)
} }
@ -238,7 +164,7 @@ impl WalletSubcommand for GroupSubcommand {
if wallet_core if wallet_core
.storage() .storage()
.user_data .user_data
.get_group_key_holder(&name) .group_key_holder(&name)
.is_some() .is_some()
{ {
anyhow::bail!("Group '{name}' already exists"); anyhow::bail!("Group '{name}' already exists");
@ -246,11 +172,8 @@ impl WalletSubcommand for GroupSubcommand {
let sealed_bytes = hex::decode(&sealed).context("Invalid sealed hex")?; let sealed_bytes = hex::decode(&sealed).context("Invalid sealed hex")?;
// Resolve the account to get the VSK let account_id: nssa::AccountId = account.parse().context("Invalid account ID")?;
let account_id: nssa::AccountId = account let (keychain, _, _) = wallet_core
.parse()
.context("Invalid account ID (use Private/<base58>)")?;
let (keychain, _) = wallet_core
.storage() .storage()
.user_data .user_data
.get_private_account(account_id) .get_private_account(account_id)
@ -260,34 +183,10 @@ impl WalletSubcommand for GroupSubcommand {
let holder = GroupKeyHolder::unseal(&sealed_bytes, &vsk) let holder = GroupKeyHolder::unseal(&sealed_bytes, &vsk)
.map_err(|e| anyhow::anyhow!("Failed to unseal: {e:?}"))?; .map_err(|e| anyhow::anyhow!("Failed to unseal: {e:?}"))?;
let epoch = holder.epoch(); wallet_core.insert_group_key_holder(name.clone(), holder);
wallet_core
.storage_mut()
.user_data
.insert_group_key_holder(name.clone(), holder);
wallet_core.store_persistent_data().await?; wallet_core.store_persistent_data().await?;
println!("Joined group '{name}' at epoch {epoch}"); println!("Joined group '{name}'");
Ok(SubcommandReturnValue::Empty)
}
Self::Ratchet { name } => {
let holder = wallet_core
.storage_mut()
.user_data
.group_key_holders
.get_mut(&name)
.context(format!("Group '{name}' not found"))?;
let mut salt = [0_u8; 32];
rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut salt);
holder.ratchet(salt);
let epoch = holder.epoch();
wallet_core.store_persistent_data().await?;
println!("Ratcheted group '{name}' to epoch {epoch}");
println!("Re-invite remaining members with 'group invite'");
Ok(SubcommandReturnValue::Empty) Ok(SubcommandReturnValue::Empty)
} }
} }

View File

@ -14,6 +14,7 @@ use crate::{
account::AccountSubcommand, account::AccountSubcommand,
chain::ChainSubcommand, chain::ChainSubcommand,
config::ConfigSubcommand, config::ConfigSubcommand,
group::GroupSubcommand,
programs::{ programs::{
amm::AmmProgramAgnosticSubcommand, ata::AtaSubcommand, amm::AmmProgramAgnosticSubcommand, ata::AtaSubcommand,
native_token_transfer::AuthTransferSubcommand, pinata::PinataProgramAgnosticSubcommand, native_token_transfer::AuthTransferSubcommand, pinata::PinataProgramAgnosticSubcommand,
@ -25,6 +26,7 @@ use crate::{
pub mod account; pub mod account;
pub mod chain; pub mod chain;
pub mod config; pub mod config;
pub mod group;
pub mod programs; pub mod programs;
pub(crate) trait WalletSubcommand { pub(crate) trait WalletSubcommand {
@ -57,6 +59,9 @@ pub enum Command {
/// Associated Token Account program interaction subcommand. /// Associated Token Account program interaction subcommand.
#[command(subcommand)] #[command(subcommand)]
Ata(AtaSubcommand), Ata(AtaSubcommand),
/// Group key management (create, invite, join, derive keys).
#[command(subcommand)]
Group(GroupSubcommand),
/// Check the wallet can connect to the node and builtin local programs /// Check the wallet can connect to the node and builtin local programs
/// match the remote versions. /// match the remote versions.
CheckHealth, CheckHealth,
@ -164,6 +169,7 @@ pub async fn execute_subcommand(
Command::Token(token_subcommand) => token_subcommand.handle_subcommand(wallet_core).await?, Command::Token(token_subcommand) => token_subcommand.handle_subcommand(wallet_core).await?,
Command::AMM(amm_subcommand) => amm_subcommand.handle_subcommand(wallet_core).await?, Command::AMM(amm_subcommand) => amm_subcommand.handle_subcommand(wallet_core).await?,
Command::Ata(ata_subcommand) => ata_subcommand.handle_subcommand(wallet_core).await?, Command::Ata(ata_subcommand) => ata_subcommand.handle_subcommand(wallet_core).await?,
Command::Group(group_subcommand) => group_subcommand.handle_subcommand(wallet_core).await?,
Command::Config(config_subcommand) => { Command::Config(config_subcommand) => {
config_subcommand.handle_subcommand(wallet_core).await? config_subcommand.handle_subcommand(wallet_core).await?
} }

View File

@ -287,6 +287,41 @@ impl WalletCore {
(account_id, cci) (account_id, cci)
} }
/// Insert a group key holder into storage.
pub fn insert_group_key_holder(
&mut self,
name: String,
holder: key_protocol::key_management::group_key_holder::GroupKeyHolder,
) {
self.storage.user_data.insert_group_key_holder(name, holder);
}
/// Remove a group key holder from storage. Returns the removed holder if it existed.
pub fn remove_group_key_holder(
&mut self,
name: &str,
) -> Option<key_protocol::key_management::group_key_holder::GroupKeyHolder> {
self.storage.user_data.group_key_holders.remove(name)
}
/// Register a shared account in storage for sync tracking.
pub fn register_shared_account(
&mut self,
account_id: AccountId,
group_label: String,
identifier: nssa_core::Identifier,
) {
use key_protocol::key_protocol_core::SharedAccountEntry;
self.storage.user_data.shared_accounts.insert(
account_id,
SharedAccountEntry {
group_label,
identifier,
account: Account::default(),
},
);
}
/// Get account balance. /// Get account balance.
pub async fn get_account_balance(&self, acc: AccountId) -> Result<u128> { pub async fn get_account_balance(&self, acc: AccountId) -> Result<u128> {
Ok(self.sequencer_client.get_account_balance(acc).await?) Ok(self.sequencer_client.get_account_balance(acc).await?)

View File

@ -337,7 +337,7 @@ async fn private_pda_preparation(
.user_data .user_data
.shared_accounts .shared_accounts
.get(&account_id) .get(&account_id)
.cloned() .map(|e| e.account.clone())
.unwrap_or_default(); .unwrap_or_default();
let exists = acc != nssa_core::account::Account::default(); let exists = acc != nssa_core::account::Account::default();
@ -388,7 +388,7 @@ async fn private_shared_preparation(
.user_data .user_data
.shared_accounts .shared_accounts
.get(&account_id) .get(&account_id)
.cloned() .map(|e| e.account.clone())
.unwrap_or_default(); .unwrap_or_default();
let exists = acc != nssa_core::account::Account::default(); let exists = acc != nssa_core::account::Account::default();