1use bitcoin::{
7 Address, Network, ScriptBuf,
8 bip32::{DerivationPath, Xpub},
9 script::Builder as ScriptBuilder,
10 secp256k1::{PublicKey, Secp256k1},
11};
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use std::str::FromStr;
15
16use crate::error::{BitcoinError, Result};
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct MultisigConfig {
21 pub required_signatures: u8,
23 pub total_keys: u8,
25 pub xpubs: Vec<String>,
27 pub key_labels: Vec<String>,
29 pub derivation_path: String,
31 pub network: Network,
33}
34
35impl MultisigConfig {
36 pub fn new_2of3(xpubs: Vec<String>, key_labels: Vec<String>, network: Network) -> Result<Self> {
38 if xpubs.len() != 3 {
39 return Err(BitcoinError::Wallet(
40 "2-of-3 requires exactly 3 xpubs".to_string(),
41 ));
42 }
43 if key_labels.len() != 3 {
44 return Err(BitcoinError::Wallet(
45 "2-of-3 requires exactly 3 labels".to_string(),
46 ));
47 }
48
49 Ok(Self {
50 required_signatures: 2,
51 total_keys: 3,
52 xpubs,
53 key_labels,
54 derivation_path: "m/48'/0'/0'/2'".to_string(), network,
56 })
57 }
58
59 pub fn new_custom(
61 required_signatures: u8,
62 xpubs: Vec<String>,
63 key_labels: Vec<String>,
64 network: Network,
65 ) -> Result<Self> {
66 let total_keys = xpubs.len() as u8;
67
68 if required_signatures > total_keys {
69 return Err(BitcoinError::Wallet(
70 "Required signatures cannot exceed total keys".to_string(),
71 ));
72 }
73
74 if required_signatures == 0 {
75 return Err(BitcoinError::Wallet(
76 "Required signatures must be at least 1".to_string(),
77 ));
78 }
79
80 if total_keys > 15 {
81 return Err(BitcoinError::Wallet(
82 "Maximum 15 keys supported for standard multisig".to_string(),
83 ));
84 }
85
86 if key_labels.len() != xpubs.len() {
87 return Err(BitcoinError::Wallet(
88 "Number of labels must match number of xpubs".to_string(),
89 ));
90 }
91
92 Ok(Self {
93 required_signatures,
94 total_keys,
95 xpubs,
96 key_labels,
97 derivation_path: "m/48'/0'/0'/2'".to_string(),
98 network,
99 })
100 }
101
102 pub fn validate(&self) -> Result<()> {
104 for (i, xpub) in self.xpubs.iter().enumerate() {
105 Xpub::from_str(xpub).map_err(|e| {
106 BitcoinError::InvalidXpub(format!("Invalid xpub at index {}: {}", i, e))
107 })?;
108 }
109 Ok(())
110 }
111
112 pub fn type_string(&self) -> String {
114 format!("{}-of-{}", self.required_signatures, self.total_keys)
115 }
116}
117
118pub struct MultisigWallet {
120 config: MultisigConfig,
121 xpubs: Vec<Xpub>,
123 address_cache: HashMap<u32, MultisigAddress>,
125 next_index: u32,
127}
128
129impl MultisigWallet {
130 pub fn new(config: MultisigConfig) -> Result<Self> {
132 config.validate()?;
133
134 let xpubs: Vec<Xpub> = config
135 .xpubs
136 .iter()
137 .map(|x| Xpub::from_str(x))
138 .collect::<std::result::Result<Vec<_>, _>>()
139 .map_err(|e| BitcoinError::InvalidXpub(e.to_string()))?;
140
141 Ok(Self {
142 config,
143 xpubs,
144 address_cache: HashMap::new(),
145 next_index: 0,
146 })
147 }
148
149 pub fn config(&self) -> &MultisigConfig {
151 &self.config
152 }
153
154 pub fn get_new_address(&mut self) -> Result<MultisigAddress> {
156 let index = self.next_index;
157 let address = self.derive_address(index, false)?;
158 self.address_cache.insert(index, address.clone());
159 self.next_index += 1;
160 Ok(address)
161 }
162
163 pub fn get_address(&mut self, index: u32) -> Result<MultisigAddress> {
165 if let Some(cached) = self.address_cache.get(&index) {
166 return Ok(cached.clone());
167 }
168
169 let address = self.derive_address(index, false)?;
170 self.address_cache.insert(index, address.clone());
171 Ok(address)
172 }
173
174 fn derive_address(&self, index: u32, is_change: bool) -> Result<MultisigAddress> {
176 let secp = Secp256k1::new();
177
178 let chain = if is_change { 1 } else { 0 };
180 let path = DerivationPath::from_str(&format!("m/{}/{}", chain, index))
181 .map_err(|e| BitcoinError::DerivationFailed(e.to_string()))?;
182
183 let mut pubkeys: Vec<PublicKey> = Vec::new();
184
185 for xpub in &self.xpubs {
186 let derived = xpub
187 .derive_pub(&secp, &path)
188 .map_err(|e| BitcoinError::DerivationFailed(e.to_string()))?;
189 pubkeys.push(derived.public_key);
190 }
191
192 pubkeys.sort_by_key(|a| a.serialize());
194
195 let redeem_script =
197 Self::create_multisig_script(self.config.required_signatures, &pubkeys)?;
198
199 let witness_script = redeem_script.clone();
201 let _script_hash = witness_script.wscript_hash();
202
203 let address = Address::p2wsh(&witness_script, self.config.network);
204
205 Ok(MultisigAddress {
206 address: address.to_string(),
207 index,
208 is_change,
209 redeem_script: hex::encode(redeem_script.as_bytes()),
210 witness_script: hex::encode(witness_script.as_bytes()),
211 pubkeys: pubkeys.iter().map(|p| hex::encode(p.serialize())).collect(),
212 })
213 }
214
215 fn create_multisig_script(required: u8, pubkeys: &[PublicKey]) -> Result<ScriptBuf> {
217 let mut builder = ScriptBuilder::new().push_int(required as i64);
218
219 for pubkey in pubkeys {
220 let serialized = pubkey.serialize();
221 builder = builder.push_slice(serialized);
222 }
223
224 let script = builder
225 .push_int(pubkeys.len() as i64)
226 .push_opcode(bitcoin::opcodes::all::OP_CHECKMULTISIG)
227 .into_script();
228
229 Ok(script)
230 }
231
232 pub fn is_our_address(&self, address: &str) -> bool {
234 for cached in self.address_cache.values() {
236 if cached.address == address {
237 return true;
238 }
239 }
240
241 for i in 0..1000 {
243 if let Ok(addr) = self.derive_address_uncached(i, false) {
244 if addr.address == address {
245 return true;
246 }
247 }
248 if let Ok(addr) = self.derive_address_uncached(i, true) {
249 if addr.address == address {
250 return true;
251 }
252 }
253 }
254
255 false
256 }
257
258 fn derive_address_uncached(&self, index: u32, is_change: bool) -> Result<MultisigAddress> {
260 self.derive_address(index, is_change)
261 }
262
263 pub fn next_index(&self) -> u32 {
265 self.next_index
266 }
267
268 pub fn set_next_index(&mut self, index: u32) {
270 self.next_index = index;
271 }
272}
273
274#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct MultisigAddress {
277 pub address: String,
279 pub index: u32,
281 pub is_change: bool,
283 pub redeem_script: String,
285 pub witness_script: String,
287 pub pubkeys: Vec<String>,
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize)]
293pub struct MultisigTransaction {
294 pub txid: Option<String>,
296 pub unsigned_tx: String,
298 pub psbt: String,
300 pub signatures: Vec<MultisigSignature>,
302 pub required_signatures: u8,
304 pub total_signers: u8,
306 pub status: MultisigTxStatus,
308 pub inputs: Vec<MultisigInput>,
310 pub outputs: Vec<MultisigOutput>,
312}
313
314impl MultisigTransaction {
315 pub fn has_enough_signatures(&self) -> bool {
317 self.signatures.len() as u8 >= self.required_signatures
318 }
319
320 pub fn signatures_needed(&self) -> u8 {
322 self.required_signatures
323 .saturating_sub(self.signatures.len() as u8)
324 }
325
326 pub fn signed_by(&self) -> Vec<&str> {
328 self.signatures
329 .iter()
330 .map(|s| s.signer_label.as_str())
331 .collect()
332 }
333
334 pub fn pending_signers(&self, all_labels: &[String]) -> Vec<String> {
336 let signed: std::collections::HashSet<_> =
337 self.signatures.iter().map(|s| &s.signer_label).collect();
338 all_labels
339 .iter()
340 .filter(|l| !signed.contains(*l))
341 .cloned()
342 .collect()
343 }
344}
345
346#[derive(Debug, Clone, Serialize, Deserialize)]
348pub struct MultisigSignature {
349 pub signer_label: String,
351 pub signer_pubkey: String,
353 pub signature: String,
355 pub input_index: u32,
357 pub signed_at: String,
359}
360
361#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
363pub enum MultisigTxStatus {
364 Pending,
366 ReadyToBroadcast,
368 Broadcasted,
370 Confirmed,
372 Rejected,
374}
375
376#[derive(Debug, Clone, Serialize, Deserialize)]
378pub struct MultisigInput {
379 pub txid: String,
381 pub vout: u32,
383 pub amount_sats: u64,
385 pub address: String,
387 pub witness_script: String,
389}
390
391#[derive(Debug, Clone, Serialize, Deserialize)]
393pub struct MultisigOutput {
394 pub address: String,
396 pub amount_sats: u64,
398 pub is_change: bool,
400}
401
402pub struct MultisigTxBuilder {
404 wallet: MultisigWallet,
405 inputs: Vec<MultisigInput>,
406 outputs: Vec<MultisigOutput>,
407 fee_rate: u64,
408}
409
410impl MultisigTxBuilder {
411 pub fn new(wallet: MultisigWallet) -> Self {
413 Self {
414 wallet,
415 inputs: Vec::new(),
416 outputs: Vec::new(),
417 fee_rate: 1, }
419 }
420
421 pub fn add_input(mut self, input: MultisigInput) -> Self {
423 self.inputs.push(input);
424 self
425 }
426
427 pub fn add_inputs(mut self, inputs: Vec<MultisigInput>) -> Self {
429 self.inputs.extend(inputs);
430 self
431 }
432
433 pub fn add_output(mut self, address: impl Into<String>, amount_sats: u64) -> Self {
435 self.outputs.push(MultisigOutput {
436 address: address.into(),
437 amount_sats,
438 is_change: false,
439 });
440 self
441 }
442
443 pub fn fee_rate(mut self, rate: u64) -> Self {
445 self.fee_rate = rate;
446 self
447 }
448
449 fn estimate_vsize(&self) -> u64 {
451 let m = self.wallet.config.required_signatures as u64;
455 let n = self.wallet.config.total_keys as u64;
456
457 let input_weight = self.inputs.len() as u64 * (41 * 4 + 73 * m + 34 * n + 20);
458 let output_weight = self.outputs.len() as u64 * 43 * 4;
459 let overhead_weight = 44; (input_weight + output_weight + overhead_weight).div_ceil(4)
462 }
463
464 pub fn calculate_fee(&self) -> u64 {
466 self.estimate_vsize() * self.fee_rate
467 }
468
469 pub fn build(mut self) -> Result<MultisigTransaction> {
471 if self.inputs.is_empty() {
472 return Err(BitcoinError::Wallet("No inputs provided".to_string()));
473 }
474
475 if self.outputs.is_empty() {
476 return Err(BitcoinError::Wallet("No outputs provided".to_string()));
477 }
478
479 let fee = self.calculate_fee();
480 let total_input: u64 = self.inputs.iter().map(|i| i.amount_sats).sum();
481 let total_output: u64 = self.outputs.iter().map(|o| o.amount_sats).sum();
482
483 if total_input < total_output + fee {
484 return Err(BitcoinError::Wallet(format!(
485 "Insufficient funds: {} < {} + {} (fee)",
486 total_input, total_output, fee
487 )));
488 }
489
490 let change = total_input - total_output - fee;
492 if change > 546 {
493 let change_address = self.wallet.derive_address(self.wallet.next_index, true)?;
495 self.outputs.push(MultisigOutput {
496 address: change_address.address,
497 amount_sats: change,
498 is_change: true,
499 });
500 }
501
502 let unsigned_tx = format!(
505 "unsigned_tx_{}inputs_{}outputs",
506 self.inputs.len(),
507 self.outputs.len()
508 );
509
510 let psbt = base64::Engine::encode(
511 &base64::engine::general_purpose::STANDARD,
512 format!("psbt_placeholder_{}", unsigned_tx).as_bytes(),
513 );
514
515 Ok(MultisigTransaction {
516 txid: None,
517 unsigned_tx,
518 psbt,
519 signatures: Vec::new(),
520 required_signatures: self.wallet.config.required_signatures,
521 total_signers: self.wallet.config.total_keys,
522 status: MultisigTxStatus::Pending,
523 inputs: self.inputs,
524 outputs: self.outputs,
525 })
526 }
527}
528
529pub struct CustodyManager {
531 #[allow(dead_code)]
533 hot_wallet_xpub: Option<String>,
534 cold_wallet: Option<MultisigWallet>,
536 auto_sweep_threshold_sats: u64,
538 min_hot_balance_sats: u64,
540}
541
542impl CustodyManager {
543 pub fn new() -> Self {
545 Self {
546 hot_wallet_xpub: None,
547 cold_wallet: None,
548 auto_sweep_threshold_sats: 10_000_000, min_hot_balance_sats: 1_000_000, }
551 }
552
553 pub fn with_hot_wallet(mut self, xpub: impl Into<String>) -> Self {
555 self.hot_wallet_xpub = Some(xpub.into());
556 self
557 }
558
559 pub fn with_cold_wallet(mut self, config: MultisigConfig) -> Result<Self> {
561 self.cold_wallet = Some(MultisigWallet::new(config)?);
562 Ok(self)
563 }
564
565 pub fn auto_sweep_threshold(mut self, sats: u64) -> Self {
567 self.auto_sweep_threshold_sats = sats;
568 self
569 }
570
571 pub fn min_hot_balance(mut self, sats: u64) -> Self {
573 self.min_hot_balance_sats = sats;
574 self
575 }
576
577 pub fn should_sweep(&self, hot_balance_sats: u64) -> bool {
579 hot_balance_sats > self.auto_sweep_threshold_sats
580 }
581
582 pub fn sweep_amount(&self, hot_balance_sats: u64) -> u64 {
584 if hot_balance_sats <= self.auto_sweep_threshold_sats {
585 return 0;
586 }
587
588 hot_balance_sats.saturating_sub(self.min_hot_balance_sats)
590 }
591
592 pub fn get_cold_address(&mut self) -> Result<String> {
594 let wallet = self
595 .cold_wallet
596 .as_mut()
597 .ok_or_else(|| BitcoinError::Wallet("Cold wallet not configured".to_string()))?;
598
599 let address = wallet.get_new_address()?;
600 Ok(address.address)
601 }
602
603 pub fn has_cold_storage(&self) -> bool {
605 self.cold_wallet.is_some()
606 }
607
608 pub fn cold_wallet_info(&self) -> Option<ColdWalletInfo> {
610 self.cold_wallet.as_ref().map(|w| ColdWalletInfo {
611 type_string: w.config.type_string(),
612 key_labels: w.config.key_labels.clone(),
613 next_address_index: w.next_index,
614 })
615 }
616}
617
618impl Default for CustodyManager {
619 fn default() -> Self {
620 Self::new()
621 }
622}
623
624#[derive(Debug, Clone, Serialize, Deserialize)]
626pub struct ColdWalletInfo {
627 pub type_string: String,
629 pub key_labels: Vec<String>,
631 pub next_address_index: u32,
633}
634
635type TransactionReadyCallback = Box<dyn Fn(&MultisigTransaction) + Send + Sync>;
637
638pub struct SignatureCoordinator {
640 pending_txs: HashMap<String, MultisigTransaction>,
642 #[allow(dead_code)]
644 on_ready: Option<TransactionReadyCallback>,
645}
646
647impl SignatureCoordinator {
648 pub fn new() -> Self {
650 Self {
651 pending_txs: HashMap::new(),
652 on_ready: None,
653 }
654 }
655
656 pub fn on_ready<F>(mut self, callback: F) -> Self
658 where
659 F: Fn(&MultisigTransaction) + Send + Sync + 'static,
660 {
661 self.on_ready = Some(Box::new(callback));
662 self
663 }
664
665 pub fn add_transaction(&mut self, id: impl Into<String>, tx: MultisigTransaction) {
667 self.pending_txs.insert(id.into(), tx);
668 }
669
670 pub fn add_signature(&mut self, tx_id: &str, signature: MultisigSignature) -> Result<bool> {
672 let tx = self
673 .pending_txs
674 .get_mut(tx_id)
675 .ok_or_else(|| BitcoinError::Wallet(format!("Transaction {} not found", tx_id)))?;
676
677 if tx
679 .signatures
680 .iter()
681 .any(|s| s.signer_label == signature.signer_label)
682 {
683 return Err(BitcoinError::Wallet(format!(
684 "Already signed by {}",
685 signature.signer_label
686 )));
687 }
688
689 tx.signatures.push(signature);
690
691 if tx.has_enough_signatures() {
693 tx.status = MultisigTxStatus::ReadyToBroadcast;
694
695 if let Some(ref callback) = self.on_ready {
696 callback(tx);
697 }
698
699 return Ok(true);
700 }
701
702 Ok(false)
703 }
704
705 pub fn get_status(&self, tx_id: &str) -> Option<&MultisigTransaction> {
707 self.pending_txs.get(tx_id)
708 }
709
710 pub fn pending_transactions(&self) -> Vec<(&String, &MultisigTransaction)> {
712 self.pending_txs.iter().collect()
713 }
714
715 pub fn remove_transaction(&mut self, tx_id: &str) -> Option<MultisigTransaction> {
717 self.pending_txs.remove(tx_id)
718 }
719}
720
721impl Default for SignatureCoordinator {
722 fn default() -> Self {
723 Self::new()
724 }
725}
726
727#[cfg(test)]
728mod tests {
729 use super::*;
730
731 #[test]
732 fn test_multisig_config_validation() {
733 let config = MultisigConfig::new_2of3(
735 vec![
736 "xpub1".to_string(),
737 "xpub2".to_string(),
738 "xpub3".to_string(),
739 ],
740 vec![
741 "platform".to_string(),
742 "user".to_string(),
743 "cold".to_string(),
744 ],
745 Network::Bitcoin,
746 )
747 .unwrap();
748
749 assert_eq!(config.type_string(), "2-of-3");
750 assert_eq!(config.required_signatures, 2);
751 assert_eq!(config.total_keys, 3);
752 }
753
754 #[test]
755 fn test_invalid_config() {
756 let result = MultisigConfig::new_2of3(
758 vec!["xpub1".to_string(), "xpub2".to_string()],
759 vec!["a".to_string(), "b".to_string(), "c".to_string()],
760 Network::Bitcoin,
761 );
762
763 assert!(result.is_err());
764 }
765
766 #[test]
767 fn test_multisig_tx_signatures() {
768 let tx = MultisigTransaction {
769 txid: None,
770 unsigned_tx: "test".to_string(),
771 psbt: "test".to_string(),
772 signatures: vec![],
773 required_signatures: 2,
774 total_signers: 3,
775 status: MultisigTxStatus::Pending,
776 inputs: vec![],
777 outputs: vec![],
778 };
779
780 assert!(!tx.has_enough_signatures());
781 assert_eq!(tx.signatures_needed(), 2);
782 }
783
784 #[test]
785 fn test_custody_manager() {
786 let manager = CustodyManager::new()
787 .auto_sweep_threshold(10_000_000)
788 .min_hot_balance(1_000_000);
789
790 assert!(manager.should_sweep(15_000_000));
791 assert!(!manager.should_sweep(5_000_000));
792
793 let sweep = manager.sweep_amount(15_000_000);
794 assert_eq!(sweep, 14_000_000); }
796}