use guardian_client::delta_status::Status;
use guardian_shared::SignatureScheme;
use miden_client::note::NoteFile;
use miden_client::store::NoteExportType;
use miden_protocol::note::NoteId;
use miden_protocol::utils::serde::{Deserializable, Serializable};
use super::MultisigClient;
use crate::error::{MultisigError, Result};
use crate::export::{ExportedProposal, ExportedSignature};
impl MultisigClient {
pub async fn export_proposal(
&mut self,
proposal_id: &str,
path: &std::path::Path,
) -> Result<()> {
let exported = self.export_proposal_to_exported(proposal_id).await?;
let json = exported.to_json()?;
std::fs::write(path, json)
.map_err(|e| MultisigError::InvalidConfig(format!("failed to write file: {}", e)))?;
Ok(())
}
pub async fn export_proposal_to_string(&mut self, proposal_id: &str) -> Result<String> {
let exported = self.export_proposal_to_exported(proposal_id).await?;
exported.to_json()
}
async fn export_proposal_to_exported(&mut self, proposal_id: &str) -> Result<ExportedProposal> {
let account = self.require_account()?.clone();
let account_id = account.id();
let mut guardian_client = self.create_authenticated_guardian_client().await?;
let response = guardian_client
.get_delta_proposal(&account_id, proposal_id)
.await
.map_err(|e| MultisigError::GuardianServer(format!("failed to get proposal: {}", e)))?;
let raw_proposal = response
.proposal
.as_ref()
.ok_or_else(|| MultisigError::ProposalNotFound(proposal_id.to_string()))?;
Self::ensure_proposal_account_id(&raw_proposal.account_id, &account_id)?;
let proposal = crate::proposal::Proposal::from(raw_proposal)?;
self.verify_proposal_summary_binding(&proposal).await?;
let status = raw_proposal.status.as_ref().ok_or_else(|| {
MultisigError::GuardianServer(format!("proposal {} has no status field", proposal_id))
})?;
let status_oneof = status.status.as_ref().ok_or_else(|| {
MultisigError::GuardianServer(format!("proposal {} has empty status", proposal_id))
})?;
let pending = match status_oneof {
Status::Pending(p) => p,
_ => {
return Err(MultisigError::GuardianServer(format!(
"proposal {} is not in pending state",
proposal_id
)));
}
};
let mut signatures = Vec::new();
for cosigner_sig in pending.cosigner_sigs.iter() {
if let Some(ref sig) = cosigner_sig.signature {
let scheme = if sig.scheme.eq_ignore_ascii_case("ecdsa") {
SignatureScheme::Ecdsa
} else {
SignatureScheme::Falcon
};
signatures.push(ExportedSignature {
signer_commitment: cosigner_sig.signer_id.clone(),
signature: sig.signature.clone(),
scheme,
public_key_hex: sig.public_key.clone(),
});
}
}
let exported =
ExportedProposal::from_proposal(&proposal, account_id)?.with_signatures(signatures);
Ok(exported)
}
pub async fn import_proposal(&mut self, path: &std::path::Path) -> Result<ExportedProposal> {
let json = std::fs::read_to_string(path)
.map_err(|e| MultisigError::InvalidConfig(format!("failed to read file: {}", e)))?;
self.import_proposal_from_string(&json).await
}
pub async fn import_proposal_from_string(&mut self, json: &str) -> Result<ExportedProposal> {
let exported = ExportedProposal::from_json(json)?;
exported.validate(self.account.as_ref().map(|account| account.id()))?;
let proposal = exported.to_proposal()?;
self.verify_proposal_summary_binding(&proposal).await?;
Ok(exported)
}
pub async fn export_note_to_file(&self, note_id: &str, path: &std::path::Path) -> Result<()> {
let bytes = self.export_note_to_bytes(note_id).await?;
tokio::fs::write(path, bytes)
.await
.map_err(|e| MultisigError::InvalidConfig(format!("failed to write file: {}", e)))?;
Ok(())
}
pub async fn export_note_to_bytes(&self, note_id: &str) -> Result<Vec<u8>> {
let note_id = NoteId::try_from_hex(note_id.trim())
.map_err(|e| MultisigError::InvalidConfig(format!("invalid note id: {}", e)))?;
let record = self
.miden_client
.get_output_note(note_id)
.await
.map_err(|e| MultisigError::MidenClient(format!("failed to get output note: {}", e)))?
.ok_or_else(|| {
MultisigError::MidenClient(format!(
"output note {} not found in the local store; only notes created by \
this client can be exported",
note_id.to_hex()
))
})?;
let export_type = if record.inclusion_proof().is_some() {
NoteExportType::NoteWithProof
} else {
NoteExportType::NoteDetails
};
let note_file = record.into_note_file(&export_type).map_err(|e| {
MultisigError::MidenClient(format!("failed to convert note for export: {}", e))
})?;
Ok(note_file.to_bytes())
}
pub async fn import_note_from_file(&mut self, path: &std::path::Path) -> Result<String> {
let bytes = tokio::fs::read(path)
.await
.map_err(|e| MultisigError::InvalidConfig(format!("failed to read file: {}", e)))?;
self.import_note_from_bytes(&bytes).await
}
pub async fn import_note_from_bytes(&mut self, bytes: &[u8]) -> Result<String> {
let note_file = NoteFile::read_from_bytes(bytes).map_err(|e| {
MultisigError::InvalidConfig(format!("failed to decode note file: {}", e))
})?;
let known_id = note_file_note_id(¬e_file);
let commitments = self
.miden_client
.import_notes(std::slice::from_ref(¬e_file))
.await
.map_err(|e| MultisigError::MidenClient(format!("failed to import note: {}", e)))?;
match known_id {
Some(id) => Ok(id),
None => commitments
.first()
.map(|c| format!("0x{}", hex::encode(c.to_bytes())))
.ok_or_else(|| {
MultisigError::MidenClient("note import reported no imported notes".to_string())
}),
}
}
}
fn note_file_note_id(note_file: &NoteFile) -> Option<String> {
match note_file {
NoteFile::NoteId(id) => Some(id.to_hex()),
NoteFile::NoteWithProof(note, _) => Some(note.id().to_hex()),
NoteFile::NoteDetails { .. } => None,
}
}
#[cfg(test)]
mod tests {
use miden_protocol::Word;
use miden_protocol::account::AccountId;
use miden_protocol::block::BlockNumber;
use miden_protocol::crypto::rand::RandomCoin;
use miden_protocol::note::{Note, NoteType};
use miden_standards::note::P2idNote;
use super::*;
fn build_test_note() -> Note {
let sender = AccountId::from_hex("0x7b7b7b7a7b7b7b017b7b7b7b7b7b7b").unwrap();
let target = AccountId::from_hex("0x1b1b1b1a1b1b1b011b1b1b1b1b1b1b").unwrap();
let mut rng = RandomCoin::new(Word::default());
P2idNote::create(
sender,
target,
vec![],
NoteType::Private,
Default::default(),
&mut rng,
)
.unwrap()
}
#[test]
fn note_file_note_id_by_variant() {
let note = build_test_note();
let expected = note.id().to_hex();
let id_file = NoteFile::NoteId(note.id());
assert_eq!(note_file_note_id(&id_file), Some(expected));
let details_file = NoteFile::NoteDetails {
details: note.into(),
after_block_num: BlockNumber::from(0u32),
tag: None,
};
assert_eq!(note_file_note_id(&details_file), None);
}
#[test]
fn note_file_roundtrips_through_bytes() {
let note = build_test_note();
let file = NoteFile::NoteDetails {
details: note.into(),
after_block_num: BlockNumber::from(7u32),
tag: None,
};
let bytes = file.to_bytes();
let decoded = NoteFile::read_from_bytes(&bytes).unwrap();
match decoded {
NoteFile::NoteDetails {
after_block_num, ..
} => assert_eq!(after_block_num, BlockNumber::from(7u32)),
_ => panic!("expected details variant"),
}
assert!(NoteFile::read_from_bytes(b"not a note file").is_err());
}
}