use std::collections::HashSet;
use guardian_shared::ToJson;
use super::MultisigClient;
use crate::error::{MultisigError, Result};
use crate::execution::{SignatureInput, build_final_transaction_request, collect_signature_advice};
use crate::export::{EXPORT_VERSION, ExportedMetadata, ExportedProposal, ExportedSignature};
use crate::guardian_endpoint::verify_endpoint_commitment;
use crate::keystore::proposal_public_key_hex;
use crate::proposal::TransactionType;
impl MultisigClient {
pub async fn create_proposal_offline(
&mut self,
transaction_type: TransactionType,
) -> Result<ExportedProposal> {
self.sync_network_only().await?;
let account = self.require_account()?.clone();
let account_id = account.id();
let signatures_required =
account.effective_threshold_for_transaction(&transaction_type)? as usize;
let salt = crate::transaction::generate_salt();
let (new_endpoint, new_commitment) = match &transaction_type {
TransactionType::SwitchGuardian {
new_endpoint,
new_commitment,
} => {
verify_endpoint_commitment(new_endpoint, *new_commitment).await?;
(new_endpoint.clone(), *new_commitment)
}
_ => {
return Err(MultisigError::OfflineUnsupportedTransaction(
transaction_type.type_name().to_string(),
));
}
};
let tx_request = crate::transaction::build_update_guardian_transaction_request(
new_commitment,
salt,
std::iter::empty(),
)?;
let metadata = ExportedMetadata {
proposal_type: "switch_guardian".to_string(),
salt_hex: Some(crate::transaction::word_to_hex(&salt)),
new_guardian_pubkey_hex: Some(crate::transaction::word_to_hex(&new_commitment)),
new_guardian_endpoint: Some(new_endpoint),
..Default::default()
};
let tx_summary =
crate::transaction::execute_for_summary(&mut self.miden_client, account_id, tx_request)
.await?;
let tx_commitment = tx_summary.to_commitment();
let signature_hex = self.key_manager.sign_word_hex(tx_commitment);
let id = crate::transaction::word_to_hex(&tx_commitment);
let exported = ExportedProposal {
version: EXPORT_VERSION,
account_id: account_id.to_string(),
id,
nonce: account.nonce() + 1,
tx_summary: tx_summary.to_json(),
signatures: vec![ExportedSignature {
signer_commitment: self.key_manager.commitment_hex(),
signature: signature_hex,
scheme: self.key_manager.scheme(),
public_key_hex: proposal_public_key_hex(self.key_manager.as_ref()),
}],
signatures_required,
metadata,
};
Ok(exported)
}
pub async fn sign_imported_proposal(&mut self, proposal: &mut ExportedProposal) -> Result<()> {
let bound_proposal = proposal.to_proposal()?;
if !bound_proposal.transaction_type.supports_offline_execution() {
return Err(MultisigError::OfflineUnsupportedTransaction(
bound_proposal.transaction_type.type_name().to_string(),
));
}
self.verify_proposal_summary_binding(&bound_proposal)
.await?;
let account = self.require_account()?;
let account_id = account.id();
proposal.validate(Some(account_id))?;
let user_commitment = self.key_manager.commitment();
if !account.is_cosigner(&user_commitment) {
return Err(MultisigError::NotCosigner);
}
Self::ensure_proposal_account_id(&proposal.account_id, &account_id)?;
let user_commitment_hex = self.key_manager.commitment_hex();
if proposal.signatures.iter().any(|s| {
s.signer_commitment
.eq_ignore_ascii_case(&user_commitment_hex)
}) {
return Err(MultisigError::AlreadySigned);
}
let tx_commitment = bound_proposal.tx_summary.to_commitment();
let signature_hex = self.key_manager.sign_word_hex(tx_commitment);
proposal.add_signature(ExportedSignature {
signer_commitment: user_commitment_hex,
signature: signature_hex,
scheme: self.key_manager.scheme(),
public_key_hex: proposal_public_key_hex(self.key_manager.as_ref()),
})?;
Ok(())
}
pub async fn execute_imported_proposal(&mut self, exported: &ExportedProposal) -> Result<()> {
self.sync_network_only().await?;
let account = self.require_account()?.clone();
let account_id = account.id();
exported.validate(Some(account_id))?;
if !exported.is_ready() {
return Err(MultisigError::ProposalNotReady {
collected: exported.signatures_collected(),
required: exported.signatures_required,
});
}
let proposal = exported.to_proposal()?;
if !proposal.transaction_type.supports_offline_execution() {
return Err(MultisigError::OfflineUnsupportedTransaction(
proposal.transaction_type.type_name().to_string(),
));
}
self.verify_proposal_summary_binding(&proposal).await?;
let tx_summary = proposal.tx_summary.clone();
let tx_summary_commitment = tx_summary.to_commitment();
let signature_inputs: Vec<SignatureInput> = exported
.signatures
.iter()
.map(|sig| SignatureInput {
signer_commitment: sig.signer_commitment.clone(),
signature_hex: sig.signature.clone(),
scheme: sig.scheme,
public_key_hex: sig.public_key_hex.clone(),
})
.collect();
let required_commitments: HashSet<String> =
account.cosigner_commitments_hex().into_iter().collect();
let signature_advice = collect_signature_advice(
signature_inputs,
&required_commitments,
tx_summary_commitment,
)?;
let salt = proposal.metadata.salt()?;
let final_tx_request = build_final_transaction_request(
&self.miden_client,
&proposal.transaction_type,
account.inner(),
salt,
signature_advice,
None,
None,
self.key_manager.scheme(),
)
.await?;
self.finalize_transaction(account_id, final_tx_request, &proposal.transaction_type)
.await
}
}