1use bdk_chain::spk_client::{FullScanRequest, SyncRequest};
7use bdk_chain::Merge;
8use bdk_esplora::EsploraAsyncExt;
9
10use bdk_wallet::{KeychainKind, Wallet};
11use bitcoin::address::{AddressType, NetworkUnchecked};
12use bitcoin::hashes::Hash;
13use bitcoin::psbt::Psbt;
14use bitcoin::{Address, Amount, FeeRate, Network, Transaction};
15use crate::error::ZincError;
17use crate::keys::ZincMnemonic;
18use serde::{Deserialize, Serialize};
19use std::str::FromStr;
20
21const LOG_TARGET_BUILDER: &str = "zinc_core::builder";
22
23fn wasm_now_secs() -> u64 {
31 #[cfg(target_arch = "wasm32")]
32 {
33 (js_sys::Date::now() / 1000.0) as u64
34 }
35 #[cfg(not(target_arch = "wasm32"))]
36 {
37 std::time::UNIX_EPOCH
38 .elapsed()
39 .unwrap_or_default()
40 .as_secs()
41 }
42}
43
44#[derive(Debug, Clone, Deserialize, Default)]
46#[serde(rename_all = "camelCase")]
47pub struct SignOptions {
48 pub sign_inputs: Option<Vec<usize>>,
50 pub sighash: Option<u8>,
52 #[serde(default)]
55 pub finalize: bool,
56}
57
58use zeroize::{Zeroize, ZeroizeOnDrop};
59
60#[derive(Debug, Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
62pub struct Seed64([u8; 64]);
63
64impl Seed64 {
65 #[must_use]
67 pub const fn from_array(bytes: [u8; 64]) -> Self {
68 Self(bytes)
69 }
70}
71
72impl AsRef<[u8]> for Seed64 {
73 fn as_ref(&self) -> &[u8] {
74 &self.0
75 }
76}
77
78impl TryFrom<&[u8]> for Seed64 {
79 type Error = ZincError;
80
81 fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
82 let array: [u8; 64] = value.try_into().map_err(|_| {
83 ZincError::ConfigError(format!(
84 "Invalid seed length: {}. Expected 64 bytes.",
85 value.len()
86 ))
87 })?;
88 Ok(Self(array))
89 }
90}
91
92#[derive(Debug, Clone)]
94pub struct CreatePsbtRequest {
95 pub recipient: Address<NetworkUnchecked>,
97 pub amount: Amount,
99 pub fee_rate: FeeRate,
101}
102
103impl CreatePsbtRequest {
104 pub fn from_parts(
106 recipient: &str,
107 amount_sats: u64,
108 fee_rate_sat_vb: u64,
109 ) -> Result<Self, ZincError> {
110 let recipient = recipient
111 .parse::<Address<NetworkUnchecked>>()
112 .map_err(|e| ZincError::ConfigError(format!("Invalid address: {e}")))?;
113 let fee_rate = FeeRate::from_sat_per_vb(fee_rate_sat_vb)
114 .ok_or_else(|| ZincError::ConfigError("Invalid fee rate".to_string()))?;
115
116 Ok(Self {
117 recipient,
118 amount: Amount::from_sat(amount_sats),
119 fee_rate,
120 })
121 }
122}
123
124#[derive(Debug, Clone, Deserialize, Serialize)]
126#[serde(rename_all = "camelCase")]
127pub struct CreatePsbtTransportRequest {
128 pub recipient: String,
130 pub amount_sats: u64,
132 pub fee_rate_sat_vb: u64,
134}
135
136impl TryFrom<CreatePsbtTransportRequest> for CreatePsbtRequest {
137 type Error = ZincError;
138
139 fn try_from(value: CreatePsbtTransportRequest) -> Result<Self, Self::Error> {
140 Self::from_parts(&value.recipient, value.amount_sats, value.fee_rate_sat_vb)
141 }
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub enum AddressScheme {
147 Unified,
149 Dual,
151}
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
155#[serde(rename_all = "lowercase")]
156#[derive(Default)]
157pub enum PaymentAddressType {
158 #[default]
160 NativeSegwit,
161 NestedSegwit,
163 Legacy,
165}
166
167impl PaymentAddressType {
168 #[must_use]
169 pub fn purpose(self) -> u32 {
170 match self {
171 Self::NativeSegwit => 84,
172 Self::NestedSegwit => 49,
173 Self::Legacy => 44,
174 }
175 }
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
180#[serde(rename_all = "lowercase")]
181#[derive(Default)]
182pub enum DerivationMode {
183 #[default]
185 Account,
186 Index,
188}
189
190#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
192#[serde(rename_all = "lowercase")]
193pub enum ProfileMode {
194 Seed,
196 Watch,
198}
199
200#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
202#[serde(rename_all = "camelCase")]
203pub struct ScanPolicy {
204 pub account_gap_limit: u32,
206 pub address_scan_depth: u32,
208}
209
210impl Default for ScanPolicy {
211 fn default() -> Self {
212 Self {
213 account_gap_limit: 20,
214 address_scan_depth: 1,
215 }
216 }
217}
218
219#[cfg(target_arch = "wasm32")]
220#[derive(Debug, Clone, Copy, Default)]
221pub struct WasmSleeper;
222
223#[cfg(target_arch = "wasm32")]
224pub struct WasmSleep(gloo_timers::future::TimeoutFuture);
225
226#[cfg(target_arch = "wasm32")]
227impl std::future::Future for WasmSleep {
228 type Output = ();
229 fn poll(
230 mut self: std::pin::Pin<&mut Self>,
231 cx: &mut std::task::Context<'_>,
232 ) -> std::task::Poll<Self::Output> {
233 std::pin::Pin::new(&mut self.0).poll(cx)
234 }
235}
236
237#[cfg(target_arch = "wasm32")]
238#[allow(unsafe_code)]
240unsafe impl Send for WasmSleep {}
241
242#[cfg(target_arch = "wasm32")]
243impl esplora_client::Sleeper for WasmSleeper {
244 type Sleep = WasmSleep;
245 fn sleep(dur: std::time::Duration) -> Self::Sleep {
246 WasmSleep(gloo_timers::future::TimeoutFuture::new(
247 dur.as_millis() as u32
248 ))
249 }
250}
251
252#[cfg(target_arch = "wasm32")]
253pub type SyncSleeper = WasmSleeper;
254
255#[cfg(not(target_arch = "wasm32"))]
257#[derive(Debug, Clone, Copy, Default)]
258pub struct TokioSleeper;
259
260#[cfg(not(target_arch = "wasm32"))]
261impl esplora_client::Sleeper for TokioSleeper {
262 type Sleep = tokio::time::Sleep;
263 fn sleep(dur: std::time::Duration) -> Self::Sleep {
264 tokio::time::sleep(dur)
265 }
266}
267
268#[cfg(not(target_arch = "wasm32"))]
270pub type SyncSleeper = TokioSleeper;
271
272pub fn now_unix() -> u64 {
274 #[cfg(target_arch = "wasm32")]
275 {
276 (js_sys::Date::now() / 1000.0) as u64
277 }
278
279 #[cfg(not(target_arch = "wasm32"))]
280 {
281 std::time::SystemTime::now()
282 .duration_since(std::time::UNIX_EPOCH)
283 .unwrap_or_default()
284 .as_secs()
285 }
286}
287
288#[derive(Debug, Clone)]
290pub enum WalletKind {
291 Seed {
293 master_xprv: bdk_wallet::bitcoin::bip32::Xpriv,
295 },
296 Hardware {
298 fingerprint: [u8; 4],
300 taproot_external: String,
302 payment_external: Option<String>,
304 },
305 WatchAddress(Address),
307}
308
309impl WalletKind {
310 #[must_use]
312 pub fn is_watch(&self) -> bool {
313 !matches!(self, Self::Seed { .. })
314 }
315
316 pub fn derive_descriptors(
319 &self,
320 scheme: AddressScheme,
321 payment_type: PaymentAddressType,
322 network: Network,
323 account: u32,
324 ) -> (String, String, Option<String>, Option<String>) {
325 let coin_type = u32::from(network != Network::Bitcoin);
326
327 match self {
328 Self::Seed {
329 master_xprv: master,
330 } => {
331 let vault_ext = format!("tr({master}/86'/{coin_type}'/{account}'/0/*)");
332 let vault_int = format!("tr({master}/86'/{coin_type}'/{account}'/1/*)");
333
334 if scheme == AddressScheme::Dual {
335 let pay_ext =
336 payment_descriptor_for_xprv(master, payment_type, coin_type, account, 0);
337 let pay_int =
338 payment_descriptor_for_xprv(master, payment_type, coin_type, account, 1);
339 (vault_ext, vault_int, Some(pay_ext), Some(pay_int))
340 } else {
341 (vault_ext, vault_int, None, None)
342 }
343 }
344 Self::Hardware {
345 taproot_external,
346 payment_external,
347 ..
348 } => {
349 (
351 taproot_external.clone(),
352 taproot_external.replace("/0/*", "/1/*"),
353 payment_external.clone(),
354 payment_external.as_ref().map(|e| e.replace("/0/*", "/1/*")),
355 )
356 }
357 Self::WatchAddress(address) => {
358 let descriptor = taproot_watch_descriptor(address)
359 .expect("watch-address identity must hold a validated taproot address");
360 (descriptor.clone(), descriptor, None, None)
361 }
362 }
363 }
364}
365
366pub struct ZincWallet {
368 pub(crate) vault_wallet: Wallet,
372 pub(crate) payment_wallet: Option<Wallet>,
374 pub(crate) scheme: AddressScheme,
376 pub(crate) derivation_mode: DerivationMode,
378 pub(crate) payment_address_type: PaymentAddressType,
380 pub(crate) loaded_vault_changeset: bdk_wallet::ChangeSet,
383 pub(crate) loaded_payment_changeset: Option<bdk_wallet::ChangeSet>,
385 pub(crate) account_index: u32,
387 pub(crate) mode: ProfileMode,
389 pub(crate) scan_policy: ScanPolicy,
391 pub(crate) inscribed_utxos: std::collections::HashSet<bitcoin::OutPoint>,
394 pub(crate) inscriptions: Vec<crate::ordinals::types::Inscription>,
396 pub(crate) rune_balances: Vec<crate::ordinals::types::RuneBalance>,
398 pub(crate) ordinals_verified: bool,
400 pub(crate) ordinals_metadata_complete: bool,
402 pub(crate) kind: WalletKind,
404 #[allow(dead_code)]
406 pub(crate) is_syncing: bool,
407 pub(crate) account_generation: u64,
409}
410
411pub enum SyncRequestType {
413 Full(FullScanRequest<KeychainKind>),
415 Incremental(SyncRequest<(KeychainKind, u32)>),
417}
418
419pub struct ZincSyncRequest {
421 pub taproot: SyncRequestType,
423 pub payment: Option<SyncRequestType>,
425}
426
427#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
429pub struct ZincBalance {
430 pub total: bdk_wallet::Balance,
432 pub spendable: bdk_wallet::Balance,
434 pub display_spendable: bdk_wallet::Balance,
436 pub inscribed: u64,
438}
439
440#[derive(Debug, Clone, Serialize, Deserialize)]
442#[serde(rename_all = "camelCase")]
443pub struct Account {
444 pub index: u32,
446 pub label: String,
448 #[serde(alias = "vaultAddress")]
450 pub taproot_address: String,
451 #[serde(alias = "vaultPublicKey")]
453 pub taproot_public_key: String,
454 pub payment_address: Option<String>,
456 pub payment_public_key: Option<String>,
458}
459
460#[derive(Debug, Clone)]
462pub struct DiscoveryAccountPlan {
463 pub index: u32,
465 pub taproot_descriptor: String,
467 pub taproot_change_descriptor: String,
469 pub taproot_public_key: String,
471 pub taproot_receive_index: u32,
473 pub payment_descriptor: Option<String>,
475 pub payment_change_descriptor: Option<String>,
477 pub payment_public_key: Option<String>,
479 pub payment_receive_index: Option<u32>,
481}
482
483#[derive(Debug, Clone)]
485pub struct DiscoveryContext {
486 pub network: Network,
488 pub scheme: AddressScheme,
490 pub derivation_mode: DerivationMode,
492 pub payment_address_type: PaymentAddressType,
494 pub kind: WalletKind,
496 pub accounts: Vec<DiscoveryAccountPlan>,
498 pub is_syncing: bool,
500 pub account_generation: u64,
502}
503
504fn payment_descriptor_for_xprv(
505 xprv: &bdk_wallet::bitcoin::bip32::Xpriv,
506 address_type: PaymentAddressType,
507 coin_type: u32,
508 account: u32,
509 chain: u32,
510) -> String {
511 let pay_purpose = address_type.purpose();
512
513 match address_type {
514 PaymentAddressType::NativeSegwit => {
515 format!("wpkh({xprv}/{pay_purpose}'/{coin_type}'/{account}'/{chain}/*)")
516 }
517 PaymentAddressType::NestedSegwit => {
518 format!("sh(wpkh({xprv}/{pay_purpose}'/{coin_type}'/{account}'/{chain}/*))")
519 }
520 PaymentAddressType::Legacy => {
521 format!("pkh({xprv}/{pay_purpose}'/{coin_type}'/{account}'/{chain}/*)")
522 }
523 }
524}
525
526fn payment_descriptor_for_xpub(
527 xpub: &bdk_wallet::bitcoin::bip32::Xpub,
528 address_type: PaymentAddressType,
529 chain: u32,
530) -> String {
531 match address_type {
532 PaymentAddressType::NativeSegwit => format!("wpkh({xpub}/{chain}/*)"),
533 PaymentAddressType::NestedSegwit => format!("sh(wpkh({xpub}/{chain}/*))"),
534 PaymentAddressType::Legacy => format!("pkh({xpub}/{chain}/*)"),
535 }
536}
537
538fn parse_extended_public_key(xpub: &str) -> Result<bdk_wallet::bitcoin::bip32::Xpub, String> {
539 use bdk_wallet::bitcoin::bip32::Xpub;
540
541 if let Ok(parsed) = Xpub::from_str(xpub) {
542 return Ok(parsed);
543 }
544
545 let mut data = bdk_wallet::bitcoin::base58::decode_check(xpub)
546 .map_err(|e| format!("Invalid extended public key: {e}"))?;
547 if data.len() != 78 {
548 return Err(format!(
549 "Invalid extended public key payload length: {} (expected 78)",
550 data.len()
551 ));
552 }
553
554 let version: [u8; 4] = [data[0], data[1], data[2], data[3]];
555 let normalized_version = match version {
556 [0x04, 0x88, 0xB2, 0x1E]
558 | [0x04, 0x9D, 0x7C, 0xB2]
559 | [0x04, 0xB2, 0x47, 0x46]
560 | [0x02, 0x95, 0xB4, 0x3F]
561 | [0x02, 0xAA, 0x7E, 0xD3] => [0x04, 0x88, 0xB2, 0x1E],
562 [0x04, 0x35, 0x87, 0xCF]
564 | [0x04, 0x4A, 0x52, 0x62]
565 | [0x04, 0x5F, 0x1C, 0xF6]
566 | [0x02, 0x42, 0x89, 0xEF]
567 | [0x02, 0x57, 0x54, 0x83] => [0x04, 0x35, 0x87, 0xCF],
568 _ => {
569 return Err(
570 "Unsupported extended public key prefix (expected xpub/ypub/zpub/tpub/upub/vpub)"
571 .to_string(),
572 );
573 }
574 };
575
576 data[0..4].copy_from_slice(&normalized_version);
577 Xpub::decode(&data).map_err(|e| format!("Invalid extended public key: {e}"))
578}
579
580fn taproot_output_key_from_address(
581 address: &Address,
582) -> Result<bitcoin::secp256k1::XOnlyPublicKey, String> {
583 if address.address_type() != Some(AddressType::P2tr) {
584 return Err(
585 "Address watch mode currently supports taproot (bc1p/tb1p/bcrt1p) addresses only"
586 .to_string(),
587 );
588 }
589
590 let witness_program = address
591 .witness_program()
592 .ok_or_else(|| "Taproot address missing witness program".to_string())?;
593 let key_bytes = witness_program.program().as_bytes();
594 if key_bytes.len() != 32 {
595 return Err(format!(
596 "Invalid taproot witness program length: {}",
597 key_bytes.len()
598 ));
599 }
600
601 bitcoin::secp256k1::XOnlyPublicKey::from_slice(key_bytes)
602 .map_err(|e| format!("Invalid taproot output key: {e}"))
603}
604
605fn taproot_watch_descriptor(address: &Address) -> Result<String, String> {
606 let output_key = taproot_output_key_from_address(address)?;
607 Ok(format!("tr({output_key})"))
608}
609
610#[derive(Clone)]
612pub struct WalletBuilder {
613 network: Network,
614 kind: Option<WalletKind>,
615 mode: ProfileMode,
616 scheme: AddressScheme,
617 derivation_mode: DerivationMode,
618 payment_address_type: PaymentAddressType,
619 persistence: Option<ZincPersistence>,
620 account_index: u32,
621 scan_policy: ScanPolicy,
622}
623
624impl ZincWallet {
625 fn watched_address(&self) -> Option<&Address> {
626 match &self.kind {
627 WalletKind::WatchAddress(address) => Some(address),
628 _ => None,
629 }
630 }
631
632 pub fn derive_public_key_internal(
633 &self,
634 purpose: u32,
635 _network: Network,
636 account: u32,
637 index: u32,
638 ) -> Result<String, String> {
639 use bitcoin::secp256k1::Secp256k1;
640 let secp = Secp256k1::new();
641
642 match &self.kind {
643 WalletKind::Seed { master_xprv } => {
644 let network = self.vault_wallet.network();
645 let coin_type = if network == Network::Bitcoin { 0 } else { 1 };
646 let chain = 0; let derivation_path = [
649 bdk_wallet::bitcoin::bip32::ChildNumber::from_hardened_idx(purpose).unwrap(),
650 bdk_wallet::bitcoin::bip32::ChildNumber::from_hardened_idx(coin_type).unwrap(),
651 bdk_wallet::bitcoin::bip32::ChildNumber::from_hardened_idx(account).unwrap(),
652 bdk_wallet::bitcoin::bip32::ChildNumber::from_normal_idx(chain).unwrap(),
653 bdk_wallet::bitcoin::bip32::ChildNumber::from_normal_idx(index).unwrap(),
654 ];
655
656 let child_xprv = master_xprv
657 .derive_priv(&secp, &derivation_path)
658 .map_err(|e| format!("Key derivation failed: {e}"))?;
659
660 let public_key = child_xprv.private_key.public_key(&secp);
661
662 if purpose == 86 {
664 let (x_only, _parity) = public_key.x_only_public_key();
666 Ok(x_only.to_string())
667 } else {
668 Ok(public_key.to_string())
670 }
671 }
672 WalletKind::Hardware {
673 taproot_external,
674 payment_external,
675 ..
676 } => {
677 let desc_str = if purpose == 86 {
680 taproot_external
681 } else {
682 payment_external.as_ref().ok_or_else(|| {
683 "Payment descriptor missing for this hardware wallet".to_string()
684 })?
685 };
686
687 let xpub_start_part = if let Some(pos) = desc_str.find(']') {
690 &desc_str[pos + 1..]
691 } else if let Some(pos) = desc_str.find('(') {
692 &desc_str[pos + 1..]
693 } else {
694 desc_str
695 };
696
697 let xpub_end_pos = xpub_start_part.find('/').unwrap_or(xpub_start_part.len());
700 let xpub_str = xpub_start_part[..xpub_end_pos].trim_end_matches(')');
701
702 use bitcoin::bip32::{ChildNumber, Xpub};
704 use std::str::FromStr;
705 let xpub = Xpub::from_str(xpub_str).map_err(|e| {
706 format!(
707 "Failed to parse xpub from descriptor (part: {}): {}",
708 xpub_str, e
709 )
710 })?;
711
712 let derived_xpub = xpub
714 .derive_pub(
715 &secp,
716 &[
717 ChildNumber::from_normal_idx(0).unwrap(),
718 ChildNumber::from_normal_idx(index).unwrap(),
719 ],
720 )
721 .map_err(|e| format!("Failed to derive public key from xpub: {}", e))?;
722
723 let public_key = derived_xpub.public_key;
724
725 if purpose == 86 {
726 let (x_only, _parity) = public_key.x_only_public_key();
727 Ok(x_only.to_string())
728 } else {
729 Ok(public_key.to_string())
730 }
731 }
732 WalletKind::WatchAddress(address) => {
733 if purpose != 86 {
734 return Err(ZincError::CapabilityMissing.to_string());
735 }
736 let output_key = taproot_output_key_from_address(address)
737 .map_err(|_| ZincError::CapabilityMissing.to_string())?;
738 return Ok(output_key.to_string());
739 }
740 }
741 }
742
743 #[must_use]
745 pub fn inscriptions(&self) -> &[crate::ordinals::types::Inscription] {
746 &self.inscriptions
747 }
748
749 #[must_use]
751 pub fn rune_balances(&self) -> &[crate::ordinals::types::RuneBalance] {
752 &self.rune_balances
753 }
754
755 #[must_use]
757 pub fn account_generation(&self) -> u64 {
758 self.account_generation
759 }
760
761 #[must_use]
763 pub fn active_account_index(&self) -> u32 {
764 self.account_index
765 }
766
767 #[must_use]
769 pub fn is_syncing(&self) -> bool {
770 self.is_syncing
771 }
772
773 #[must_use]
775 pub fn ordinals_verified(&self) -> bool {
776 self.ordinals_verified
777 }
778
779 #[must_use]
781 pub fn ordinals_metadata_complete(&self) -> bool {
782 self.ordinals_metadata_complete
783 }
784
785 pub fn is_unified(&self) -> bool {
787 self.scheme == AddressScheme::Unified
788 }
789
790 #[must_use]
792 pub fn derivation_mode(&self) -> DerivationMode {
793 self.derivation_mode
794 }
795
796 #[must_use]
798 pub fn profile_mode(&self) -> ProfileMode {
799 self.mode
800 }
801
802 #[must_use]
804 pub fn payment_address_type(&self) -> PaymentAddressType {
805 self.payment_address_type
806 }
807
808 fn logical_account_path(&self, logical_account_index: u32) -> (u32, u32) {
809 match self.derivation_mode {
810 DerivationMode::Account => (logical_account_index, 0),
811 DerivationMode::Index => (0, logical_account_index),
812 }
813 }
814
815 fn active_receive_index(&self) -> u32 {
816 self.logical_account_path(self.account_index).1
817 }
818
819 fn active_derivation_account(&self) -> u32 {
820 self.logical_account_path(self.account_index).0
821 }
822
823 fn dual_payment_purpose(&self) -> u32 {
824 self.payment_address_type.purpose()
825 }
826
827 pub fn needs_full_scan(&self) -> bool {
829 self.vault_wallet.local_chain().tip().height() == 0
831 }
832
833 pub fn next_taproot_address(&mut self) -> Result<Address, String> {
835 if let Some(address) = self.watched_address() {
836 return Ok(address.clone());
837 }
838
839 if self.derivation_mode == DerivationMode::Index {
840 return Ok(self.peek_taproot_address(0));
841 }
842 let info = self
843 .vault_wallet
844 .reveal_next_address(KeychainKind::External);
845 Ok(info.address)
846 }
847
848 pub fn peek_taproot_address(&self, index: u32) -> Address {
850 if let Some(address) = self.watched_address() {
851 let _ = index;
852 return address.clone();
853 }
854
855 let resolved_index = self.active_receive_index().saturating_add(index);
856 self.vault_wallet
857 .peek_address(KeychainKind::External, resolved_index)
858 .address
859 }
860
861 pub fn get_payment_address(&mut self) -> Result<bitcoin::Address, String> {
865 if self.scheme == AddressScheme::Dual {
866 if self.derivation_mode == DerivationMode::Index {
867 return self
868 .peek_payment_address(0)
869 .ok_or_else(|| "Payment wallet not initialized".to_string());
870 }
871 if let Some(wallet) = &mut self.payment_wallet {
872 Ok(wallet.reveal_next_address(KeychainKind::External).address)
873 } else {
874 Err("Payment wallet not initialized".to_string())
875 }
876 } else {
877 self.next_taproot_address()
878 }
879 }
880
881 pub fn peek_payment_address(&self, index: u32) -> Option<Address> {
885 if self.scheme == AddressScheme::Dual {
886 let resolved_index = self.active_receive_index().saturating_add(index);
887 self.payment_wallet.as_ref().map(|w| {
888 w.peek_address(KeychainKind::External, resolved_index)
889 .address
890 })
891 } else {
892 Some(self.peek_taproot_address(index))
893 }
894 }
895
896 pub fn export_changeset(&self) -> Result<ZincPersistence, String> {
898 let mut vault_changeset = self.loaded_vault_changeset.clone();
900 if let Some(staged) = self.vault_wallet.staged() {
901 vault_changeset.merge(staged.clone());
902 }
903
904 let network = self.vault_wallet.network();
906 vault_changeset.network = Some(network);
907
908 vault_changeset.descriptor = Some(
910 self.vault_wallet
911 .public_descriptor(KeychainKind::External)
912 .clone(),
913 );
914 vault_changeset.change_descriptor = Some(
915 self.vault_wallet
916 .public_descriptor(KeychainKind::Internal)
917 .clone(),
918 );
919
920 let genesis_hash = bitcoin::blockdata::constants::genesis_block(network)
921 .header
922 .block_hash();
923 vault_changeset
925 .local_chain
926 .blocks
927 .entry(0)
928 .or_insert(Some(genesis_hash));
929
930 let mut payment_changeset = self.loaded_payment_changeset.clone();
932
933 if let Some(w) = &self.payment_wallet {
934 let mut pcs = payment_changeset.take().unwrap_or_default();
936
937 if let Some(staged) = w.staged() {
938 pcs.merge(staged.clone());
939 }
940
941 let net = w.network();
942 pcs.network = Some(net);
943
944 pcs.descriptor = Some(w.public_descriptor(KeychainKind::External).clone());
946 pcs.change_descriptor = Some(w.public_descriptor(KeychainKind::Internal).clone());
947
948 let gen_hash = bitcoin::blockdata::constants::genesis_block(net)
949 .header
950 .block_hash();
951 pcs.local_chain.blocks.entry(0).or_insert(Some(gen_hash));
952 payment_changeset = Some(pcs);
954 } else {
955 payment_changeset = None;
957 }
958
959 Ok(ZincPersistence {
960 taproot: Some(vault_changeset),
961 payment: payment_changeset,
962 })
963 }
964
965 pub async fn check_connection(esplora_url: &str) -> bool {
967 let client = esplora_client::Builder::new(esplora_url.trim_end_matches('/'))
968 .build_async_with_sleeper::<SyncSleeper>();
969
970 match client {
971 Ok(c) => c.get_height().await.is_ok(),
972 Err(_) => false,
973 }
974 }
975
976 pub fn prepare_requests(&self) -> ZincSyncRequest {
978 let now = wasm_now_secs();
979 let vault = SyncRequestType::Full(Self::flexible_full_scan_request(
980 &self.vault_wallet,
981 self.scan_policy,
982 now,
983 ));
984
985 let payment = self.payment_wallet.as_ref().map(|w| {
986 SyncRequestType::Full(Self::flexible_full_scan_request(w, self.scan_policy, now))
987 });
988
989 ZincSyncRequest {
990 taproot: vault,
991 payment,
992 }
993 }
994
995 pub fn apply_sync(
997 &mut self,
998 vault_update: impl Into<bdk_wallet::Update>,
999 payment_update: Option<impl Into<bdk_wallet::Update>>,
1000 ) -> Result<Vec<String>, String> {
1001 let mut all_events = Vec::new();
1002
1003 let vault_events = self
1005 .vault_wallet
1006 .apply_update_events(vault_update)
1007 .map_err(|e| e.to_string())?;
1008
1009 for event in vault_events {
1010 all_events.push(format!("taproot:{event:?}"));
1011 }
1012
1013 if let (Some(w), Some(u)) = (&mut self.payment_wallet, payment_update) {
1015 let payment_events = w.apply_update_events(u).map_err(|e| e.to_string())?;
1016 for event in payment_events {
1017 all_events.push(format!("payment:{event:?}"));
1018 }
1019 }
1020
1021 Ok(all_events)
1022 }
1023
1024 pub fn reset_sync_state(&mut self) -> Result<(), String> {
1026 zinc_log_info!(
1027 target: LOG_TARGET_BUILDER,
1028 "resetting wallet sync state (chain mismatch recovery)"
1029 );
1030
1031 let vault_desc = self
1033 .vault_wallet
1034 .public_descriptor(KeychainKind::External)
1035 .to_string();
1036 let network = self.vault_wallet.network();
1037 self.vault_wallet = if matches!(&self.kind, WalletKind::WatchAddress(_)) {
1038 Wallet::create_single(vault_desc)
1039 .network(network)
1040 .create_wallet_no_persist()
1041 .map_err(|e| format!("Failed to reset taproot wallet: {e}"))?
1042 } else {
1043 let vault_change_desc = self
1044 .vault_wallet
1045 .public_descriptor(KeychainKind::Internal)
1046 .to_string();
1047 Wallet::create(vault_desc, vault_change_desc)
1048 .network(network)
1049 .create_wallet_no_persist()
1050 .map_err(|e| format!("Failed to reset taproot wallet: {e}"))?
1051 };
1052 self.loaded_vault_changeset = bdk_wallet::ChangeSet::default();
1053
1054 if let Some(w) = &self.payment_wallet {
1056 let pay_desc = w.public_descriptor(KeychainKind::External).to_string();
1057 let pay_change_desc = w.public_descriptor(KeychainKind::Internal).to_string();
1058
1059 self.payment_wallet = Some(
1060 Wallet::create(pay_desc, pay_change_desc)
1061 .network(network)
1062 .create_wallet_no_persist()
1063 .map_err(|e| format!("Failed to reset payment wallet: {e}"))?,
1064 );
1065 self.loaded_payment_changeset = Some(bdk_wallet::ChangeSet::default());
1066 }
1067
1068 self.account_generation += 1;
1070 self.ordinals_verified = false;
1071 self.ordinals_metadata_complete = false;
1072
1073 Ok(())
1074 }
1075
1076 pub async fn sync(&mut self, esplora_url: &str) -> Result<Vec<String>, String> {
1078 let client = esplora_client::Builder::new(esplora_url.trim_end_matches('/'))
1079 .build_async_with_sleeper::<SyncSleeper>()
1080 .map_err(|e| format!("{e:?}"))?;
1081
1082 let now = wasm_now_secs();
1083 let vault_req = Self::flexible_full_scan_request(&self.vault_wallet, self.scan_policy, now);
1084 let payment_req = self
1085 .payment_wallet
1086 .as_ref()
1087 .map(|w| Self::flexible_full_scan_request(w, self.scan_policy, now));
1088
1089 let stop_gap = self.scan_policy.account_gap_limit as usize;
1090 let parallel_requests = 5;
1091
1092 let vault_update = client
1094 .full_scan(vault_req, stop_gap, parallel_requests)
1095 .await
1096 .map_err(|e| e.to_string())?;
1097
1098 let payment_update = if let Some(req) = payment_req {
1100 Some(
1101 client
1102 .full_scan(req, stop_gap, parallel_requests)
1103 .await
1104 .map_err(|e| e.to_string())?,
1105 )
1106 } else {
1107 None
1108 };
1109
1110 self.apply_sync(vault_update, payment_update)
1111 }
1112
1113 pub fn collect_active_addresses(&self) -> Vec<String> {
1123 if let Some(address) = self.watched_address() {
1124 return vec![address.to_string()];
1125 }
1126
1127 let mut addresses = Vec::new();
1128 let mut seen = std::collections::HashSet::new();
1129
1130 let mut collect_from_wallet = |wallet: &Wallet| {
1131 for i in 0..self.scan_policy.address_scan_depth {
1133 let addr = wallet
1134 .peek_address(KeychainKind::External, i)
1135 .address
1136 .to_string();
1137 if seen.insert(addr.clone()) {
1138 addresses.push(addr);
1139 }
1140 }
1141
1142 for info in wallet.list_unused_addresses(KeychainKind::External) {
1145 let addr = info.address.to_string();
1146 if seen.insert(addr.clone()) {
1147 addresses.push(addr);
1148 }
1149 }
1150 };
1151
1152 collect_from_wallet(&self.vault_wallet);
1153 if let Some(w) = &self.payment_wallet {
1154 collect_from_wallet(w);
1155 }
1156
1157 addresses
1158 }
1159
1160 pub fn apply_verified_ordinals_update(
1163 &mut self,
1164 inscriptions: Vec<crate::ordinals::types::Inscription>,
1165 protected_outpoints: std::collections::HashSet<bitcoin::OutPoint>,
1166 rune_balances: Vec<crate::ordinals::types::RuneBalance>,
1167 ) -> usize {
1168 zinc_log_info!(
1169 target: LOG_TARGET_BUILDER,
1170 "applying ordinals update: {} inscriptions received",
1171 inscriptions.len()
1172 );
1173 for inscription in &inscriptions {
1174 zinc_log_debug!(
1175 target: LOG_TARGET_BUILDER,
1176 "inscribed outpoint updated: {}",
1177 inscription.satpoint.outpoint
1178 );
1179 }
1180
1181 self.inscribed_utxos = protected_outpoints;
1182 self.inscriptions = inscriptions;
1183 self.rune_balances = rune_balances;
1184 self.ordinals_verified = true;
1185 self.ordinals_metadata_complete = true;
1186
1187 zinc_log_info!(
1188 target: LOG_TARGET_BUILDER,
1189 "total inscribed_utxos set size: {}",
1190 self.inscribed_utxos.len()
1191 );
1192 self.inscriptions.len()
1193 }
1194
1195 pub fn apply_unverified_inscriptions_cache(
1200 &mut self,
1201 inscriptions: Vec<crate::ordinals::types::Inscription>,
1202 ) -> usize {
1203 zinc_log_info!(
1204 target: LOG_TARGET_BUILDER,
1205 "applying unverified inscription cache: {} inscriptions received",
1206 inscriptions.len()
1207 );
1208
1209 self.inscribed_utxos.clear();
1210 self.inscriptions = inscriptions;
1211 self.rune_balances.clear();
1212 self.ordinals_verified = false;
1213 self.ordinals_metadata_complete = true;
1214
1215 self.inscriptions.len()
1216 }
1217
1218 fn verify_ord_indexer_is_current(
1219 &mut self,
1220 ord_height: u32,
1221 wallet_height: u32,
1222 ) -> Result<(), String> {
1223 if ord_height < wallet_height.saturating_sub(1) {
1224 self.ordinals_verified = false;
1225 return Err(format!(
1226 "Ord Indexer is lagging! Ord: {ord_height}, Wallet: {wallet_height}. Safety lock engaged."
1227 ));
1228 }
1229 Ok(())
1230 }
1231
1232 pub async fn sync_ordinals_protection(&mut self, ord_url: &str) -> Result<usize, String> {
1234 self.ordinals_verified = false;
1235 let addresses = self.collect_active_addresses();
1236 let client = crate::ordinals::OrdClient::new(ord_url.to_string());
1237
1238 let ord_height = client
1240 .get_indexing_height()
1241 .await
1242 .map_err(|e| e.to_string())?;
1243
1244 let wallet_height = self.vault_wallet.local_chain().tip().height();
1246
1247 self.verify_ord_indexer_is_current(ord_height, wallet_height)?;
1248
1249 let mut protected_outpoints = std::collections::HashSet::new();
1250 for addr_str in addresses {
1251 let snapshot = client
1252 .get_address_asset_snapshot(&addr_str)
1253 .await
1254 .map_err(|e| format!("Failed to fetch for {addr_str}: {e}"))?;
1255
1256 let protected = client
1257 .get_protected_outpoints_from_outputs(&snapshot.outputs)
1258 .await
1259 .map_err(|e| format!("Failed to fetch protected outputs for {addr_str}: {e}"))?;
1260 protected_outpoints.extend(protected);
1261 }
1262
1263 self.inscribed_utxos = protected_outpoints;
1264 self.ordinals_verified = true;
1265 Ok(self.inscribed_utxos.len())
1266 }
1267
1268 pub async fn sync_ordinals_metadata(&mut self, ord_url: &str) -> Result<usize, String> {
1270 self.ordinals_metadata_complete = false;
1271 let addresses = self.collect_active_addresses();
1272 let client = crate::ordinals::OrdClient::new(ord_url.to_string());
1273
1274 let ord_height = client
1275 .get_indexing_height()
1276 .await
1277 .map_err(|e| e.to_string())?;
1278 let wallet_height = self.vault_wallet.local_chain().tip().height();
1279 self.verify_ord_indexer_is_current(ord_height, wallet_height)?;
1280 let rune_balances = client
1281 .get_rune_balances_for_addresses(&addresses)
1282 .await
1283 .map_err(|e| format!("Failed to fetch rune balances: {e}"))?;
1284
1285 let mut all_inscriptions = Vec::new();
1286 for addr_str in addresses {
1287 let snapshot = client
1288 .get_address_asset_snapshot(&addr_str)
1289 .await
1290 .map_err(|e| format!("Failed to fetch for {addr_str}: {e}"))?;
1291
1292 for inscription_id in snapshot.inscription_ids {
1293 let inscription = client
1294 .get_inscription_details(&inscription_id)
1295 .await
1296 .map_err(|e| format!("Failed to fetch details for {inscription_id}: {e}"))?;
1297 all_inscriptions.push(inscription);
1298 }
1299 }
1300
1301 self.inscriptions = all_inscriptions;
1302 self.rune_balances = rune_balances;
1303 self.ordinals_metadata_complete = true;
1304 Ok(self.inscriptions.len())
1305 }
1306
1307 pub async fn sync_ordinals(&mut self, ord_url: &str) -> Result<usize, String> {
1310 self.sync_ordinals_protection(ord_url).await?;
1311 self.sync_ordinals_metadata(ord_url).await
1312 }
1313
1314 pub fn get_accounts(&self, count: u32) -> Vec<Account> {
1316 match &self.kind {
1317 WalletKind::WatchAddress(address) => {
1318 let taproot_address = address.to_string();
1319 let taproot_public_key = self.get_taproot_public_key(0).unwrap_or_default();
1320 vec![Account {
1321 index: self.account_index,
1322 label: format!("Account {}", self.account_index + 1),
1323 taproot_address: taproot_address.clone(),
1324 taproot_public_key: taproot_public_key.clone(),
1325 payment_address: Some(taproot_address),
1326 payment_public_key: Some(taproot_public_key),
1327 }]
1328 }
1329 WalletKind::Hardware { .. } => {
1330 let taproot_address = self.peek_taproot_address(0).to_string();
1331 let taproot_public_key = self.get_taproot_public_key(0).unwrap_or_default();
1332 let (payment_address, payment_public_key) = if self.scheme == AddressScheme::Dual {
1333 (
1334 self.peek_payment_address(0).map(|a| a.to_string()),
1335 self.get_payment_public_key(0).ok(),
1336 )
1337 } else {
1338 (
1339 Some(taproot_address.clone()),
1340 Some(taproot_public_key.clone()),
1341 )
1342 };
1343 vec![Account {
1344 index: self.account_index,
1345 label: format!("Account {}", self.account_index + 1),
1346 taproot_address,
1347 taproot_public_key,
1348 payment_address,
1349 payment_public_key,
1350 }]
1351 }
1352 WalletKind::Seed { master_xprv } => {
1353 let mut accounts = Vec::new();
1354 for i in 0..count {
1355 let builder = WalletBuilder::new(self.vault_wallet.network())
1357 .kind(WalletKind::Seed {
1358 master_xprv: *master_xprv,
1359 })
1360 .with_scheme(self.scheme)
1361 .with_derivation_mode(self.derivation_mode)
1362 .with_payment_address_type(self.payment_address_type)
1363 .with_account_index(i);
1364
1365 if let Ok(zwallet) = builder.build() {
1366 let taproot_address = zwallet.peek_taproot_address(0).to_string();
1367 let taproot_public_key =
1368 zwallet.get_taproot_public_key(0).unwrap_or_default();
1369 let (payment_address, payment_public_key) =
1370 if self.scheme == AddressScheme::Dual {
1371 (
1372 zwallet.peek_payment_address(0).map(|a| a.to_string()),
1373 zwallet.get_payment_public_key(0).ok(),
1374 )
1375 } else {
1376 (
1377 Some(taproot_address.clone()),
1378 Some(taproot_public_key.clone()),
1379 )
1380 };
1381 accounts.push(Account {
1382 index: i,
1383 label: format!("Account {}", i + 1),
1384 taproot_address,
1385 taproot_public_key,
1386 payment_address,
1387 payment_public_key,
1388 });
1389 }
1390 }
1391 accounts
1392 }
1393 }
1394 }
1395
1396 pub fn get_raw_balance(&self) -> bdk_wallet::Balance {
1398 let vault_bal = self.vault_wallet.balance();
1399 if let Some(payment_wallet) = &self.payment_wallet {
1400 let pay_bal = payment_wallet.balance();
1401 bdk_wallet::Balance {
1402 immature: vault_bal.immature + pay_bal.immature,
1403 trusted_pending: vault_bal.trusted_pending + pay_bal.trusted_pending,
1404 untrusted_pending: vault_bal.untrusted_pending + pay_bal.untrusted_pending,
1405 confirmed: vault_bal.confirmed + pay_bal.confirmed,
1406 }
1407 } else {
1408 vault_bal
1409 }
1410 }
1411
1412 pub fn get_balance(&self) -> ZincBalance {
1414 let raw = self.get_raw_balance();
1415
1416 let calc_balance = |wallet: &Wallet| {
1418 let mut bal = bdk_wallet::Balance::default();
1419 for utxo in wallet.list_unspent() {
1420 if self.inscribed_utxos.contains(&utxo.outpoint) {
1421 zinc_log_debug!(
1422 target: LOG_TARGET_BUILDER,
1423 "skipping inscribed UTXO while calculating balance: {:?}",
1424 utxo.outpoint
1425 );
1426 continue;
1427 }
1428 match utxo.keychain {
1429 KeychainKind::Internal | KeychainKind::External => {
1430 match utxo.chain_position {
1432 bdk_chain::ChainPosition::Confirmed { .. } => {
1433 bal.confirmed += utxo.txout.value;
1434 }
1435 bdk_chain::ChainPosition::Unconfirmed { .. } => {
1436 bal.trusted_pending += utxo.txout.value;
1437 }
1438 }
1439 }
1440 }
1441 }
1442 bal
1443 };
1444
1445 let mut safe_bal = calc_balance(&self.vault_wallet);
1446 if let Some(w) = &self.payment_wallet {
1447 let p_bal = calc_balance(w);
1448 safe_bal.confirmed += p_bal.confirmed;
1449 safe_bal.trusted_pending += p_bal.trusted_pending;
1450 safe_bal.untrusted_pending += p_bal.untrusted_pending;
1451 safe_bal.immature += p_bal.immature;
1452 }
1453
1454 let display_spendable = if let Some(payment_wallet) = &self.payment_wallet {
1455 calc_balance(payment_wallet)
1456 } else {
1457 safe_bal.clone()
1458 };
1459
1460 ZincBalance {
1461 total: raw.clone(),
1462 spendable: safe_bal.clone(),
1463 display_spendable,
1464 inscribed: raw
1465 .confirmed
1466 .to_sat()
1467 .saturating_sub(safe_bal.confirmed.to_sat())
1468 + raw
1469 .trusted_pending
1470 .to_sat()
1471 .saturating_sub(safe_bal.trusted_pending.to_sat()), }
1473 }
1474
1475 pub fn create_psbt_tx(&mut self, request: &CreatePsbtRequest) -> Result<Psbt, ZincError> {
1477 if !self.ordinals_verified {
1478 return Err(ZincError::WalletError(
1479 "Ordinals verification failed - safety lock engaged. Please retry sync."
1480 .to_string(),
1481 ));
1482 }
1483
1484 let active_receive_index = self.active_receive_index();
1485 let wallet = if self.scheme == AddressScheme::Dual {
1486 self.payment_wallet.as_mut().ok_or_else(|| {
1487 ZincError::WalletError("Payment wallet not initialized".to_string())
1488 })?
1489 } else {
1490 &mut self.vault_wallet
1491 };
1492
1493 let recipient = request
1494 .recipient
1495 .clone()
1496 .require_network(wallet.network())
1497 .map_err(|e| ZincError::ConfigError(format!("Network mismatch: {e}")))?;
1498
1499 let change_script = wallet
1500 .peek_address(KeychainKind::External, active_receive_index)
1501 .script_pubkey();
1502
1503 let mut builder = wallet.build_tx();
1504 if !self.inscribed_utxos.is_empty() {
1505 builder.unspendable(self.inscribed_utxos.iter().copied().collect());
1506 }
1507
1508 builder
1509 .add_recipient(recipient.script_pubkey(), request.amount)
1510 .fee_rate(request.fee_rate)
1511 .drain_to(change_script);
1512
1513 builder
1514 .finish()
1515 .map_err(|e| ZincError::WalletError(format!("Failed to build tx: {e}")))
1516 }
1517
1518 pub fn create_psbt_base64(&mut self, request: &CreatePsbtRequest) -> Result<String, ZincError> {
1520 let psbt = self.create_psbt_tx(request)?;
1521 Ok(Self::encode_psbt_base64(&psbt))
1522 }
1523
1524 pub fn create_offer(
1526 &mut self,
1527 request: &crate::offer_create::CreateOfferRequest,
1528 ) -> Result<crate::offer_create::OfferCreateResultV1, ZincError> {
1529 crate::offer_create::create_offer(self, request)
1530 }
1531
1532 pub fn create_listing_purchase(
1534 &mut self,
1535 request: &crate::listing::CreateListingPurchaseRequest,
1536 ) -> Result<crate::listing::CreateListingPurchaseResultV1, ZincError> {
1537 crate::listing::create_listing_purchase(self, request)
1538 }
1539
1540 #[doc(hidden)]
1546 #[deprecated(note = "Use create_psbt_base64 with CreatePsbtRequest")]
1547 pub fn create_psbt(
1548 &mut self,
1549 recipient: &str,
1550 amount_sats: u64,
1551 fee_rate_sat_vb: u64,
1552 ) -> Result<String, String> {
1553 let request = CreatePsbtRequest::from_parts(recipient, amount_sats, fee_rate_sat_vb)
1554 .map_err(|e| e.to_string())?;
1555 self.create_psbt_base64(&request).map_err(|e| e.to_string())
1556 }
1557
1558 fn encode_psbt_base64(psbt: &Psbt) -> String {
1559 use base64::Engine;
1560 base64::engine::general_purpose::STANDARD.encode(psbt.serialize())
1561 }
1562
1563 #[allow(deprecated)]
1566 pub fn sign_psbt(
1567 &mut self,
1568 psbt_base64: &str,
1569 options: Option<SignOptions>,
1570 ) -> Result<String, String> {
1571 use base64::Engine;
1572
1573 let psbt_bytes = base64::engine::general_purpose::STANDARD
1575 .decode(psbt_base64)
1576 .map_err(|e| format!("Invalid base64: {e}"))?;
1577
1578 let mut psbt = Psbt::deserialize(&psbt_bytes).map_err(|e| format!("Invalid PSBT: {e}"))?;
1579
1580 use std::collections::HashMap;
1583 let mut known_utxos = HashMap::new();
1584
1585 let collect_utxos = |w: &Wallet, map: &mut HashMap<bitcoin::OutPoint, bitcoin::TxOut>| {
1586 for utxo in w.list_unspent() {
1587 map.insert(utxo.outpoint, utxo.txout);
1588 }
1589 };
1590
1591 collect_utxos(&self.vault_wallet, &mut known_utxos);
1592 if let Some(w) = &self.payment_wallet {
1593 collect_utxos(w, &mut known_utxos);
1594 }
1595
1596 for (i, input) in psbt.inputs.iter_mut().enumerate() {
1597 if input.witness_utxo.is_none() && input.non_witness_utxo.is_none() {
1598 let outpoint = psbt.unsigned_tx.input[i].previous_output;
1599 if let Some(txout) = known_utxos.get(&outpoint) {
1600 input.witness_utxo = Some(txout.clone());
1601 }
1602 }
1603 }
1604
1605 let should_finalize = options.as_ref().is_some_and(|o| o.finalize);
1608 let bdk_options = bdk_wallet::SignOptions {
1609 trust_witness_utxo: true,
1613 try_finalize: should_finalize,
1616 ..Default::default()
1617 };
1618 let mut inputs_to_sign: Option<Vec<usize>> = None;
1619
1620 if let Some(opts) = &options {
1621 if let Some(sighash_u8) = opts.sighash {
1622 let target_sighash =
1624 bitcoin::psbt::PsbtSighashType::from_u32(u32::from(sighash_u8));
1625 for input in &mut psbt.inputs {
1626 input.sighash_type = Some(target_sighash);
1627 }
1628 }
1629 inputs_to_sign = opts.sign_inputs.clone();
1630 }
1631
1632 if let Some(indices) = inputs_to_sign.as_ref() {
1633 let mut seen = std::collections::HashSet::new();
1634 for index in indices {
1635 if *index >= psbt.inputs.len() {
1636 return Err(format!(
1637 "Security Violation: sign_inputs index {} is out of bounds for {} inputs",
1638 index,
1639 psbt.inputs.len()
1640 ));
1641 }
1642 if !seen.insert(*index) {
1643 return Err(format!(
1644 "Security Violation: sign_inputs index {index} is duplicated"
1645 ));
1646 }
1647 let input = &psbt.inputs[*index];
1648 if input.witness_utxo.is_none() && input.non_witness_utxo.is_none() {
1649 return Err(format!(
1650 "Security Violation: Requested input #{index} is missing UTXO metadata"
1651 ));
1652 }
1653 }
1654 }
1655
1656 for (index, input) in psbt.inputs.iter().enumerate() {
1657 if let Some(sighash) = input.sighash_type {
1658 let value = sighash.to_u32();
1659 let base_type = value & 0x1f;
1660 let anyone_can_pay = (value & 0x80) != 0;
1661 let is_allowed_base = base_type == 0 || base_type == 1; if anyone_can_pay || !is_allowed_base {
1664 return Err(format!(
1665 "Security Violation: Sighash type is not allowed on input #{index} (value={value})"
1666 ));
1667 }
1668 }
1669 }
1670
1671 let mut known_inscriptions: HashMap<(bitcoin::Txid, u32), Vec<(String, u64)>> =
1674 HashMap::new();
1675 for ins in &self.inscriptions {
1676 known_inscriptions
1677 .entry((ins.satpoint.outpoint.txid, ins.satpoint.outpoint.vout))
1678 .or_default()
1679 .push((ins.id.clone(), ins.satpoint.offset));
1680 }
1681 for items in known_inscriptions.values_mut() {
1683 items.sort_by_key(|(_, offset)| *offset);
1684 }
1685
1686 if let Err(e) = crate::ordinals::shield::audit_psbt(
1687 &psbt,
1688 &known_inscriptions,
1689 inputs_to_sign.as_deref(),
1690 self.vault_wallet.network(),
1691 ) {
1692 return Err(format!("Security Violation: {e}"));
1693 }
1694
1695 let original_psbt = if inputs_to_sign.is_some() {
1697 Some(psbt.clone())
1698 } else {
1699 None
1700 };
1701
1702 self.vault_wallet
1705 .sign(&mut psbt, bdk_options.clone())
1706 .map_err(|e| format!("Vault signing failed: {e}"))?;
1707
1708 if let Some(payment_wallet) = &self.payment_wallet {
1709 payment_wallet
1710 .sign(&mut psbt, bdk_options)
1711 .map_err(|e| format!("Payment signing failed: {e}"))?;
1712 }
1713
1714 self.sign_inscription_script_paths(&mut psbt, should_finalize, inputs_to_sign.as_deref())?;
1719
1720 if let Some(indices) = inputs_to_sign.as_ref() {
1722 let original = original_psbt
1724 .as_ref()
1725 .ok_or_else(|| "Security Violation: missing original PSBT snapshot".to_string())?;
1726 for (i, input) in psbt.inputs.iter_mut().enumerate() {
1727 if !indices.contains(&i) {
1728 *input = original.inputs[i].clone();
1729 }
1730 }
1731 }
1732
1733 if let Some(indices) = inputs_to_sign.as_ref() {
1734 let original = original_psbt
1735 .as_ref()
1736 .ok_or_else(|| "Security Violation: missing original PSBT snapshot".to_string())?;
1737 for index in indices {
1738 let before = &original.inputs[*index];
1739 let after = &psbt.inputs[*index];
1740
1741 let signature_changed = before.tap_key_sig != after.tap_key_sig
1742 || before.tap_script_sigs != after.tap_script_sigs
1743 || before.partial_sigs != after.partial_sigs
1744 || before.final_script_witness != after.final_script_witness;
1745
1746 if !signature_changed {
1747 return Err(format!(
1748 "Security Violation: Requested input #{index} was not signed by this wallet"
1749 ));
1750 }
1751 }
1752 }
1753
1754 let signed_bytes = psbt.serialize();
1757 let signed_base64 = base64::engine::general_purpose::STANDARD.encode(&signed_bytes);
1758
1759 Ok(signed_base64)
1760 }
1761
1762 pub fn prepare_external_sign_psbt(
1768 &self,
1769 psbt_base64: &str,
1770 options: Option<SignOptions>,
1771 ) -> Result<String, String> {
1772 use base64::Engine;
1773
1774 let psbt_bytes = base64::engine::general_purpose::STANDARD
1775 .decode(psbt_base64)
1776 .map_err(|e| format!("Invalid base64: {e}"))?;
1777
1778 let mut psbt = Psbt::deserialize(&psbt_bytes).map_err(|e| format!("Invalid PSBT: {e}"))?;
1779
1780 use std::collections::HashMap;
1781 let mut known_utxos = HashMap::new();
1782
1783 let collect_utxos = |w: &Wallet, map: &mut HashMap<bitcoin::OutPoint, bitcoin::TxOut>| {
1784 for utxo in w.list_unspent() {
1785 map.insert(utxo.outpoint, utxo.txout);
1786 }
1787 };
1788
1789 collect_utxos(&self.vault_wallet, &mut known_utxos);
1790 if let Some(w) = &self.payment_wallet {
1791 collect_utxos(w, &mut known_utxos);
1792 }
1793
1794 for (i, input) in psbt.inputs.iter_mut().enumerate() {
1795 if input.witness_utxo.is_none() && input.non_witness_utxo.is_none() {
1796 let outpoint = psbt.unsigned_tx.input[i].previous_output;
1797 if let Some(txout) = known_utxos.get(&outpoint) {
1798 input.witness_utxo = Some(txout.clone());
1799 }
1800 }
1801 }
1802
1803 #[allow(deprecated)]
1804 let _ = self
1805 .vault_wallet
1806 .sign(&mut psbt, bdk_wallet::SignOptions::default());
1807 if let Some(w) = &self.payment_wallet {
1808 #[allow(deprecated)]
1809 let _ = w.sign(&mut psbt, bdk_wallet::SignOptions::default());
1810 }
1811
1812 if let Some(opts) = &options {
1813 if let Some(sighash_u8) = opts.sighash {
1814 let target_sighash = bitcoin::psbt::PsbtSighashType::from_u32(sighash_u8 as u32);
1815 for input in psbt.inputs.iter_mut() {
1816 input.sighash_type = Some(target_sighash);
1817 }
1818 }
1819 }
1820
1821 let inputs_to_sign = options.as_ref().and_then(|o| o.sign_inputs.clone());
1822 if let Some(indices) = inputs_to_sign.as_ref() {
1823 let mut seen = std::collections::HashSet::new();
1824 for index in indices {
1825 if *index >= psbt.inputs.len() {
1826 return Err(format!(
1827 "Security Violation: sign_inputs index {} is out of bounds for {} inputs",
1828 index,
1829 psbt.inputs.len()
1830 ));
1831 }
1832 if !seen.insert(*index) {
1833 return Err(format!(
1834 "Security Violation: sign_inputs index {} is duplicated",
1835 index
1836 ));
1837 }
1838 let input = &psbt.inputs[*index];
1839 if input.witness_utxo.is_none() && input.non_witness_utxo.is_none() {
1840 return Err(format!(
1841 "Security Violation: Requested input #{} is missing UTXO metadata",
1842 index
1843 ));
1844 }
1845 }
1846 }
1847
1848 for (index, input) in psbt.inputs.iter().enumerate() {
1849 if let Some(sighash) = input.sighash_type {
1850 let value = sighash.to_u32();
1851 let base_type = value & 0x1f;
1852 let anyone_can_pay = (value & 0x80) != 0;
1853 let is_allowed_base = base_type == 0 || base_type == 1; if anyone_can_pay || !is_allowed_base {
1856 return Err(format!(
1857 "Security Violation: Sighash type is not allowed on input #{} (value={})",
1858 index, value
1859 ));
1860 }
1861 }
1862 }
1863
1864 let mut known_inscriptions: HashMap<(bitcoin::Txid, u32), Vec<(String, u64)>> =
1865 HashMap::new();
1866 for ins in &self.inscriptions {
1867 known_inscriptions
1868 .entry((ins.satpoint.outpoint.txid, ins.satpoint.outpoint.vout))
1869 .or_default()
1870 .push((ins.id.clone(), ins.satpoint.offset));
1871 }
1872 for items in known_inscriptions.values_mut() {
1873 items.sort_by_key(|(_, offset)| *offset);
1874 }
1875
1876 if let Err(e) = crate::ordinals::shield::audit_psbt(
1877 &psbt,
1878 &known_inscriptions,
1879 inputs_to_sign.as_deref(),
1880 self.vault_wallet.network(),
1881 ) {
1882 return Err(format!("Security Violation: {}", e));
1883 }
1884
1885 let prepared_bytes = psbt.serialize();
1886 Ok(base64::engine::general_purpose::STANDARD.encode(&prepared_bytes))
1887 }
1888
1889 pub fn verify_external_signed_psbt(
1894 &self,
1895 original_psbt_base64: &str,
1896 signed_psbt_base64: &str,
1897 required_input_indices: Option<&[usize]>,
1898 finalize: bool,
1899 ) -> Result<String, String> {
1900 use base64::Engine;
1901 use bitcoin::consensus::Encodable;
1902
1903 let decode = |b64: &str, label: &str| -> Result<Psbt, String> {
1904 let bytes = base64::engine::general_purpose::STANDARD
1905 .decode(b64)
1906 .map_err(|e| format!("Invalid base64 in {label}: {e}"))?;
1907 Psbt::deserialize(&bytes).map_err(|e| format!("Invalid PSBT in {label}: {e}"))
1908 };
1909
1910 let original = decode(original_psbt_base64, "original")?;
1911 let mut signed = decode(signed_psbt_base64, "signed")?;
1912
1913 let mut orig_tx_bytes = Vec::new();
1914 original
1915 .unsigned_tx
1916 .consensus_encode(&mut orig_tx_bytes)
1917 .map_err(|e| format!("Failed to encode original tx: {e}"))?;
1918
1919 let mut signed_tx_bytes = Vec::new();
1920 signed
1921 .unsigned_tx
1922 .consensus_encode(&mut signed_tx_bytes)
1923 .map_err(|e| format!("Failed to encode signed tx: {e}"))?;
1924
1925 if orig_tx_bytes != signed_tx_bytes {
1926 return Err(
1927 "Security Violation: Device returned a PSBT with a modified transaction. \
1928 The unsigned_tx bytes do not match the original."
1929 .to_string(),
1930 );
1931 }
1932
1933 let check_indices: Vec<usize> = required_input_indices
1934 .map(|v| v.to_vec())
1935 .unwrap_or_else(|| (0..signed.inputs.len()).collect());
1936
1937 for &idx in &check_indices {
1938 if idx >= signed.inputs.len() {
1939 return Err(format!(
1940 "Security Violation: required input index {} is out of bounds",
1941 idx
1942 ));
1943 }
1944
1945 let input = &signed.inputs[idx];
1946 let has_signature = input.tap_key_sig.is_some()
1947 || !input.tap_script_sigs.is_empty()
1948 || !input.partial_sigs.is_empty()
1949 || input.final_script_witness.is_some();
1950
1951 if !has_signature {
1952 return Err(format!(
1953 "Security Violation: Required input #{} was not signed by the device",
1954 idx
1955 ));
1956 }
1957 }
1958
1959 if required_input_indices.is_some() {
1960 let required_set: std::collections::HashSet<usize> =
1961 check_indices.iter().copied().collect();
1962
1963 for (i, (orig_input, signed_input)) in
1964 original.inputs.iter().zip(signed.inputs.iter()).enumerate()
1965 {
1966 if required_set.contains(&i) {
1967 continue;
1968 }
1969
1970 let signatures_changed = orig_input.tap_key_sig != signed_input.tap_key_sig
1971 || orig_input.tap_script_sigs != signed_input.tap_script_sigs
1972 || orig_input.partial_sigs != signed_input.partial_sigs
1973 || orig_input.final_script_witness != signed_input.final_script_witness;
1974
1975 if signatures_changed {
1976 return Err(format!(
1977 "Security Violation: Input #{} received an unauthorized signature \
1978 (not in required_input_indices)",
1979 i
1980 ));
1981 }
1982 }
1983 }
1984
1985 if !finalize {
1986 for input in signed.inputs.iter_mut() {
1993 input.bip32_derivation.clear();
1994 input.tap_key_origins.clear();
1995 }
1996 }
1997
1998 if finalize {
1999 for input in signed.inputs.iter_mut() {
2000 if let Some(sig) = input.tap_key_sig {
2001 let mut witness = bitcoin::Witness::new();
2002 witness.push(sig.to_vec());
2003 input.final_script_witness = Some(witness);
2004 input.tap_key_sig = None;
2005 input.tap_internal_key = None;
2006 input.tap_merkle_root = None;
2007 input.tap_key_origins.clear();
2008 input.witness_utxo = None;
2009 input.sighash_type = None;
2010 } else if !input.partial_sigs.is_empty() {
2011 if let Some((pubkey, sig)) = input.partial_sigs.iter().next() {
2012 let mut witness = bitcoin::Witness::new();
2013 witness.push(sig.to_vec());
2014 witness.push(pubkey.to_bytes());
2015 input.final_script_witness = Some(witness);
2016 input.partial_sigs.clear();
2017 input.bip32_derivation.clear();
2018 input.witness_utxo = None;
2019 input.sighash_type = None;
2020 }
2021 }
2022 }
2023 }
2024
2025 let verified_bytes = signed.serialize();
2026 Ok(base64::engine::general_purpose::STANDARD.encode(&verified_bytes))
2027 }
2028
2029 pub fn analyze_psbt(&self, psbt_base64: &str) -> Result<String, String> {
2032 use crate::ordinals::shield::analyze_psbt;
2034 use base64::Engine;
2035 use std::collections::HashMap;
2036
2037 let psbt_bytes = base64::engine::general_purpose::STANDARD
2039 .decode(psbt_base64)
2040 .map_err(|e| format!("Invalid base64: {e}"))?;
2041
2042 let mut psbt = match Psbt::deserialize(&psbt_bytes) {
2043 Ok(p) => p,
2044 Err(e) => {
2045 return Err(format!("Invalid PSBT: {e}"));
2046 }
2047 };
2048
2049 let mut known_utxos = HashMap::new();
2052
2053 let collect_utxos = |w: &Wallet, map: &mut HashMap<bitcoin::OutPoint, bitcoin::TxOut>| {
2054 for utxo in w.list_unspent() {
2055 map.insert(utxo.outpoint, utxo.txout);
2056 }
2057 };
2058
2059 collect_utxos(&self.vault_wallet, &mut known_utxos);
2060 if let Some(w) = &self.payment_wallet {
2061 collect_utxos(w, &mut known_utxos);
2062 }
2063
2064 for (i, input) in psbt.inputs.iter_mut().enumerate() {
2065 if input.witness_utxo.is_none() && input.non_witness_utxo.is_none() {
2066 let outpoint = psbt.unsigned_tx.input[i].previous_output;
2067 if let Some(txout) = known_utxos.get(&outpoint) {
2068 input.witness_utxo = Some(txout.clone());
2069 }
2070 }
2071 }
2072
2073 let mut known_inscriptions: HashMap<(bitcoin::Txid, u32), Vec<(String, u64)>> =
2076 HashMap::new();
2077
2078 for ins in &self.inscriptions {
2101 known_inscriptions
2102 .entry((ins.satpoint.outpoint.txid, ins.satpoint.outpoint.vout))
2103 .or_default()
2104 .push((ins.id.clone(), ins.satpoint.offset));
2105 }
2106
2107 for items in known_inscriptions.values_mut() {
2109 items.sort_by_key(|(_, offset)| *offset);
2110 }
2111
2112 let result = match analyze_psbt(&psbt, &known_inscriptions, self.vault_wallet.network()) {
2113 Ok(r) => r,
2114 Err(e) => {
2115 return Err(e.to_string());
2116 }
2117 };
2118
2119 serde_json::to_string(&result).map_err(|e| e.to_string())
2120 }
2121
2122 pub async fn broadcast(
2125 &mut self,
2126 signed_psbt_base64: &str,
2127 esplora_url: &str,
2128 ) -> Result<String, String> {
2129 use base64::Engine;
2130
2131 let psbt_bytes = base64::engine::general_purpose::STANDARD
2133 .decode(signed_psbt_base64)
2134 .map_err(|e| format!("Invalid base64: {e}"))?;
2135
2136 let psbt = Psbt::deserialize(&psbt_bytes).map_err(|e| format!("Invalid PSBT: {e}"))?;
2137
2138 let tx: Transaction = psbt
2140 .extract_tx()
2141 .map_err(|e| format!("Failed to extract tx: {e}"))?;
2142
2143 let client = esplora_client::Builder::new(esplora_url.trim_end_matches('/'))
2145 .build_async_with_sleeper::<SyncSleeper>()
2146 .map_err(|e| format!("Failed to create client: {e:?}"))?;
2147
2148 let broadcast_res: Result<(), _> = client.broadcast(&tx).await;
2149
2150 broadcast_res.map_err(|e| format!("Broadcast failed: {e}"))?;
2151
2152 Ok(tx.compute_txid().to_string())
2153 }
2154
2155 pub fn sign_message(&self, address: &str, message: &str) -> Result<String, String> {
2158 use base64::Engine;
2159 use bitcoin::hashes::Hash;
2160 use bitcoin::secp256k1::{Message, Secp256k1};
2161
2162 if let Some(watched) = self.watched_address() {
2163 if watched.to_string() == address {
2164 let _ = message;
2165 return Err(ZincError::CapabilityMissing.to_string());
2166 }
2167 }
2168
2169 let active_receive_index = self.active_receive_index();
2171 let vault_addr = self
2172 .vault_wallet
2173 .peek_address(KeychainKind::External, active_receive_index)
2174 .address
2175 .to_string();
2176
2177 let (is_vault, is_payment) = if address == vault_addr {
2178 (true, false)
2179 } else if let Some(w) = &self.payment_wallet {
2180 let pay_addr = w
2181 .peek_address(KeychainKind::External, active_receive_index)
2182 .address
2183 .to_string();
2184 (false, address == pay_addr)
2185 } else {
2186 (false, false)
2187 };
2188
2189 if !is_vault && !is_payment {
2190 return Err("Address not found in wallet".to_string());
2191 }
2192
2193 let secp = Secp256k1::new();
2195 let (purpose, chain) = if is_vault {
2197 (86, 0)
2198 } else {
2199 (self.dual_payment_purpose(), 0)
2200 };
2201 let priv_key = self
2202 .derive_private_key(purpose, chain, 0)
2203 .map_err(|_| ZincError::CapabilityMissing.to_string())?;
2204
2205 let signature_hash = bitcoin::sign_message::signed_msg_hash(message);
2207 let msg = Message::from_digest(signature_hash.to_byte_array());
2208
2209 let sig = secp.sign_ecdsa_recoverable(&msg, &priv_key);
2210 let (rec_id, sig_bytes_compact) = sig.serialize_compact();
2211
2212 let mut header = 27 + u8::try_from(rec_id.to_i32()).unwrap();
2213 header += 4; let mut sig_bytes = Vec::with_capacity(65);
2216 sig_bytes.push(header);
2217 sig_bytes.extend_from_slice(&sig_bytes_compact);
2218
2219 Ok(base64::engine::general_purpose::STANDARD.encode(&sig_bytes))
2220 }
2221
2222 pub fn sign_bip322_simple_hex(&self, address: &str, message: &str) -> Result<String, String> {
2224 use bitcoin::PrivateKey;
2225
2226 if let Some(watched) = self.watched_address() {
2227 if watched.to_string() == address {
2228 let _ = message;
2229 return Err(ZincError::CapabilityMissing.to_string());
2230 }
2231 }
2232
2233 let active_receive_index = self.active_receive_index();
2234 let vault_addr = self
2235 .vault_wallet
2236 .peek_address(KeychainKind::External, active_receive_index)
2237 .address
2238 .to_string();
2239
2240 let (is_vault, is_payment) = if address == vault_addr {
2241 (true, false)
2242 } else if let Some(w) = &self.payment_wallet {
2243 let pay_addr = w
2244 .peek_address(KeychainKind::External, active_receive_index)
2245 .address
2246 .to_string();
2247 (false, address == pay_addr)
2248 } else {
2249 (false, false)
2250 };
2251
2252 if !is_vault && !is_payment {
2253 return Err("Address not found in wallet".to_string());
2254 }
2255
2256 let (purpose, chain) = if is_vault {
2257 (86, 0)
2258 } else {
2259 (self.dual_payment_purpose(), 0)
2260 };
2261 let secret_key = self
2262 .derive_private_key(purpose, chain, 0)
2263 .map_err(|_| ZincError::CapabilityMissing.to_string())?;
2264 let network = self.vault_wallet.network();
2265 let private_key = PrivateKey::new(secret_key, network);
2266 let witness = bip322::sign_simple(
2267 &address
2268 .parse::<bitcoin::Address<bitcoin::address::NetworkUnchecked>>()
2269 .map_err(|e| format!("invalid address: {e}"))?
2270 .require_network(network)
2271 .map_err(|e| format!("address network mismatch: {e}"))?,
2272 message,
2273 private_key,
2274 )
2275 .map_err(|e| format!("failed to sign BIP-322 message: {e}"))?;
2276 let bytes = bitcoin::consensus::serialize(&witness);
2277 Ok(hex::encode(bytes))
2278 }
2279
2280 pub fn get_pairing_secret_key_hex(&self) -> Result<String, String> {
2284 let key = self.derive_private_key(86, 0, 0)?;
2285 Ok(bytes_to_lower_hex(&key.secret_bytes()))
2286 }
2287 pub fn get_taproot_public_key(&self, index: u32) -> Result<String, String> {
2289 self.derive_public_key(86, index)
2290 }
2291
2292 pub fn get_payment_public_key(&self, index: u32) -> Result<String, String> {
2296 let purpose = if self.scheme == AddressScheme::Dual {
2298 self.dual_payment_purpose()
2299 } else {
2300 86
2301 };
2302 self.derive_public_key(purpose, index)
2303 }
2304
2305 fn derive_public_key(&self, purpose: u32, index: u32) -> Result<String, String> {
2306 let account = self.active_derivation_account();
2307 let effective_index = self.active_receive_index().saturating_add(index);
2308 self.derive_public_key_internal(
2309 purpose,
2310 self.vault_wallet.network(),
2311 account,
2312 effective_index,
2313 )
2314 }
2315
2316 fn derive_private_key(
2317 &self,
2318 purpose: u32,
2319 chain: u32,
2320 index: u32,
2321 ) -> Result<bitcoin::secp256k1::SecretKey, String> {
2322 let account = self.active_derivation_account();
2323 let effective_index = self.active_receive_index().saturating_add(index);
2324 self.derive_private_key_internal(purpose, account, chain, effective_index)
2325 }
2326
2327 fn derive_private_key_internal(
2328 &self,
2329 purpose: u32,
2330 account: u32,
2331 chain: u32,
2332 index: u32,
2333 ) -> Result<bitcoin::secp256k1::SecretKey, String> {
2334 use bitcoin::secp256k1::Secp256k1;
2335 let secp = Secp256k1::new();
2336
2337 match &self.kind {
2338 WalletKind::Seed { master_xprv } => {
2339 let network = self.vault_wallet.network();
2340 let coin_type = u32::from(network != Network::Bitcoin);
2341
2342 let derivation_path = [
2343 bdk_wallet::bitcoin::bip32::ChildNumber::from_hardened_idx(purpose).unwrap(),
2344 bdk_wallet::bitcoin::bip32::ChildNumber::from_hardened_idx(coin_type).unwrap(),
2345 bdk_wallet::bitcoin::bip32::ChildNumber::from_hardened_idx(account).unwrap(),
2346 bdk_wallet::bitcoin::bip32::ChildNumber::from_normal_idx(chain).unwrap(),
2347 bdk_wallet::bitcoin::bip32::ChildNumber::from_normal_idx(index).unwrap(),
2348 ];
2349
2350 let child_xprv = master_xprv
2351 .derive_priv(&secp, &derivation_path)
2352 .map_err(|e| format!("Key derivation failed: {e}"))?;
2353
2354 Ok(child_xprv.private_key)
2355 }
2356 _ => Err("Private key derivation not supported for this wallet kind".to_string()),
2357 }
2358 }
2359
2360 fn sign_inscription_script_paths(
2361 &self,
2362 psbt: &mut Psbt,
2363 finalize: bool,
2364 indices: Option<&[usize]>,
2365 ) -> Result<(), String> {
2366 use bitcoin::secp256k1::{Message, Secp256k1};
2367 use bitcoin::sighash::{Prevouts, SighashCache};
2368
2369 let secp = Secp256k1::new();
2370 let network = self.vault_wallet.network();
2371
2372 let mut prevouts = Vec::with_capacity(psbt.unsigned_tx.input.len());
2374 for (i, input) in psbt.inputs.iter().enumerate() {
2375 let utxo = input
2376 .witness_utxo
2377 .as_ref()
2378 .or_else(|| {
2379 input.non_witness_utxo.as_ref().and_then(|tx| {
2380 tx.output
2381 .get(psbt.unsigned_tx.input[i].previous_output.vout as usize)
2382 })
2383 })
2384 .ok_or_else(|| format!("Missing witness_utxo for input #{i}"))?;
2385 prevouts.push(utxo.clone());
2386 }
2387 let prevouts_all = Prevouts::All(&prevouts);
2388
2389 for i in 0..psbt.inputs.len() {
2391 if let Some(allowed) = indices {
2392 if !allowed.contains(&i) {
2393 continue;
2394 }
2395 }
2396
2397 let input = &mut psbt.inputs[i];
2398 if input.tap_key_sig.is_some() || !input.tap_script_sigs.is_empty() {
2399 continue; }
2401
2402 let mut key_found = false;
2404 for (pubkey, (_, origin)) in &input.tap_key_origins {
2405 if *origin.0.as_bytes() == [0, 0, 0, 0] {
2408 let account = self.active_derivation_account();
2410 let effective_index = self.active_receive_index();
2411 if let Ok(derived_pubkey_hex) =
2412 self.derive_public_key_internal(86, network, account, effective_index)
2413 {
2414 if pubkey.to_string() == derived_pubkey_hex {
2415 let priv_key = self.derive_private_key(86, 0, 0)?;
2417
2418 let mut cache = SighashCache::new(&psbt.unsigned_tx);
2419 let sighash_type = input
2420 .sighash_type
2421 .unwrap_or(bitcoin::psbt::PsbtSighashType::from_u32(0)); for (_control_block, (script, _)) in &input.tap_scripts {
2425 let leaf_hash = bitcoin::taproot::TapLeafHash::from_script(
2426 script,
2427 bitcoin::taproot::LeafVersion::TapScript,
2428 );
2429
2430 let tap_sighash_type = match sighash_type.to_u32() {
2432 0 => bitcoin::sighash::TapSighashType::Default,
2433 1 => bitcoin::sighash::TapSighashType::All,
2434 2 => bitcoin::sighash::TapSighashType::None,
2435 3 => bitcoin::sighash::TapSighashType::Single,
2436 0x81 => bitcoin::sighash::TapSighashType::AllPlusAnyoneCanPay,
2437 0x82 => bitcoin::sighash::TapSighashType::NonePlusAnyoneCanPay,
2438 0x83 => {
2439 bitcoin::sighash::TapSighashType::SinglePlusAnyoneCanPay
2440 }
2441 _ => bitcoin::sighash::TapSighashType::Default,
2442 };
2443
2444 let sighash = cache
2445 .taproot_script_spend_signature_hash(
2446 i,
2447 &prevouts_all,
2448 leaf_hash,
2449 tap_sighash_type,
2450 )
2451 .map_err(|e| format!("Sighash calculation failed: {e}"))?;
2452
2453 let msg = Message::from_digest(sighash.to_byte_array());
2454 let sig = secp.sign_schnorr(&msg, &priv_key.keypair(&secp));
2455
2456 let mut final_sig = sig.as_ref().to_vec();
2457 if tap_sighash_type != bitcoin::sighash::TapSighashType::Default {
2458 final_sig.push(tap_sighash_type as u8);
2459 }
2460
2461 input.tap_script_sigs.insert(
2462 (*pubkey, leaf_hash),
2463 bitcoin::taproot::Signature::from_slice(&final_sig).unwrap(),
2464 );
2465 key_found = true;
2466 }
2467 }
2468 }
2469 }
2470 }
2471
2472 if key_found && finalize {
2473 }
2476 }
2477
2478 Ok(())
2479 }
2480
2481 pub fn get_revealed_addresses(&self, keychain: KeychainKind) -> Vec<String> {
2483 let wallet = match keychain {
2484 KeychainKind::External => &self.vault_wallet,
2485 KeychainKind::Internal => &self.vault_wallet,
2486 };
2487 wallet
2488 .list_unused_addresses(keychain)
2489 .into_iter()
2490 .map(|info| info.address.to_string())
2491 .collect()
2492 }
2493
2494 fn flexible_full_scan_request(
2495 wallet: &Wallet,
2496 policy: ScanPolicy,
2497 now: u64,
2498 ) -> FullScanRequest<KeychainKind> {
2499 let mut builder = wallet.start_full_scan_at(now);
2502
2503 if policy.address_scan_depth > 0 {
2505 for keychain in [KeychainKind::External, KeychainKind::Internal] {
2506 let spks: Vec<(u32, bitcoin::ScriptBuf)> = (0..policy.address_scan_depth)
2508 .map(|i| (i, wallet.peek_address(keychain, i).script_pubkey()))
2509 .collect();
2510 builder = builder.spks_for_keychain(keychain, spks);
2511 }
2512 }
2513
2514 builder.build()
2515 }
2516}
2517
2518impl WalletBuilder {
2520 #[must_use]
2522 pub fn new(network: Network) -> Self {
2523 Self {
2524 network,
2525 kind: None,
2526 mode: ProfileMode::Seed,
2527 scheme: AddressScheme::Unified,
2528 derivation_mode: DerivationMode::Account,
2529 payment_address_type: PaymentAddressType::NativeSegwit,
2530 persistence: None,
2531 account_index: 0,
2532 scan_policy: ScanPolicy::default(),
2533 }
2534 }
2535
2536 pub fn from_mnemonic(network: Network, mnemonic: &ZincMnemonic) -> Self {
2538 use bdk_wallet::bitcoin::bip32::Xpriv;
2539 let seed = mnemonic.to_seed("");
2540 let master_xprv = Xpriv::new_master(network, seed.as_ref()).expect("valid seed");
2541 Self::new(network).kind(WalletKind::Seed { master_xprv })
2542 }
2543
2544 pub fn from_seed(network: Network, seed: Seed64) -> Self {
2546 use bdk_wallet::bitcoin::bip32::Xpriv;
2547 let master_xprv = Xpriv::new_master(network, seed.as_ref()).expect("valid seed");
2548 Self::new(network).kind(WalletKind::Seed { master_xprv })
2549 }
2550
2551 pub fn from_watch_only(network: Network) -> Self {
2553 Self::new(network).mode(ProfileMode::Watch)
2554 }
2555
2556 pub fn with_watch_address(mut self, address: &str) -> Result<Self, String> {
2558 let addr = address
2559 .parse::<Address<NetworkUnchecked>>()
2560 .map_err(|e| format!("Invalid address: {e}"))?
2561 .require_network(self.network)
2562 .map_err(|e| format!("Network mismatch: {e}"))?;
2563
2564 if addr.address_type() != Some(AddressType::P2tr) {
2565 return Err(
2566 "Address watch mode currently supports taproot (bc1p/tb1p/bcrt1p) addresses only"
2567 .to_string(),
2568 );
2569 }
2570
2571 self.kind = Some(WalletKind::WatchAddress(addr));
2572 Ok(self)
2573 }
2574
2575 pub fn with_xpub(mut self, xpub: &str) -> Result<Self, String> {
2577 let parsed = parse_extended_public_key(xpub)?;
2578 let taproot_desc = format!("tr({parsed}/0/*)");
2579 let payment_desc = payment_descriptor_for_xpub(&parsed, self.payment_address_type, 0);
2580 self.kind = Some(WalletKind::Hardware {
2581 fingerprint: [0, 0, 0, 0],
2582 taproot_external: taproot_desc,
2583 payment_external: Some(payment_desc),
2584 });
2585 Ok(self)
2586 }
2587
2588 pub fn with_taproot_xpub(mut self, xpub: &str) -> Result<Self, String> {
2590 let parsed = parse_extended_public_key(xpub)?;
2591 let taproot_desc = format!("tr({parsed}/0/*)");
2592
2593 let mut kind = self.kind.take().unwrap_or(WalletKind::Hardware {
2594 fingerprint: [0, 0, 0, 0],
2595 taproot_external: String::new(),
2596 payment_external: None,
2597 });
2598
2599 if let WalletKind::Hardware {
2600 ref mut taproot_external,
2601 ..
2602 } = kind
2603 {
2604 *taproot_external = taproot_desc;
2605 }
2606
2607 self.kind = Some(kind);
2608 Ok(self)
2609 }
2610
2611 pub fn with_payment_xpub(mut self, xpub: &str) -> Result<Self, String> {
2613 let parsed = parse_extended_public_key(xpub)?;
2614 let payment_desc = payment_descriptor_for_xpub(&parsed, self.payment_address_type, 0);
2615
2616 let mut kind = self.kind.take().unwrap_or(WalletKind::Hardware {
2617 fingerprint: [0, 0, 0, 0],
2618 taproot_external: String::new(),
2619 payment_external: None,
2620 });
2621
2622 if let WalletKind::Hardware {
2623 ref mut payment_external,
2624 ..
2625 } = kind
2626 {
2627 *payment_external = Some(payment_desc);
2628 }
2629
2630 self.kind = Some(kind);
2631 Ok(self)
2632 }
2633
2634 #[must_use]
2636 pub fn kind(mut self, kind: WalletKind) -> Self {
2637 self.kind = Some(kind);
2638 self
2639 }
2640
2641 #[must_use]
2643 pub fn mode(mut self, mode: ProfileMode) -> Self {
2644 self.mode = mode;
2645 self
2646 }
2647
2648 #[must_use]
2650 pub fn with_scheme(mut self, scheme: AddressScheme) -> Self {
2651 self.scheme = scheme;
2652 self
2653 }
2654
2655 #[must_use]
2657 pub fn with_derivation_mode(mut self, mode: DerivationMode) -> Self {
2658 self.derivation_mode = mode;
2659 self
2660 }
2661
2662 #[must_use]
2664 pub fn with_payment_address_type(mut self, address_type: PaymentAddressType) -> Self {
2665 self.payment_address_type = address_type;
2666 self
2667 }
2668
2669 #[must_use]
2671 pub fn with_account_index(mut self, index: u32) -> Self {
2672 self.account_index = index;
2673 self
2674 }
2675
2676 #[must_use]
2678 pub fn scan_policy(mut self, policy: ScanPolicy) -> Self {
2679 self.scan_policy = policy;
2680 self
2681 }
2682
2683 pub fn with_persistence(mut self, persistence_json: &str) -> Result<Self, String> {
2685 let persistence: ZincPersistence = serde_json::from_str(persistence_json)
2686 .map_err(|e| format!("Failed to parse persistence JSON: {e}"))?;
2687 self.persistence = Some(persistence);
2688 Ok(self)
2689 }
2690
2691 #[must_use]
2693 pub fn persistence(mut self, persistence: ZincPersistence) -> Self {
2694 self.persistence = Some(persistence);
2695 self
2696 }
2697
2698 pub fn build(self) -> Result<ZincWallet, String> {
2700 let kind = self
2701 .kind
2702 .ok_or_else(|| "Wallet identity must be set".to_string())?;
2703
2704 let mut scheme = self.scheme;
2705 if matches!(kind, WalletKind::WatchAddress(_)) {
2706 if scheme == AddressScheme::Dual {
2707 return Err("Address watch profiles support unified scheme only".to_string());
2708 }
2709 scheme = AddressScheme::Unified;
2710 }
2711
2712 let derivation_account = match self.derivation_mode {
2713 DerivationMode::Account => self.account_index,
2714 DerivationMode::Index => 0,
2715 };
2716
2717 let (vault_ext, vault_int, payment_ext, payment_int) = kind.derive_descriptors(
2718 scheme,
2719 self.payment_address_type,
2720 self.network,
2721 derivation_account,
2722 );
2723
2724 let (vault_wallet, loaded_vault_changeset) = if let Some(p) = &self.persistence {
2726 if let Some(changeset) = &p.taproot {
2727 let mut loader =
2728 Wallet::load().descriptor(KeychainKind::External, Some(vault_ext.clone()));
2729
2730 if !matches!(kind, WalletKind::WatchAddress(_)) {
2731 loader = loader.descriptor(KeychainKind::Internal, Some(vault_int.clone()));
2732 }
2733
2734 let res = loader
2735 .extract_keys()
2736 .load_wallet_no_persist(changeset.clone());
2737
2738 match res {
2739 Ok(Some(w)) => (w, changeset.clone()),
2740 Ok(None) | Err(_) => {
2741 let creator = if matches!(kind, WalletKind::WatchAddress(_)) {
2742 Wallet::create_single(vault_ext)
2743 } else {
2744 Wallet::create(vault_ext, vault_int)
2745 };
2746 let w = creator
2747 .network(self.network)
2748 .create_wallet_no_persist()
2749 .map_err(|e| format!("Failed to create taproot wallet: {e}"))?;
2750 (w, bdk_wallet::ChangeSet::default())
2751 }
2752 }
2753 } else {
2754 let creator = if matches!(kind, WalletKind::WatchAddress(_)) {
2755 Wallet::create_single(vault_ext)
2756 } else {
2757 Wallet::create(vault_ext, vault_int)
2758 };
2759 let w = creator
2760 .network(self.network)
2761 .create_wallet_no_persist()
2762 .map_err(|e| format!("Failed to create taproot wallet: {e}"))?;
2763 (w, bdk_wallet::ChangeSet::default())
2764 }
2765 } else {
2766 let creator = if matches!(kind, WalletKind::WatchAddress(_)) {
2767 Wallet::create_single(vault_ext)
2768 } else {
2769 Wallet::create(vault_ext, vault_int)
2770 };
2771 let w = creator
2772 .network(self.network)
2773 .create_wallet_no_persist()
2774 .map_err(|e| format!("Failed to create taproot wallet: {e}"))?;
2775 (w, bdk_wallet::ChangeSet::default())
2776 };
2777
2778 let (payment_wallet, loaded_payment_changeset) =
2780 if let (Some(pay_ext), Some(pay_int)) = (payment_ext, payment_int) {
2781 let (wallet, changeset) = if let Some(p) = &self.persistence {
2782 if let Some(changeset) = &p.payment {
2783 let res = Wallet::load()
2784 .descriptor(KeychainKind::External, Some(pay_ext.clone()))
2785 .descriptor(KeychainKind::Internal, Some(pay_int.clone()))
2786 .extract_keys()
2787 .load_wallet_no_persist(changeset.clone());
2788
2789 match res {
2790 Ok(Some(w)) => (w, Some(changeset.clone())),
2791 Ok(None) | Err(_) => {
2792 let w = Wallet::create(pay_ext, pay_int)
2793 .network(self.network)
2794 .create_wallet_no_persist()
2795 .map_err(|e| format!("Failed to create payment wallet: {e}"))?;
2796 (w, None)
2797 }
2798 }
2799 } else {
2800 let w = Wallet::create(pay_ext, pay_int)
2801 .network(self.network)
2802 .create_wallet_no_persist()
2803 .map_err(|e| format!("Failed to create payment wallet: {e}"))?;
2804 (w, None)
2805 }
2806 } else {
2807 let w = Wallet::create(pay_ext, pay_int)
2808 .network(self.network)
2809 .create_wallet_no_persist()
2810 .map_err(|e| format!("Failed to create payment wallet: {e}"))?;
2811 (w, None)
2812 };
2813 (Some(wallet), changeset)
2814 } else {
2815 (None, None)
2816 };
2817
2818 Ok(ZincWallet {
2819 vault_wallet,
2820 payment_wallet,
2821 scheme: self.scheme,
2822 derivation_mode: self.derivation_mode,
2823 payment_address_type: self.payment_address_type,
2824 loaded_vault_changeset,
2825 loaded_payment_changeset,
2826 account_index: self.account_index,
2827 mode: self.mode,
2828 scan_policy: self.scan_policy,
2829 inscribed_utxos: std::collections::HashSet::default(),
2830 inscriptions: Vec::new(),
2831 rune_balances: Vec::new(),
2832 ordinals_verified: false,
2833 ordinals_metadata_complete: false,
2834 kind,
2835 is_syncing: false,
2836 account_generation: 0,
2837 })
2838 }
2839
2840 pub fn build_hardware(
2842 self,
2843 fingerprint_hex: &str,
2844 taproot_external_desc: String,
2845 taproot_internal_desc: String,
2846 payment_external_desc: Option<String>,
2847 payment_internal_desc: Option<String>,
2848 ) -> Result<ZincWallet, String> {
2849 let fingerprint_vec =
2850 hex::decode(fingerprint_hex).map_err(|e| format!("Invalid fingerprint hex: {e}"))?;
2851 let fingerprint: [u8; 4] = fingerprint_vec
2852 .try_into()
2853 .map_err(|_| "Fingerprint must be 4 bytes".to_string())?;
2854
2855 let network = self.network;
2856 let account_index = self.account_index;
2857 let persistence = self.persistence;
2858
2859 let scheme = if payment_external_desc.is_some() {
2860 AddressScheme::Dual
2861 } else {
2862 AddressScheme::Unified
2863 };
2864
2865 let (vault_wallet, loaded_vault_changeset) = if let Some(p) = &persistence {
2867 if let Some(changeset) = &p.taproot {
2868 let res = Wallet::load()
2869 .descriptor(KeychainKind::External, Some(taproot_external_desc.clone()))
2870 .descriptor(KeychainKind::Internal, Some(taproot_internal_desc.clone()))
2871 .extract_keys()
2872 .load_wallet_no_persist(changeset.clone());
2873
2874 match res {
2875 Ok(Some(w)) => (w, changeset.clone()),
2876 Ok(None) | Err(_) => {
2877 let w = Wallet::create(
2878 taproot_external_desc.clone(),
2879 taproot_internal_desc.clone(),
2880 )
2881 .network(network)
2882 .create_wallet_no_persist()
2883 .map_err(|e| {
2884 format!("Failed to create taproot wallet from descriptor: {e}")
2885 })?;
2886 (w, bdk_wallet::ChangeSet::default())
2887 }
2888 }
2889 } else {
2890 let w =
2891 Wallet::create(taproot_external_desc.clone(), taproot_internal_desc.clone())
2892 .network(network)
2893 .create_wallet_no_persist()
2894 .map_err(|e| {
2895 format!("Failed to create taproot wallet from descriptor: {e}")
2896 })?;
2897 (w, bdk_wallet::ChangeSet::default())
2898 }
2899 } else {
2900 let w = Wallet::create(taproot_external_desc.clone(), taproot_internal_desc.clone())
2901 .network(network)
2902 .create_wallet_no_persist()
2903 .map_err(|e| format!("Failed to create taproot wallet from descriptor: {e}"))?;
2904 (w, bdk_wallet::ChangeSet::default())
2905 };
2906
2907 let (payment_wallet, loaded_payment_changeset) = if let (Some(pay_ext), Some(pay_int)) =
2909 (&payment_external_desc, &payment_internal_desc)
2910 {
2911 let (wallet, changeset) = if let Some(p) = &persistence {
2912 if let Some(changeset) = &p.payment {
2913 let res = Wallet::load()
2914 .descriptor(KeychainKind::External, Some(pay_ext.clone()))
2915 .descriptor(KeychainKind::Internal, Some(pay_int.clone()))
2916 .extract_keys()
2917 .load_wallet_no_persist(changeset.clone());
2918
2919 match res {
2920 Ok(Some(w)) => (w, Some(changeset.clone())),
2921 Ok(None) | Err(_) => {
2922 let w = Wallet::create(pay_ext.clone(), pay_int.clone())
2923 .network(network)
2924 .create_wallet_no_persist()
2925 .map_err(|e| {
2926 format!("Failed to create payment wallet from descriptor: {e}")
2927 })?;
2928 (w, None)
2929 }
2930 }
2931 } else {
2932 let w = Wallet::create(pay_ext.clone(), pay_int.clone())
2933 .network(network)
2934 .create_wallet_no_persist()
2935 .map_err(|e| {
2936 format!("Failed to create payment wallet from descriptor: {e}")
2937 })?;
2938 (w, None)
2939 }
2940 } else {
2941 let w = Wallet::create(pay_ext.clone(), pay_int.clone())
2942 .network(network)
2943 .create_wallet_no_persist()
2944 .map_err(|e| format!("Failed to create payment wallet from descriptor: {e}"))?;
2945 (w, None)
2946 };
2947 (Some(wallet), changeset)
2948 } else {
2949 (None, None)
2950 };
2951
2952 Ok(ZincWallet {
2953 vault_wallet,
2954 payment_wallet,
2955 scheme,
2956 derivation_mode: self.derivation_mode,
2957 payment_address_type: self.payment_address_type,
2958 loaded_vault_changeset,
2959 loaded_payment_changeset,
2960 account_index,
2961 mode: ProfileMode::Watch,
2962 scan_policy: self.scan_policy,
2963 inscribed_utxos: std::collections::HashSet::default(),
2964 inscriptions: Vec::new(),
2965 rune_balances: Vec::new(),
2966 ordinals_verified: false,
2967 ordinals_metadata_complete: false,
2968 kind: WalletKind::Hardware {
2969 fingerprint,
2970 taproot_external: taproot_external_desc,
2971 payment_external: payment_external_desc,
2972 },
2973 is_syncing: false,
2974 account_generation: 0,
2975 })
2976 }
2977}
2978
2979#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2981pub struct ZincPersistence {
2982 pub taproot: Option<bdk_wallet::ChangeSet>,
2984 pub payment: Option<bdk_wallet::ChangeSet>,
2986}
2987
2988fn bytes_to_lower_hex(bytes: &[u8]) -> String {
2989 let mut s = String::with_capacity(bytes.len() * 2);
2990 for &b in bytes {
2991 use std::fmt::Write;
2992 write!(&mut s, "{:02x}", b).unwrap();
2993 }
2994 s
2995}
2996
2997#[cfg(test)]
2998mod tests {
2999 use super::*;
3000 use bitcoin::Network;
3001
3002 #[test]
3003 fn test_builder_basic() {
3004 use bdk_wallet::bitcoin::bip32::Xpriv;
3005 let mnemonic = ZincMnemonic::generate(12).unwrap();
3006 let seed = mnemonic.to_seed("");
3007 let master_xprv = Xpriv::new_master(Network::Signet, seed.as_ref()).expect("valid seed");
3008
3009 let wallet = WalletBuilder::new(Network::Signet)
3010 .kind(WalletKind::Seed { master_xprv })
3011 .build()
3012 .unwrap();
3013
3014 assert_eq!(wallet.vault_wallet.network(), Network::Signet);
3015 assert!(wallet.is_unified());
3016 }
3017
3018 #[test]
3019 fn flexible_full_scan_request_uses_explicit_start_time() {
3020 use bdk_wallet::bitcoin::bip32::Xpriv;
3021 let mnemonic = ZincMnemonic::generate(12).unwrap();
3022 let seed = mnemonic.to_seed("");
3023 let master_xprv = Xpriv::new_master(Network::Signet, seed.as_ref()).expect("valid seed");
3024 let wallet = WalletBuilder::new(Network::Signet)
3025 .kind(WalletKind::Seed { master_xprv })
3026 .build()
3027 .unwrap();
3028
3029 let explicit_start = 1_777_777_777_u64;
3030 let req = ZincWallet::flexible_full_scan_request(
3031 &wallet.vault_wallet,
3032 ScanPolicy::default(),
3033 explicit_start,
3034 );
3035 assert_eq!(req.start_time(), explicit_start);
3036 }
3037}