Skip to main content

onemoney_protocol/api/
multisig.rs

1//! Multi-signature account API endpoints.
2//!
3//! This module provides API methods for creating and managing multi-signature
4//! accounts.
5
6use alloy_primitives::{Address, SignatureError, U256};
7use om_primitives_types::{
8    core::types::Money,
9    transaction::{
10        MultiSigSignatureEntry, Signature, Signed,
11        envelope::RawTransactionEnvelope,
12        payload::{CreateMultiSigPayload, MultiSigSigner, PaymentPayload, TokenIssuePayload, TokenMintPayload},
13    },
14};
15
16use crate::{
17    Client, Error, Result,
18    crypto::sign_transaction_payload,
19    utils::{SignerConfig, ThresholdConfig, derive_multisig_address},
20};
21
22impl Client {
23    /// Create a multi-signature account transaction payload.
24    ///
25    /// This method derives the multi-sig account address and creates the
26    /// payload. The caller must sign this payload and submit via the
27    /// standard transaction API.
28    ///
29    /// # Arguments
30    /// * `signers` - List of authorized signers with their weights
31    /// * `threshold` - Minimum total weight required for transaction approval
32    /// * `chain_id` - Chain ID for the transaction
33    /// * `nonce` - Transaction nonce (get from account state)
34    ///
35    /// # Returns
36    /// Tuple of (multi-sig account address, unsigned transaction payload)
37    ///
38    /// # Errors
39    /// Returns error if signer configuration is invalid (e.g., threshold
40    /// exceeds total weight)
41    ///
42    /// # Example
43    /// ```no_run
44    /// use onemoney_protocol::{
45    ///     Client,
46    ///     NamedChain
47    ///     utils::{SignerConfig, ThresholdConfig},
48    /// };
49    ///
50    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
51    /// let client = Client::testnet()?;
52    ///
53    /// let signer1 = SignerConfig::new(vec![2; 33], 1)?;
54    /// let signer2 = SignerConfig::new(vec![3; 33], 1)?;
55    /// let threshold = ThresholdConfig::new(2)?;
56    ///
57    /// let (_multisig_address, payload) = client.create_multisig_account_payload(
58    ///     &[signer1, signer2],
59    ///     &threshold,
60    ///     NamedChain::MAINNET_CHAIN_ID, // chain_id
61    ///     0,     // nonce
62    /// )?;
63    ///
64    /// println!("Payload created: {:?}", payload);
65    /// # Ok(())
66    /// # }
67    /// ```
68    pub fn create_multisig_account_payload(
69        &self,
70        signers: &[SignerConfig],
71        threshold: &ThresholdConfig,
72        chain_id: u64,
73        nonce: u64,
74    ) -> Result<(Address, CreateMultiSigPayload)> {
75        // Derive the multi-sig account address
76        let multisig_address = derive_multisig_address(signers, threshold).map_err(|e| Error::InvalidParameter {
77            parameter: "signers/threshold".to_string(),
78            message: format!("Invalid multi-sig configuration: {}", e),
79        })?;
80
81        // Convert to wire format
82        let wire_signers: Vec<MultiSigSigner> = signers
83            .iter()
84            .map(|s| MultiSigSigner {
85                public_key: s.public_key.clone(),
86                weight: s.weight,
87            })
88            .collect();
89
90        // Create payload
91        let payload = CreateMultiSigPayload {
92            chain_id,
93            nonce,
94            signers: wire_signers,
95            threshold: threshold.threshold,
96        };
97
98        Ok((multisig_address, payload))
99    }
100
101    /// Create and submit a multi-signature account creation transaction.
102    ///
103    /// This is a convenience method that creates the payload, signs it, and
104    /// submits it.
105    ///
106    /// # Arguments
107    /// * `signers` - List of authorized signers with their weights
108    /// * `threshold` - Minimum total weight required for transaction approval
109    /// * `chain_id` - Chain ID for the transaction
110    /// * `nonce` - Transaction nonce (get from creator's account state)
111    /// * `private_key` - Creator's private key (pays for account creation)
112    ///
113    /// # Returns
114    /// Tuple of (multi-sig account address, transaction hash)
115    pub async fn submit_create_multisig_account(
116        &self,
117        signers: &[SignerConfig],
118        threshold: &ThresholdConfig,
119        chain_id: u64,
120        nonce: u64,
121        private_key: &str,
122    ) -> Result<(Address, om_rest_types::responses::TransactionResponse)> {
123        // Create payload
124        let (multisig_address, payload) = self.create_multisig_account_payload(signers, threshold, chain_id, nonce)?;
125
126        // Sign the payload
127        let rest_signature = sign_transaction_payload(&payload, private_key)?;
128        let signature: Signature = rest_signature
129            .try_into()
130            .map_err(|e: SignatureError| Error::invalid_parameter("signature", e.to_string()))?;
131
132        // Create signed transaction and envelope
133        let signed_tx = Signed::new(payload, signature);
134        let envelope = RawTransactionEnvelope::CreateMultiSig(signed_tx);
135
136        let response = self.submit_raw_transaction(envelope).await?;
137
138        Ok((multisig_address, response))
139    }
140
141    /// Submit a multi-signature payment transaction.
142    ///
143    /// This method assumes signatures have already been collected off-chain.
144    pub async fn submit_multisig_payment_transaction(
145        &self,
146        payload: PaymentPayload,
147        multisig_account: Address,
148        signatures: Vec<MultiSigSignatureEntry>,
149    ) -> Result<om_rest_types::responses::TransactionResponse> {
150        let signed_tx = Signed::new_multi_sig(payload, multisig_account, signatures);
151        let envelope = RawTransactionEnvelope::Payment {
152            signed: signed_tx,
153            fee: Money::ZERO,
154        };
155        self.submit_raw_transaction(envelope).await
156    }
157
158    /// Submit a multi-signature token issue transaction.
159    ///
160    /// This method assumes signatures have already been collected off-chain.
161    pub async fn submit_multisig_token_issue_transaction(
162        &self,
163        payload: TokenIssuePayload,
164        multisig_account: Address,
165        signatures: Vec<MultiSigSignatureEntry>,
166    ) -> Result<om_rest_types::responses::TransactionResponse> {
167        let mint_address = payload.derive_mint_address();
168        let signed_tx = Signed::new_multi_sig(payload, multisig_account, signatures);
169        let envelope = RawTransactionEnvelope::TokenIssue(signed_tx, mint_address);
170        self.submit_raw_transaction(envelope).await
171    }
172
173    /// Submit a multi-signature token mint transaction.
174    ///
175    /// This method assumes signatures have already been collected off-chain.
176    pub async fn submit_multisig_token_mint_transaction(
177        &self,
178        payload: TokenMintPayload,
179        multisig_account: Address,
180        signatures: Vec<MultiSigSignatureEntry>,
181    ) -> Result<om_rest_types::responses::TransactionResponse> {
182        let signed_tx = Signed::new_multi_sig(payload, multisig_account, signatures);
183        let envelope = RawTransactionEnvelope::TokenMint(signed_tx);
184        self.submit_raw_transaction(envelope).await
185    }
186
187    /// Create a multi-signature payment transaction.
188    ///
189    /// This helper creates a payment transaction payload that needs to be
190    /// signed by multiple signers of a multi-sig account.
191    ///
192    /// # Workflow
193    /// 1. Create payment payload with this method
194    /// 2. Each signer signs the payload independently (offline signing
195    ///    supported)
196    /// 3. Collect signatures using `MultiSigSignatureCollector`
197    /// 4. Create signed transaction with `Signed::new_multi_sig()`
198    /// 5. Submit transaction
199    ///
200    /// # Arguments
201    /// * `recipient` - Recipient address
202    /// * `token` - Token mint address
203    /// * `amount` - Amount to send
204    /// * `chain_id` - Chain ID
205    /// * `nonce` - Multi-sig account's nonce
206    ///
207    /// # Returns
208    /// Unsigned payment payload ready for signing
209    ///
210    /// # Example
211    /// ```no_run
212    /// use alloy_primitives::{Address, U256};
213    /// use onemoney_protocol::{
214    ///     Client, NamedChain, crypto::sign_multisig_transaction_payload,
215    ///     utils::MultiSigSignatureCollector,
216    /// };
217    ///
218    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
219    /// let client = Client::testnet()?;
220    ///
221    /// // Create payment payload
222    /// let multisig_account = Address::ZERO; // Your multi-sig account
223    /// let recipient = Address::ZERO;
224    /// let payload = client.create_multisig_payment_payload(
225    ///     recipient,
226    ///     Address::repeat_byte(1),
227    ///     U256::from(1000),
228    ///     NamedChain::MAINNET_CHAIN_ID, // chain_id
229    ///     5,                            // nonce
230    /// );
231    ///
232    /// // Collect signatures from signers
233    /// let mut collector = MultiSigSignatureCollector::new();
234    ///
235    /// // Signer 1 signs (can be offline)
236    /// let sig1 =
237    ///     sign_multisig_transaction_payload(&payload, multisig_account, "signer1_private_key")?;
238    /// collector.add_signature(signer1_pubkey, sig1);
239    ///
240    /// // Signer 2 signs (can be offline)
241    /// let sig2 =
242    ///     sign_multisig_transaction_payload(&payload, multisig_account, "signer2_private_key")?;
243    /// collector.add_signature(signer2_pubkey, sig2);
244    ///
245    /// // Create multi-sig transaction
246    /// let signatures = collector.signatures();
247    /// let signed_tx = om_primitives_types::transaction::Signed::new_multi_sig(
248    ///     payload,
249    ///     multisig_account,
250    ///     signatures,
251    /// );
252    ///
253    /// // Submit via RawTransactionEnvelope::Payment...
254    /// # Ok(())
255    /// # }
256    /// ```
257    pub fn create_multisig_payment_payload(
258        &self,
259        recipient: Address,
260        token: Address,
261        amount: U256,
262        chain_id: u64,
263        nonce: u64,
264    ) -> PaymentPayload {
265        PaymentPayload {
266            chain_id,
267            nonce,
268            token,
269            recipient,
270            value: amount,
271        }
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278    use crate::NamedChain;
279
280    #[test]
281    fn test_create_multisig_account_payload() {
282        let client = Client::local().unwrap();
283
284        let signer1 = SignerConfig::new(vec![2; 33], 1).unwrap();
285        let signer2 = SignerConfig::new(vec![3; 33], 1).unwrap();
286        let threshold = ThresholdConfig::new(2).unwrap();
287
288        let result = client.create_multisig_account_payload(&[signer1, signer2], &threshold, 1, 0);
289        assert!(result.is_ok());
290
291        let (address, payload) = result.unwrap();
292        assert_eq!(payload.signers.len(), 2);
293        assert_eq!(payload.threshold, 2);
294        assert_ne!(address, Address::ZERO);
295    }
296
297    #[test]
298    fn test_create_multisig_account_invalid_threshold() {
299        let client = Client::local().unwrap();
300
301        let signer1 = SignerConfig::new(vec![2; 33], 1).unwrap();
302        let threshold = ThresholdConfig::new(2).unwrap(); // Exceeds total weight
303
304        let result = client.create_multisig_account_payload(&[signer1], &threshold, 1, 0);
305        assert!(result.is_err());
306    }
307
308    #[test]
309    fn test_create_multisig_payment_payload() {
310        use alloy_primitives::U256;
311
312        let client = Client::local().unwrap();
313
314        let recipient = Address::ZERO;
315        let payload = client.create_multisig_payment_payload(
316            recipient,
317            Address::ZERO,
318            U256::from(1000),
319            NamedChain::TESTNET_CHAIN_ID,
320            5,
321        );
322
323        assert_eq!(payload.chain_id, NamedChain::TESTNET_CHAIN_ID);
324        assert_eq!(payload.nonce, 5);
325        assert_eq!(payload.recipient, recipient);
326        assert_eq!(payload.value, U256::from(1000));
327    }
328}