1use crate::error::BitcoinError;
10use bitcoin::{Address, Amount, OutPoint, Transaction, TxIn, TxOut};
11use chrono::{DateTime, Utc};
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use std::str::FromStr;
15use uuid::Uuid;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
19pub enum SessionState {
20 Registration,
22 InputCollection,
24 OutputCollection,
26 Signing,
28 Broadcasting,
30 Completed,
32 Failed,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct SessionConfig {
39 pub min_participants: usize,
41 pub max_participants: usize,
43 pub denomination: u64,
45 pub coordinator_fee: u64,
47 pub mining_fee_per_participant: u64,
49 pub registration_timeout: u64,
51 pub signing_timeout: u64,
53}
54
55impl Default for SessionConfig {
56 fn default() -> Self {
57 Self {
58 min_participants: 3,
59 max_participants: 100,
60 denomination: 100_000, coordinator_fee: 1000, mining_fee_per_participant: 500, registration_timeout: 600, signing_timeout: 300, }
66 }
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct Participant {
72 pub id: Uuid,
74 pub input: ParticipantInput,
76 pub output_address: String,
78 pub change_address: Option<String>,
80 pub registered_at: DateTime<Utc>,
82 pub has_signed: bool,
84}
85
86impl Participant {
87 pub fn get_output_address(&self) -> Result<Address, BitcoinError> {
89 Address::from_str(&self.output_address)
90 .map_err(|e| BitcoinError::InvalidAddress(e.to_string()))
91 .map(|a| a.assume_checked())
92 }
93
94 pub fn get_change_address(&self) -> Result<Option<Address>, BitcoinError> {
96 self.change_address
97 .as_ref()
98 .map(|addr| {
99 Address::from_str(addr)
100 .map_err(|e| BitcoinError::InvalidAddress(e.to_string()))
101 .map(|a| a.assume_checked())
102 })
103 .transpose()
104 }
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct ParticipantInput {
110 pub outpoint: OutPoint,
112 pub value: u64,
114 pub script_pubkey: Vec<u8>,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct CoinJoinSession {
121 pub id: Uuid,
123 pub config: SessionConfig,
125 pub state: SessionState,
127 pub participants: Vec<Participant>,
129 pub created_at: DateTime<Utc>,
131 pub transaction: Option<Transaction>,
133 pub signatures: HashMap<Uuid, Vec<u8>>,
135}
136
137impl CoinJoinSession {
138 pub fn new(config: SessionConfig) -> Self {
140 Self {
141 id: Uuid::new_v4(),
142 config,
143 state: SessionState::Registration,
144 participants: Vec::new(),
145 created_at: Utc::now(),
146 transaction: None,
147 signatures: HashMap::new(),
148 }
149 }
150
151 pub fn add_participant(
153 &mut self,
154 input: ParticipantInput,
155 output_address: Address,
156 change_address: Option<Address>,
157 ) -> Result<Uuid, BitcoinError> {
158 if self.state != SessionState::Registration {
159 return Err(BitcoinError::Validation(
160 "Session is not in registration state".to_string(),
161 ));
162 }
163
164 if self.participants.len() >= self.config.max_participants {
165 return Err(BitcoinError::Validation("Session is full".to_string()));
166 }
167
168 let required_input = self.config.denomination
170 + self.config.coordinator_fee
171 + self.config.mining_fee_per_participant;
172
173 if input.value < required_input {
174 return Err(BitcoinError::Validation(format!(
175 "Input value {} is less than required {}",
176 input.value, required_input
177 )));
178 }
179
180 let participant = Participant {
181 id: Uuid::new_v4(),
182 input,
183 output_address: output_address.to_string(),
184 change_address: change_address.map(|a| a.to_string()),
185 registered_at: Utc::now(),
186 has_signed: false,
187 };
188
189 let participant_id = participant.id;
190 self.participants.push(participant);
191
192 if self.participants.len() >= self.config.min_participants {
194 self.state = SessionState::InputCollection;
195 }
196
197 Ok(participant_id)
198 }
199
200 pub fn build_transaction(&mut self) -> Result<(), BitcoinError> {
202 if self.state != SessionState::InputCollection {
203 return Err(BitcoinError::Validation(
204 "Cannot build transaction in current state".to_string(),
205 ));
206 }
207
208 if self.participants.len() < self.config.min_participants {
209 return Err(BitcoinError::Validation(
210 "Not enough participants".to_string(),
211 ));
212 }
213
214 let mut inputs = Vec::new();
216 for participant in &self.participants {
217 inputs.push(TxIn {
218 previous_output: participant.input.outpoint,
219 script_sig: bitcoin::blockdata::script::ScriptBuf::new(),
220 sequence: bitcoin::Sequence::MAX,
221 witness: bitcoin::Witness::new(),
222 });
223 }
224
225 let mut outputs = Vec::new();
227
228 for participant in &self.participants {
230 let addr = participant.get_output_address()?;
231 outputs.push(TxOut {
232 value: Amount::from_sat(self.config.denomination),
233 script_pubkey: addr.script_pubkey(),
234 });
235 }
236
237 for participant in &self.participants {
239 let total_input = participant.input.value;
240 let total_output = self.config.denomination
241 + self.config.coordinator_fee
242 + self.config.mining_fee_per_participant;
243
244 let change = total_input.saturating_sub(total_output);
245 if change > 0 {
246 if let Some(change_addr) = participant.get_change_address()? {
247 outputs.push(TxOut {
248 value: Amount::from_sat(change),
249 script_pubkey: change_addr.script_pubkey(),
250 });
251 }
252 }
253 }
254
255 let transaction = Transaction {
259 version: bitcoin::transaction::Version::TWO,
260 lock_time: bitcoin::blockdata::locktime::absolute::LockTime::ZERO,
261 input: inputs,
262 output: outputs,
263 };
264
265 self.transaction = Some(transaction);
266 self.state = SessionState::Signing;
267
268 Ok(())
269 }
270
271 pub fn add_signature(
273 &mut self,
274 participant_id: Uuid,
275 signature: Vec<u8>,
276 ) -> Result<(), BitcoinError> {
277 if self.state != SessionState::Signing {
278 return Err(BitcoinError::Validation(
279 "Session is not in signing state".to_string(),
280 ));
281 }
282
283 let participant = self
285 .participants
286 .iter_mut()
287 .find(|p| p.id == participant_id)
288 .ok_or_else(|| BitcoinError::Validation("Participant not found".to_string()))?;
289
290 participant.has_signed = true;
291 self.signatures.insert(participant_id, signature);
292
293 if self.participants.iter().all(|p| p.has_signed) {
295 self.state = SessionState::Broadcasting;
296 }
297
298 Ok(())
299 }
300
301 pub fn signature_count(&self) -> usize {
303 self.signatures.len()
304 }
305
306 pub fn is_ready_to_broadcast(&self) -> bool {
308 self.state == SessionState::Broadcasting && self.signatures.len() == self.participants.len()
309 }
310
311 pub fn complete(&mut self) {
313 self.state = SessionState::Completed;
314 }
315
316 pub fn fail(&mut self) {
318 self.state = SessionState::Failed;
319 }
320}
321
322pub struct CoinJoinCoordinator {
324 sessions: HashMap<Uuid, CoinJoinSession>,
326 default_config: SessionConfig,
328}
329
330impl CoinJoinCoordinator {
331 pub fn new(default_config: SessionConfig) -> Self {
333 Self {
334 sessions: HashMap::new(),
335 default_config,
336 }
337 }
338
339 pub fn create_session(&mut self, config: Option<SessionConfig>) -> Uuid {
341 let session = CoinJoinSession::new(config.unwrap_or_else(|| self.default_config.clone()));
342 let session_id = session.id;
343 self.sessions.insert(session_id, session);
344 session_id
345 }
346
347 pub fn get_session(&self, session_id: &Uuid) -> Option<&CoinJoinSession> {
349 self.sessions.get(session_id)
350 }
351
352 pub fn get_session_mut(&mut self, session_id: &Uuid) -> Option<&mut CoinJoinSession> {
354 self.sessions.get_mut(session_id)
355 }
356
357 pub fn join_session(
359 &mut self,
360 session_id: Uuid,
361 input: ParticipantInput,
362 output_address: Address,
363 change_address: Option<Address>,
364 ) -> Result<Uuid, BitcoinError> {
365 let session = self
366 .sessions
367 .get_mut(&session_id)
368 .ok_or_else(|| BitcoinError::Validation("Session not found".to_string()))?;
369
370 session.add_participant(input, output_address, change_address)
371 }
372
373 pub fn list_active_sessions(&self) -> Vec<&CoinJoinSession> {
375 self.sessions
376 .values()
377 .filter(|s| {
378 matches!(
379 s.state,
380 SessionState::Registration
381 | SessionState::InputCollection
382 | SessionState::OutputCollection
383 | SessionState::Signing
384 )
385 })
386 .collect()
387 }
388
389 pub fn cleanup_old_sessions(&mut self, max_age_secs: i64) {
391 let now = Utc::now();
392 self.sessions.retain(|_, session| {
393 let age = now.signed_duration_since(session.created_at).num_seconds();
394 age < max_age_secs || session.state == SessionState::Broadcasting
395 });
396 }
397}
398
399pub struct CoinJoinClient {
401 participant_id: Option<Uuid>,
403}
404
405impl CoinJoinClient {
406 pub fn new() -> Self {
408 Self {
409 participant_id: None,
410 }
411 }
412
413 pub fn register(
415 &mut self,
416 coordinator: &mut CoinJoinCoordinator,
417 session_id: Uuid,
418 input: ParticipantInput,
419 output_address: Address,
420 change_address: Option<Address>,
421 ) -> Result<Uuid, BitcoinError> {
422 let participant_id =
423 coordinator.join_session(session_id, input, output_address, change_address)?;
424
425 self.participant_id = Some(participant_id);
426 Ok(participant_id)
427 }
428
429 pub fn submit_signature(
431 &self,
432 coordinator: &mut CoinJoinCoordinator,
433 session_id: Uuid,
434 signature: Vec<u8>,
435 ) -> Result<(), BitcoinError> {
436 let participant_id = self
437 .participant_id
438 .ok_or_else(|| BitcoinError::Validation("Not registered".to_string()))?;
439
440 let session = coordinator
441 .get_session_mut(&session_id)
442 .ok_or_else(|| BitcoinError::Validation("Session not found".to_string()))?;
443
444 session.add_signature(participant_id, signature)
445 }
446}
447
448impl Default for CoinJoinClient {
449 fn default() -> Self {
450 Self::new()
451 }
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457 use bitcoin::Txid;
458 use bitcoin::hashes::Hash;
459 use std::str::FromStr;
460
461 #[test]
462 fn test_session_creation() {
463 let config = SessionConfig::default();
464 let session = CoinJoinSession::new(config);
465
466 assert_eq!(session.state, SessionState::Registration);
467 assert_eq!(session.participants.len(), 0);
468 }
469
470 #[test]
471 fn test_add_participant() {
472 let mut session = CoinJoinSession::new(SessionConfig::default());
473 let address = Address::from_str("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4")
474 .unwrap()
475 .assume_checked();
476
477 let input = ParticipantInput {
478 outpoint: OutPoint {
479 txid: Txid::all_zeros(),
480 vout: 0,
481 },
482 value: 200_000,
483 script_pubkey: vec![],
484 };
485
486 let result = session.add_participant(input, address, None);
487 assert!(result.is_ok());
488 assert_eq!(session.participants.len(), 1);
489 }
490
491 #[test]
492 fn test_coordinator() {
493 let mut coordinator = CoinJoinCoordinator::new(SessionConfig::default());
494 let session_id = coordinator.create_session(None);
495
496 assert!(coordinator.get_session(&session_id).is_some());
497 }
498
499 #[test]
500 fn test_session_state_progression() {
501 let mut session = CoinJoinSession::new(SessionConfig {
502 min_participants: 2,
503 ..Default::default()
504 });
505
506 assert_eq!(session.state, SessionState::Registration);
507
508 let address = Address::from_str("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4")
509 .unwrap()
510 .assume_checked();
511
512 let input1 = ParticipantInput {
514 outpoint: OutPoint {
515 txid: Txid::all_zeros(),
516 vout: 0,
517 },
518 value: 200_000,
519 script_pubkey: vec![],
520 };
521 session
522 .add_participant(input1, address.clone(), None)
523 .unwrap();
524
525 assert_eq!(session.state, SessionState::Registration);
527
528 let input2 = ParticipantInput {
530 outpoint: OutPoint {
531 txid: Txid::all_zeros(),
532 vout: 1,
533 },
534 value: 200_000,
535 script_pubkey: vec![],
536 };
537 session.add_participant(input2, address, None).unwrap();
538
539 assert_eq!(session.state, SessionState::InputCollection);
541 }
542
543 #[test]
544 fn test_insufficient_input_value() {
545 let mut session = CoinJoinSession::new(SessionConfig::default());
546 let address = Address::from_str("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4")
547 .unwrap()
548 .assume_checked();
549
550 let input = ParticipantInput {
551 outpoint: OutPoint {
552 txid: Txid::all_zeros(),
553 vout: 0,
554 },
555 value: 1000, script_pubkey: vec![],
557 };
558
559 let result = session.add_participant(input, address, None);
560 assert!(result.is_err());
561 }
562
563 #[test]
564 fn test_coinjoin_client() {
565 let mut client = CoinJoinClient::new();
566 let mut coordinator = CoinJoinCoordinator::new(SessionConfig::default());
567 let session_id = coordinator.create_session(None);
568
569 let address = Address::from_str("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4")
570 .unwrap()
571 .assume_checked();
572
573 let input = ParticipantInput {
574 outpoint: OutPoint {
575 txid: Txid::all_zeros(),
576 vout: 0,
577 },
578 value: 200_000,
579 script_pubkey: vec![],
580 };
581
582 let result = client.register(&mut coordinator, session_id, input, address, None);
583 assert!(result.is_ok());
584 }
585}