mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-05-12 11:09:41 +00:00
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:
parent
7be0ed926c
commit
cd545819e7
@ -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 {
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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?
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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?)
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user