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}