Skip to main content

miden_multisig_client/client/
io.rs

1//! Export/import operations for MultisigClient.
2//!
3//! This module handles exporting proposals to files/strings and
4//! importing them back for offline sharing workflows.
5
6use guardian_client::delta_status::Status;
7use guardian_shared::SignatureScheme;
8
9use super::MultisigClient;
10use crate::error::{MultisigError, Result};
11use crate::export::{ExportedProposal, ExportedSignature};
12
13impl MultisigClient {
14    /// Exports a proposal to a file for offline sharing.
15    ///
16    /// This fetches the proposal from GUARDIAN, including all collected signatures,
17    /// and writes it to the specified file path as JSON.
18    ///
19    /// # Example
20    ///
21    /// ```ignore
22    /// client.export_proposal(&proposal_id, "/tmp/proposal.json").await?;
23    /// ```
24    pub async fn export_proposal(
25        &mut self,
26        proposal_id: &str,
27        path: &std::path::Path,
28    ) -> Result<()> {
29        let exported = self.export_proposal_to_exported(proposal_id).await?;
30        let json = exported.to_json()?;
31        std::fs::write(path, json)
32            .map_err(|e| MultisigError::InvalidConfig(format!("failed to write file: {}", e)))?;
33        Ok(())
34    }
35
36    /// Exports a proposal to a JSON string for programmatic use.
37    ///
38    /// # Example
39    ///
40    /// ```ignore
41    /// let json = client.export_proposal_to_string(&proposal_id).await?;
42    /// println!("{}", json);
43    /// ```
44    pub async fn export_proposal_to_string(&mut self, proposal_id: &str) -> Result<String> {
45        let exported = self.export_proposal_to_exported(proposal_id).await?;
46        exported.to_json()
47    }
48
49    /// Internal helper to create an ExportedProposal from GUARDIAN data.
50    ///
51    /// # Errors
52    ///
53    /// Returns an error if:
54    /// - The proposal is not found in GUARDIAN
55    /// - The raw delta cannot be found in GUARDIAN response
56    /// - The delta has no pending status with signature data
57    async fn export_proposal_to_exported(&mut self, proposal_id: &str) -> Result<ExportedProposal> {
58        let account = self.require_account()?.clone();
59        let account_id = account.id();
60        let mut guardian_client = self.create_authenticated_guardian_client().await?;
61        let response = guardian_client
62            .get_delta_proposal(&account_id, proposal_id)
63            .await
64            .map_err(|e| MultisigError::GuardianServer(format!("failed to get proposal: {}", e)))?;
65        let raw_proposal = response
66            .proposal
67            .as_ref()
68            .ok_or_else(|| MultisigError::ProposalNotFound(proposal_id.to_string()))?;
69        Self::ensure_proposal_account_id(&raw_proposal.account_id, &account_id)?;
70        let proposal = crate::proposal::Proposal::from(raw_proposal)?;
71        self.verify_proposal_summary_binding(&proposal).await?;
72
73        // Extract signatures - fail if status structure is missing
74        let status = raw_proposal.status.as_ref().ok_or_else(|| {
75            MultisigError::GuardianServer(format!("proposal {} has no status field", proposal_id))
76        })?;
77
78        let status_oneof = status.status.as_ref().ok_or_else(|| {
79            MultisigError::GuardianServer(format!("proposal {} has empty status", proposal_id))
80        })?;
81
82        let pending = match status_oneof {
83            Status::Pending(p) => p,
84            _ => {
85                return Err(MultisigError::GuardianServer(format!(
86                    "proposal {} is not in pending state",
87                    proposal_id
88                )));
89            }
90        };
91
92        let mut signatures = Vec::new();
93        for cosigner_sig in pending.cosigner_sigs.iter() {
94            if let Some(ref sig) = cosigner_sig.signature {
95                let scheme = if sig.scheme.eq_ignore_ascii_case("ecdsa") {
96                    SignatureScheme::Ecdsa
97                } else {
98                    SignatureScheme::Falcon
99                };
100                signatures.push(ExportedSignature {
101                    signer_commitment: cosigner_sig.signer_id.clone(),
102                    signature: sig.signature.clone(),
103                    scheme,
104                    public_key_hex: sig.public_key.clone(),
105                });
106            }
107        }
108
109        let exported =
110            ExportedProposal::from_proposal(&proposal, account_id)?.with_signatures(signatures);
111
112        Ok(exported)
113    }
114
115    /// Imports a proposal from a file.
116    ///
117    /// The proposal can then be signed with `sign_imported_proposal`
118    /// or executed with `execute_imported_proposal`.
119    ///
120    /// # Example
121    ///
122    /// ```ignore
123    /// let proposal = client.import_proposal("/tmp/proposal.json").await?;
124    /// println!("Imported proposal: {}", proposal.id);
125    /// ```
126    pub async fn import_proposal(&mut self, path: &std::path::Path) -> Result<ExportedProposal> {
127        let json = std::fs::read_to_string(path)
128            .map_err(|e| MultisigError::InvalidConfig(format!("failed to read file: {}", e)))?;
129        self.import_proposal_from_string(&json).await
130    }
131
132    /// Imports a proposal from a JSON string.
133    ///
134    /// # Example
135    ///
136    /// ```ignore
137    /// let proposal = client.import_proposal_from_string(&json).await?;
138    /// ```
139    pub async fn import_proposal_from_string(&mut self, json: &str) -> Result<ExportedProposal> {
140        let exported = ExportedProposal::from_json(json)?;
141        exported.validate(self.account.as_ref().map(|account| account.id()))?;
142
143        let proposal = exported.to_proposal()?;
144        self.verify_proposal_summary_binding(&proposal).await?;
145
146        Ok(exported)
147    }
148}