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