1use bitcoin::{
6 bip32::{DerivationPath as BitcoinDerivationPath, ExtendedPrivKey, ExtendedPubKey},
7 hashes::Hash as BitcoinHash,
8 key::TapTweak,
9 secp256k1::{self, Secp256k1, SecretKey, XOnlyPublicKey},
10 Address, Network, OutPoint, Txid,
11};
12use std::collections::{HashMap, HashSet};
13use std::str::FromStr;
14use std::sync::Mutex;
15
16use bitcoin::secp256k1::rand::{rngs::OsRng, RngCore};
17
18#[allow(unused_imports)]
19use crate::types::BitcoinSealRef;
20
21const HARDENED: u32 = 0x8000_0000;
23
24const BIP86_PURPOSE: u32 = 86;
26
27fn coin_type(network: &Network) -> u32 {
29 match network {
30 Network::Bitcoin => 0,
31 _ => 1,
32 }
33}
34
35#[derive(Clone, Debug)]
37pub struct Bip86Path {
38 pub account: u32,
39 pub change: u32,
40 pub index: u32,
41}
42
43impl Bip86Path {
44 pub fn new(account: u32, change: u32, index: u32) -> Self {
45 Self {
46 account,
47 change,
48 index,
49 }
50 }
51 pub fn external(account: u32, index: u32) -> Self {
52 Self::new(account, 0, index)
53 }
54 pub fn internal(account: u32, index: u32) -> Self {
55 Self::new(account, 1, index)
56 }
57 pub fn to_bitcoin_path(&self, network: &Network) -> BitcoinDerivationPath {
58 let coin = coin_type(network);
59 format!(
60 "m/{}'/{}'/{}'/{}/{}",
61 BIP86_PURPOSE, coin, self.account, self.change, self.index
62 )
63 .parse()
64 .expect("valid BIP-32 path")
65 }
66 pub fn to_string(&self, network: &Network) -> String {
67 let coin = coin_type(network);
68 format!(
69 "m/{}'/{}'/{}'/{}/{}",
70 BIP86_PURPOSE, coin, self.account, self.change, self.index
71 )
72 }
73}
74
75#[derive(Clone, Debug)]
77pub struct WalletUtxo {
78 pub outpoint: OutPoint,
79 pub amount_sat: u64,
80 pub path: Bip86Path,
81 pub reserved: bool,
82 pub reserved_for: Option<String>,
83}
84
85#[derive(Clone, Debug)]
87pub struct DerivedTaprootKey {
88 pub internal_xonly: XOnlyPublicKey,
89 pub output_key: bitcoin::key::TweakedPublicKey,
90 pub path: Bip86Path,
91 pub address: Address,
92}
93
94pub struct SealWallet {
96 master_key: ExtendedPrivKey,
97 network: Network,
98 utxos: Mutex<HashMap<OutPoint, WalletUtxo>>,
99 used_seals: Mutex<HashSet<Vec<u8>>>,
100 secp: Secp256k1<secp256k1::All>,
101 next_index: Mutex<HashMap<u32, u32>>,
102}
103
104impl SealWallet {
105 pub fn from_mnemonic(
106 mnemonic: &str,
107 password: &str,
108 network: Network,
109 ) -> Result<Self, WalletError> {
110 let seed = bip32::Mnemonic::new(mnemonic, bip32::Language::English)
111 .map_err(|e| WalletError::InvalidMnemonic(e.to_string()))?
112 .to_seed(password);
113 Self::from_seed(seed.as_bytes(), network)
114 }
115
116 pub fn from_seed(seed: &[u8; 64], network: Network) -> Result<Self, WalletError> {
117 let btc_net = match network {
118 Network::Bitcoin => bitcoin::Network::Bitcoin,
119 Network::Testnet => bitcoin::Network::Testnet,
120 Network::Signet => bitcoin::Network::Signet,
121 Network::Regtest => bitcoin::Network::Regtest,
122 _ => bitcoin::Network::Testnet,
123 };
124 let secp = Secp256k1::new();
125 let master_key = ExtendedPrivKey::new_master(btc_net, seed)
126 .map_err(|e| WalletError::KeyDerivationFailed(e.to_string()))?;
127 Ok(Self {
128 master_key,
129 network,
130 utxos: Mutex::new(HashMap::new()),
131 used_seals: Mutex::new(HashSet::new()),
132 secp,
133 next_index: Mutex::new(HashMap::new()),
134 })
135 }
136
137 pub fn generate_random(network: Network) -> Self {
138 let mut seed = [0u8; 64];
139 OsRng.fill_bytes(&mut seed);
140 Self::from_seed(&seed, network).expect("valid seed")
141 }
142
143 pub fn from_xpub(xpub: &str, network: Network) -> Result<Self, WalletError> {
144 let extended_pub = ExtendedPubKey::from_str(xpub)
145 .map_err(|e| WalletError::InvalidKey(format!("Invalid xpub: {}", e)))?;
146 let btc_net = match network {
147 Network::Bitcoin => bitcoin::Network::Bitcoin,
148 Network::Testnet => bitcoin::Network::Testnet,
149 Network::Signet => bitcoin::Network::Signet,
150 Network::Regtest => bitcoin::Network::Regtest,
151 _ => bitcoin::Network::Testnet,
152 };
153 if extended_pub.network != btc_net {
154 return Err(WalletError::InvalidKey(format!(
155 "xpub network mismatch: expected {:?}, got {:?}",
156 btc_net, extended_pub.network
157 )));
158 }
159 let mut seed = [0u8; 64];
160 OsRng.fill_bytes(&mut seed);
161 let wallet = Self::from_seed(&seed, network)?;
162 Ok(wallet)
163 }
164
165 fn derive_private_key(&self, path: &Bip86Path) -> Result<SecretKey, WalletError> {
166 let btc_path = path.to_bitcoin_path(&self.network);
167 let child = self
168 .master_key
169 .derive_priv(&self.secp, &btc_path)
170 .map_err(|e| WalletError::KeyDerivationFailed(format!("{:?}", e)))?;
171 Ok(child.private_key)
172 }
173
174 pub fn derive_key(&self, path: &Bip86Path) -> Result<DerivedTaprootKey, WalletError> {
176 let secret_key = self.derive_private_key(path)?;
177 let kp = secp256k1::KeyPair::from_secret_key(&self.secp, &secret_key);
178 let (xonly, _parity) = XOnlyPublicKey::from_keypair(&kp);
179 let (output_key, _) = xonly.tap_tweak(&self.secp, None);
181 let address = Address::p2tr_tweaked(output_key, self.network);
182 Ok(DerivedTaprootKey {
183 internal_xonly: xonly,
184 output_key,
185 path: path.clone(),
186 address,
187 })
188 }
189
190 pub fn sign_taproot_keypath(
192 &self,
193 path: &Bip86Path,
194 sighash: &[u8; 32],
195 ) -> Result<Vec<u8>, WalletError> {
196 let secret_key = self.derive_private_key(path)?;
197 let kp = secp256k1::KeyPair::from_secret_key(&self.secp, &secret_key);
198 let tweaked_kp = kp.tap_tweak(&self.secp, None);
200 let msg = secp256k1::Message::from_slice(sighash)
201 .map_err(|e| WalletError::SigningFailed(e.to_string()))?;
202 let sig = self
203 .secp
204 .sign_schnorr_with_rng(&msg, &tweaked_kp.to_inner(), &mut OsRng);
205 Ok(sig.as_ref().to_vec())
206 }
207
208 pub fn next_address(
209 &self,
210 account: u32,
211 ) -> Result<(DerivedTaprootKey, Bip86Path), WalletError> {
212 let mut ni = self.next_index.lock().unwrap_or_else(|e| e.into_inner());
213 let idx = ni.entry(account).or_insert(0);
214 let path = Bip86Path::external(account, *idx);
215 let key = self.derive_key(&path)?;
216 *idx += 1;
217 Ok((key, path))
218 }
219
220 pub fn get_funding_address(
221 &self,
222 account: u32,
223 index: u32,
224 ) -> Result<DerivedTaprootKey, WalletError> {
225 self.derive_key(&Bip86Path::external(account, index))
226 }
227
228 pub fn get_account_xpub(&self, account: u32) -> Result<String, WalletError> {
229 let coin = coin_type(&self.network);
230 let account_path: BitcoinDerivationPath =
231 format!("m/{}'/{}'/{}'", BIP86_PURPOSE, coin, account)
232 .parse()
233 .map_err(|e| WalletError::KeyDerivationFailed(format!("{:?}", e)))?;
234 let account_key = self
235 .master_key
236 .derive_priv(&self.secp, &account_path)
237 .map_err(|e| WalletError::KeyDerivationFailed(format!("{:?}", e)))?;
238 Ok(ExtendedPubKey::from_priv(&self.secp, &account_key).to_string())
239 }
240
241 pub fn add_utxo(&self, outpoint: OutPoint, amount_sat: u64, path: Bip86Path) {
242 self.utxos.lock().unwrap_or_else(|e| e.into_inner()).insert(
243 outpoint,
244 WalletUtxo {
245 outpoint,
246 amount_sat,
247 path,
248 reserved: false,
249 reserved_for: None,
250 },
251 );
252 }
253 pub fn balance(&self) -> u64 {
254 self.utxos
255 .lock()
256 .unwrap_or_else(|e| e.into_inner())
257 .values()
258 .filter(|u| !u.reserved)
259 .map(|u| u.amount_sat)
260 .sum()
261 }
262 pub fn utxo_count(&self) -> usize {
263 self.utxos
264 .lock()
265 .unwrap_or_else(|e| e.into_inner())
266 .values()
267 .filter(|u| !u.reserved)
268 .count()
269 }
270
271 pub fn select_utxos(&self, target_sat: u64) -> Result<Vec<WalletUtxo>, WalletError> {
272 let mut available: Vec<_> = self
273 .utxos
274 .lock()
275 .unwrap_or_else(|e| e.into_inner())
276 .values()
277 .filter(|u| !u.reserved)
278 .cloned()
279 .collect();
280 available.sort_by(|a, b| b.amount_sat.cmp(&a.amount_sat));
281 let mut sel = Vec::new();
282 let mut total = 0u64;
283 for utxo in available {
284 if total >= target_sat {
285 break;
286 }
287 total += utxo.amount_sat;
288 sel.push(utxo);
289 }
290 if total < target_sat {
291 Err(WalletError::InsufficientFunds {
292 available: total,
293 needed: target_sat,
294 })
295 } else {
296 Ok(sel)
297 }
298 }
299
300 pub fn reserve_utxos(&self, ops: &[OutPoint], reason: &str) {
301 let mut u = self.utxos.lock().unwrap_or_else(|e| e.into_inner());
302 for op in ops {
303 if let Some(x) = u.get_mut(op) {
304 x.reserved = true;
305 x.reserved_for = Some(reason.to_string());
306 }
307 }
308 }
309 pub fn unreserve_utxos(&self, ops: &[OutPoint]) {
310 let mut u = self.utxos.lock().unwrap_or_else(|e| e.into_inner());
311 for op in ops {
312 if let Some(x) = u.get_mut(op) {
313 x.reserved = false;
314 x.reserved_for = None;
315 }
316 }
317 }
318
319 pub fn sign_with_key(
320 &self,
321 path: &Bip86Path,
322 msg: &[u8; 32],
323 ) -> Result<secp256k1::ecdsa::Signature, WalletError> {
324 let sk = self.derive_private_key(path)?;
325 let msg = secp256k1::Message::from_slice(msg.as_ref())
326 .map_err(|e| WalletError::SigningFailed(e.to_string()))?;
327 Ok(self.secp.sign_ecdsa(&msg, &sk))
328 }
329
330 pub fn mark_seal_used(&self, seal: &BitcoinSealRef) -> Result<(), WalletError> {
331 let mut used = self.used_seals.lock().unwrap_or_else(|e| e.into_inner());
332 let key = seal.to_vec();
333 if used.contains(&key) {
334 return Err(WalletError::SealAlreadyUsed);
335 }
336 used.insert(key);
337 Ok(())
338 }
339 pub fn is_seal_used(&self, seal: &BitcoinSealRef) -> bool {
340 self.used_seals
341 .lock()
342 .unwrap_or_else(|e| e.into_inner())
343 .contains(&seal.to_vec())
344 }
345
346 pub fn network(&self) -> Network {
347 self.network
348 }
349 pub fn secp(&self) -> &Secp256k1<secp256k1::All> {
350 &self.secp
351 }
352 pub fn get_utxo(&self, op: &OutPoint) -> Option<WalletUtxo> {
353 self.utxos
354 .lock()
355 .unwrap_or_else(|e| e.into_inner())
356 .get(op)
357 .cloned()
358 }
359 pub fn list_utxos(&self) -> Vec<WalletUtxo> {
360 self.utxos
361 .lock()
362 .unwrap_or_else(|e| e.into_inner())
363 .values()
364 .cloned()
365 .collect()
366 }
367
368 pub fn scan_chain_for_utxos<F>(
375 &self,
376 mut fetch_utxos: F,
377 account: u32,
378 address_gap_limit: usize,
379 ) -> Result<usize, WalletError>
380 where
381 F: FnMut(&Address) -> Result<Vec<(OutPoint, u64)>, String>,
382 {
383 let mut discovered_count = 0;
384 let mut consecutive_empty = 0;
385 let mut index = 0;
386
387 loop {
388 if consecutive_empty >= address_gap_limit {
389 break;
390 }
391
392 let path = Bip86Path::external(account, index);
393 let derived = self.derive_key(&path)?;
394
395 match fetch_utxos(&derived.address) {
396 Ok(utxos) => {
397 if utxos.is_empty() {
398 consecutive_empty += 1;
399 } else {
400 consecutive_empty = 0;
401 for (outpoint, amount) in utxos {
402 self.add_utxo(outpoint, amount, path.clone());
403 discovered_count += 1;
404 }
405 }
406 }
407 Err(e) => {
408 return Err(WalletError::KeyDerivationFailed(e));
409 }
410 }
411
412 index += 1;
413 }
414
415 Ok(discovered_count)
416 }
417
418 pub fn add_utxo_from_address(
423 &self,
424 outpoint: OutPoint,
425 amount_sat: u64,
426 account: u32,
427 index: u32,
428 ) -> Result<(), WalletError> {
429 let path = Bip86Path::external(account, index);
430 let _derived = self.derive_key(&path)?;
431
432 self.add_utxo(outpoint, amount_sat, path);
435 Ok(())
436 }
437}
438
439#[derive(Debug, thiserror::Error)]
440pub enum WalletError {
441 #[error("No available UTXOs")]
442 NoAvailableUtxos,
443 #[error("Insufficient funds: available {available} sat, needed {needed} sat")]
444 InsufficientFunds { available: u64, needed: u64 },
445 #[error("UTXO not found")]
446 UtxoNotFound,
447 #[error("Seal already used")]
448 SealAlreadyUsed,
449 #[error("Invalid mnemonic: {0}")]
450 InvalidMnemonic(String),
451 #[error("Key derivation failed: {0}")]
452 KeyDerivationFailed(String),
453 #[error("Invalid key: {0}")]
454 InvalidKey(String),
455 #[error("Signing failed: {0}")]
456 SigningFailed(String),
457 #[error("PSBT error: {0}")]
458 PsbtError(String),
459 #[error("Script error: {0}")]
460 ScriptError(String),
461}
462
463pub struct MockSealWallet {
464 pub utxos: Vec<(OutPoint, u64)>,
465 pub used_seals: Mutex<HashSet<Vec<u8>>>,
466}
467impl MockSealWallet {
468 pub fn new() -> Self {
469 Self {
470 utxos: Vec::new(),
471 used_seals: Mutex::new(HashSet::new()),
472 }
473 }
474 pub fn add_utxo(&mut self, txid: [u8; 32], vout: u32, amount_sat: u64) {
475 let txid = Txid::from_raw_hash(bitcoin::hashes::sha256d::Hash::from_slice(&txid).unwrap());
476 self.utxos.push((OutPoint::new(txid, vout), amount_sat));
477 }
478}
479impl Default for MockSealWallet {
480 fn default() -> Self {
481 Self::new()
482 }
483}
484
485#[cfg(test)]
486mod tests {
487 use super::*;
488 #[test]
489 fn test_wallet_creation_from_random() {
490 let w = SealWallet::generate_random(Network::Signet);
491 assert_eq!(w.balance(), 0);
492 }
493 #[test]
494 fn test_wallet_key_derivation() {
495 let w = SealWallet::generate_random(Network::Signet);
496 let k = w.derive_key(&Bip86Path::external(0, 0)).unwrap();
497 assert_eq!(k.address.network, Network::Signet);
498 assert!(k.address.script_pubkey().is_witness_program());
499 }
500 #[test]
501 fn test_wallet_key_derivation_deterministic() {
502 let seed = [42u8; 64];
503 let w1 = SealWallet::from_seed(&seed, Network::Signet).unwrap();
504 let w2 = SealWallet::from_seed(&seed, Network::Signet).unwrap();
505 let k1 = w1.derive_key(&Bip86Path::external(0, 0)).unwrap();
506 let k2 = w2.derive_key(&Bip86Path::external(0, 0)).unwrap();
507 assert_eq!(k1.output_key, k2.output_key);
508 assert_eq!(k1.address, k2.address);
509 }
510 #[test]
511 fn test_wallet_different_paths() {
512 let w = SealWallet::generate_random(Network::Signet);
513 let k0 = w.derive_key(&Bip86Path::external(0, 0)).unwrap();
514 let k1 = w.derive_key(&Bip86Path::external(0, 1)).unwrap();
515 let k2 = w.derive_key(&Bip86Path::external(1, 0)).unwrap();
516 assert_ne!(k0.output_key, k1.output_key);
517 assert_ne!(k0.output_key, k2.output_key);
518 assert_ne!(k1.output_key, k2.output_key);
519 }
520 #[test]
521 fn test_wallet_utxo_selection() {
522 let w = SealWallet::generate_random(Network::Signet);
523 let path = Bip86Path::external(0, 0);
524 let t1 =
525 Txid::from_raw_hash(bitcoin::hashes::sha256d::Hash::from_slice(&[1u8; 32]).unwrap());
526 let t2 =
527 Txid::from_raw_hash(bitcoin::hashes::sha256d::Hash::from_slice(&[2u8; 32]).unwrap());
528 let t3 =
529 Txid::from_raw_hash(bitcoin::hashes::sha256d::Hash::from_slice(&[3u8; 32]).unwrap());
530 w.add_utxo(OutPoint::new(t1, 0), 50_000, path.clone());
531 w.add_utxo(OutPoint::new(t2, 0), 30_000, path.clone());
532 w.add_utxo(OutPoint::new(t3, 0), 20_000, path);
533 let sel = w.select_utxos(70_000).unwrap();
534 assert_eq!(sel.len(), 2);
535 assert_eq!(sel.iter().map(|u| u.amount_sat).sum::<u64>(), 80_000);
536 }
537 #[test]
538 fn test_wallet_insufficient_funds() {
539 let w = SealWallet::generate_random(Network::Signet);
540 let txid =
541 Txid::from_raw_hash(bitcoin::hashes::sha256d::Hash::from_slice(&[1u8; 32]).unwrap());
542 w.add_utxo(OutPoint::new(txid, 0), 10_000, Bip86Path::external(0, 0));
543 assert!(w.select_utxos(20_000).is_err());
544 }
545 #[test]
546 fn test_wallet_reserve_utxos() {
547 let w = SealWallet::generate_random(Network::Signet);
548 let txid =
549 Txid::from_raw_hash(bitcoin::hashes::sha256d::Hash::from_slice(&[1u8; 32]).unwrap());
550 let op = OutPoint::new(txid, 0);
551 w.add_utxo(op, 100_000, Bip86Path::external(0, 0));
552 assert_eq!(w.balance(), 100_000);
553 w.reserve_utxos(&[op], "test");
554 assert_eq!(w.balance(), 0);
555 w.unreserve_utxos(&[op]);
556 assert_eq!(w.balance(), 100_000);
557 }
558 #[test]
559 fn test_seal_lifecycle() {
560 let w = SealWallet::generate_random(Network::Signet);
561 let seal = BitcoinSealRef::new([1u8; 32], 0, Some(42));
562 assert!(!w.is_seal_used(&seal));
563 w.mark_seal_used(&seal).unwrap();
564 assert!(w.is_seal_used(&seal));
565 assert!(w.mark_seal_used(&seal).is_err());
566 }
567 #[test]
568 fn test_derivation_path_string() {
569 assert_eq!(
570 Bip86Path::new(0, 0, 5).to_string(&Network::Bitcoin),
571 "m/86'/0'/0'/0/5"
572 );
573 }
574 #[test]
575 fn test_mock_wallet() {
576 let mut w = MockSealWallet::new();
577 w.add_utxo([1u8; 32], 0, 100_000);
578 assert_eq!(w.utxos.len(), 1);
579 }
580}