use miden_objects::Word;
use miden_objects::account::AccountStorageMode;
use crate::procedures::ProcedureName;
#[derive(Debug, Clone)]
pub struct PsmConfig {
pub endpoint: String,
}
impl PsmConfig {
pub fn new(endpoint: impl Into<String>) -> Self {
Self {
endpoint: endpoint.into(),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ProcedureThreshold {
pub procedure: ProcedureName,
pub threshold: u32,
}
impl ProcedureThreshold {
pub fn new(procedure: ProcedureName, threshold: u32) -> Self {
Self {
procedure,
threshold,
}
}
pub fn procedure_root(&self) -> Word {
self.procedure.root()
}
}
#[derive(Debug, Clone)]
pub struct MultisigConfig {
pub threshold: u32,
pub signer_commitments: Vec<Word>,
pub psm_config: PsmConfig,
pub storage_mode: AccountStorageMode,
pub procedure_thresholds: Vec<ProcedureThreshold>,
}
impl MultisigConfig {
pub fn new(threshold: u32, signer_commitments: Vec<Word>, psm_config: PsmConfig) -> Self {
Self {
threshold,
signer_commitments,
psm_config,
storage_mode: AccountStorageMode::Private,
procedure_thresholds: Vec::new(),
}
}
pub fn with_storage_mode(mut self, storage_mode: AccountStorageMode) -> Self {
self.storage_mode = storage_mode;
self
}
pub fn with_procedure_thresholds(mut self, thresholds: Vec<ProcedureThreshold>) -> Self {
self.procedure_thresholds = thresholds;
self
}
pub fn validate(&self) -> Result<(), String> {
if self.threshold == 0 {
return Err("threshold must be greater than 0".to_string());
}
if self.signer_commitments.is_empty() {
return Err("at least one signer commitment is required".to_string());
}
if self.threshold > self.signer_commitments.len() as u32 {
return Err(format!(
"threshold ({}) cannot exceed number of signers ({})",
self.threshold,
self.signer_commitments.len()
));
}
if self.psm_config.endpoint.is_empty() {
return Err("PSM endpoint is required".to_string());
}
for pt in &self.procedure_thresholds {
if pt.threshold == 0 {
return Err("procedure threshold must be at least 1".to_string());
}
if pt.threshold > self.signer_commitments.len() as u32 {
return Err(format!(
"procedure threshold ({}) cannot exceed number of signers ({})",
pt.threshold,
self.signer_commitments.len()
));
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use miden_objects::Word;
use miden_objects::account::AccountStorageMode;
fn dummy_word() -> Word {
Word::default()
}
#[test]
fn psm_config_new_creates_with_endpoint() {
let config = PsmConfig::new("http://localhost:50051");
assert_eq!(config.endpoint, "http://localhost:50051");
}
#[test]
fn psm_config_new_accepts_string() {
let endpoint = String::from("http://localhost:50051");
let config = PsmConfig::new(endpoint);
assert_eq!(config.endpoint, "http://localhost:50051");
}
#[test]
fn procedure_threshold_new_creates_correctly() {
let threshold = ProcedureThreshold::new(ProcedureName::ReceiveAsset, 1);
assert_eq!(threshold.procedure, ProcedureName::ReceiveAsset);
assert_eq!(threshold.threshold, 1);
}
#[test]
fn procedure_threshold_procedure_root_returns_correct_root() {
let threshold = ProcedureThreshold::new(ProcedureName::SendAsset, 2);
let root = threshold.procedure_root();
assert_eq!(root, ProcedureName::SendAsset.root());
}
#[test]
fn multisig_config_new_sets_all_fields() {
let signers = vec![dummy_word(), dummy_word()];
let psm = PsmConfig::new("http://psm:50051");
let config = MultisigConfig::new(2, signers.clone(), psm);
assert_eq!(config.threshold, 2);
assert_eq!(config.signer_commitments.len(), 2);
assert_eq!(config.psm_config.endpoint, "http://psm:50051");
assert!(matches!(config.storage_mode, AccountStorageMode::Private));
assert!(config.procedure_thresholds.is_empty());
}
#[test]
fn multisig_config_with_storage_mode_sets_mode() {
let config = MultisigConfig::new(1, vec![dummy_word()], PsmConfig::new("http://psm:50051"))
.with_storage_mode(AccountStorageMode::Public);
assert!(matches!(config.storage_mode, AccountStorageMode::Public));
}
#[test]
fn multisig_config_with_procedure_thresholds_sets_thresholds() {
let thresholds = vec![
ProcedureThreshold::new(ProcedureName::ReceiveAsset, 1),
ProcedureThreshold::new(ProcedureName::SendAsset, 2),
];
let config = MultisigConfig::new(
2,
vec![dummy_word(), dummy_word()],
PsmConfig::new("http://psm:50051"),
)
.with_procedure_thresholds(thresholds);
assert_eq!(config.procedure_thresholds.len(), 2);
assert_eq!(
config.procedure_thresholds[0].procedure,
ProcedureName::ReceiveAsset
);
assert_eq!(config.procedure_thresholds[0].threshold, 1);
assert_eq!(
config.procedure_thresholds[1].procedure,
ProcedureName::SendAsset
);
assert_eq!(config.procedure_thresholds[1].threshold, 2);
}
#[test]
fn validate_threshold_zero_returns_error() {
let config = MultisigConfig::new(0, vec![dummy_word()], PsmConfig::new("http://psm:50051"));
let err = config.validate().unwrap_err();
assert!(err.contains("threshold"));
assert!(err.contains("greater than 0"));
}
#[test]
fn validate_empty_commitments_returns_error() {
let config = MultisigConfig::new(1, vec![], PsmConfig::new("http://psm:50051"));
let err = config.validate().unwrap_err();
assert!(err.contains("signer"));
}
#[test]
fn validate_threshold_exceeds_signers_returns_error() {
let config = MultisigConfig::new(
3,
vec![dummy_word(), dummy_word()],
PsmConfig::new("http://psm:50051"),
);
let err = config.validate().unwrap_err();
assert!(err.contains("exceed"));
assert!(err.contains("3"));
assert!(err.contains("2"));
}
#[test]
fn validate_empty_psm_endpoint_returns_error() {
let config = MultisigConfig::new(1, vec![dummy_word()], PsmConfig::new(""));
let err = config.validate().unwrap_err();
assert!(err.contains("PSM endpoint"));
}
#[test]
fn validate_valid_1_of_1_config() {
let config = MultisigConfig::new(1, vec![dummy_word()], PsmConfig::new("http://psm:50051"));
assert!(config.validate().is_ok());
}
#[test]
fn validate_valid_2_of_3_config() {
let config = MultisigConfig::new(
2,
vec![dummy_word(), dummy_word(), dummy_word()],
PsmConfig::new("http://psm:50051"),
);
assert!(config.validate().is_ok());
}
#[test]
fn validate_valid_3_of_3_config() {
let config = MultisigConfig::new(
3,
vec![dummy_word(), dummy_word(), dummy_word()],
PsmConfig::new("http://psm:50051"),
);
assert!(config.validate().is_ok());
}
#[test]
fn validate_threshold_equals_signers_is_valid() {
let config = MultisigConfig::new(
2,
vec![dummy_word(), dummy_word()],
PsmConfig::new("http://psm:50051"),
);
assert!(config.validate().is_ok());
}
#[test]
fn validate_procedure_threshold_zero_returns_error() {
let thresholds = vec![ProcedureThreshold::new(ProcedureName::ReceiveAsset, 0)];
let config = MultisigConfig::new(1, vec![dummy_word()], PsmConfig::new("http://psm:50051"))
.with_procedure_thresholds(thresholds);
let err = config.validate().unwrap_err();
assert!(err.contains("procedure threshold"));
assert!(err.contains("at least 1"));
}
#[test]
fn validate_procedure_threshold_exceeds_signers_returns_error() {
let thresholds = vec![ProcedureThreshold::new(ProcedureName::SendAsset, 5)];
let config = MultisigConfig::new(
2,
vec![dummy_word(), dummy_word(), dummy_word()],
PsmConfig::new("http://psm:50051"),
)
.with_procedure_thresholds(thresholds);
let err = config.validate().unwrap_err();
assert!(err.contains("procedure threshold"));
assert!(err.contains("5"));
assert!(err.contains("3"));
}
#[test]
fn validate_valid_config_with_procedure_thresholds() {
let thresholds = vec![
ProcedureThreshold::new(ProcedureName::ReceiveAsset, 1),
ProcedureThreshold::new(ProcedureName::UpdateSigners, 3),
];
let config = MultisigConfig::new(
2,
vec![dummy_word(), dummy_word(), dummy_word()],
PsmConfig::new("http://psm:50051"),
)
.with_procedure_thresholds(thresholds);
assert!(config.validate().is_ok());
}
#[test]
fn validate_with_storage_mode_and_thresholds() {
let thresholds = vec![ProcedureThreshold::new(ProcedureName::ReceiveAsset, 1)];
let config = MultisigConfig::new(
2,
vec![dummy_word(), dummy_word()],
PsmConfig::new("http://psm:50051"),
)
.with_storage_mode(AccountStorageMode::Public)
.with_procedure_thresholds(thresholds);
assert!(config.validate().is_ok());
assert!(matches!(config.storage_mode, AccountStorageMode::Public));
assert_eq!(config.procedure_thresholds.len(), 1);
}
}