use crate::error::{Result, WalletError};
use crate::wallet::{KeyShare, MpcWallet, WalletId};
use tenzro_crypto::bls::BlsKeyPair;
use tenzro_crypto::frost::keygen_with_trusted_dealer;
use tenzro_crypto::pq::MlDsaSigningKey;
use tenzro_types::primitives::Address;
use tracing::{debug, info};
#[derive(Debug, Clone)]
pub struct ProvisioningConfig {
pub threshold: u16,
pub total_shares: u16,
pub participant_id_prefix: String,
}
impl Default for ProvisioningConfig {
fn default() -> Self {
Self {
threshold: 2,
total_shares: 3,
participant_id_prefix: "tenzro-participant".to_string(),
}
}
}
impl ProvisioningConfig {
pub fn new(threshold: u16, total_shares: u16) -> Result<Self> {
let cfg = Self {
threshold,
total_shares,
participant_id_prefix: "tenzro-participant".to_string(),
};
cfg.validate()?;
Ok(cfg)
}
pub fn with_participant_prefix(mut self, prefix: String) -> Self {
self.participant_id_prefix = prefix;
self
}
pub fn validate(&self) -> Result<()> {
if self.threshold == 0 {
return Err(WalletError::ProvisioningFailed(
"Threshold must be at least 1".to_string(),
));
}
if self.threshold > self.total_shares {
return Err(WalletError::ProvisioningFailed(
"Threshold cannot exceed total shares".to_string(),
));
}
if self.total_shares > 100 {
return Err(WalletError::ProvisioningFailed(
"Total shares cannot exceed 100".to_string(),
));
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct WalletProvisioner {
config: ProvisioningConfig,
}
impl WalletProvisioner {
pub fn new() -> Self {
Self {
config: ProvisioningConfig::default(),
}
}
pub fn with_config(config: ProvisioningConfig) -> Result<Self> {
config.validate()?;
Ok(Self { config })
}
pub fn config(&self) -> &ProvisioningConfig {
&self.config
}
pub fn provision_wallet(&self) -> Result<MpcWallet> {
self.provision_wallet_with_id(WalletId::new())
}
pub fn provision_wallet_with_id(&self, wallet_id: WalletId) -> Result<MpcWallet> {
info!(
"Provisioning new FROST wallet: {} ({}-of-{})",
wallet_id, self.config.threshold, self.config.total_shares
);
debug!("Generating {} FROST shares", self.config.total_shares);
let (pubkey_pkg, secret_shares) =
keygen_with_trusted_dealer(self.config.threshold, self.config.total_shares)
.map_err(|e| WalletError::ProvisioningFailed(e.to_string()))?;
let key_shares: Vec<KeyShare> = secret_shares
.into_iter()
.map(|s| {
let participant_id =
format!("{}-{}", self.config.participant_id_prefix, s.index.0);
KeyShare::new(s.index, participant_id, s)
})
.collect();
let group_pk = pubkey_pkg.group_public_key.as_public_key();
let address = self.derive_address(&group_pk);
debug!("Wallet address: {}", address);
let pq_signing_key = MlDsaSigningKey::generate();
let bls_signing_key = BlsKeyPair::generate()
.map_err(|e| WalletError::ProvisioningFailed(e.to_string()))?;
let wallet = MpcWallet::new(
wallet_id.clone(),
address,
key_shares,
pubkey_pkg,
pq_signing_key,
bls_signing_key,
)?;
info!(
"Successfully provisioned FROST wallet {} at address {}",
wallet_id, address
);
Ok(wallet)
}
pub fn provision_wallet_with_label(&self, label: String) -> Result<MpcWallet> {
let mut wallet = self.provision_wallet()?;
wallet.set_label(label);
Ok(wallet)
}
fn derive_address(&self, public_key: &tenzro_crypto::PublicKey) -> Address {
let crypto_address = public_key.to_address();
let mut addr_bytes = [0u8; 32];
addr_bytes[..20].copy_from_slice(crypto_address.as_bytes());
Address::new(addr_bytes)
}
}
impl Default for WalletProvisioner {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_provisioning_config_default() {
let config = ProvisioningConfig::default();
assert_eq!(config.threshold, 2);
assert_eq!(config.total_shares, 3);
}
#[test]
fn test_provisioning_config_validation() {
let config = ProvisioningConfig::new(2, 3).unwrap();
assert!(config.validate().is_ok());
assert!(ProvisioningConfig::new(4, 3).is_err());
assert!(ProvisioningConfig::new(0, 3).is_err());
}
#[test]
fn test_provision_wallet_default() {
let provisioner = WalletProvisioner::new();
let wallet = provisioner.provision_wallet().unwrap();
assert_eq!(wallet.threshold, 2);
assert_eq!(wallet.total_shares, 3);
assert_eq!(wallet.share_count(), 3);
assert!(wallet.can_sign());
assert_eq!(wallet.key_type(), tenzro_crypto::KeyType::Ed25519);
}
#[test]
fn test_provision_wallet_with_label() {
let provisioner = WalletProvisioner::new();
let label = "My Tenzro Wallet".to_string();
let wallet = provisioner
.provision_wallet_with_label(label.clone())
.unwrap();
assert_eq!(wallet.label, Some(label));
}
#[test]
fn test_provision_wallet_3_of_5() {
let config = ProvisioningConfig::new(3, 5).unwrap();
let provisioner = WalletProvisioner::with_config(config).unwrap();
let wallet = provisioner.provision_wallet().unwrap();
assert_eq!(wallet.threshold, 3);
assert_eq!(wallet.total_shares, 5);
assert!(wallet.can_sign());
}
#[test]
fn test_provision_multiple_wallets_distinct() {
let provisioner = WalletProvisioner::new();
let wallet1 = provisioner.provision_wallet().unwrap();
let wallet2 = provisioner.provision_wallet().unwrap();
assert_ne!(wallet1.wallet_id, wallet2.wallet_id);
assert_ne!(wallet1.address, wallet2.address);
assert_ne!(wallet1.public_key, wallet2.public_key);
}
}