1use serde::Serialize;
25use std::future::Future;
26use wasm_bindgen::prelude::*;
27
28#[macro_use]
29mod logging;
30
31pub mod builder;
33pub mod crypto;
34pub mod error;
35pub mod history;
37pub mod keys;
38pub mod offer;
40pub mod offer_accept;
42pub mod offer_create;
44pub mod offer_nostr;
46#[cfg(not(target_arch = "wasm32"))]
48pub mod offer_relay;
49pub mod ordinals;
51
52pub use builder::{
54 Account, AddressScheme, CreatePsbtRequest, CreatePsbtTransportRequest, DiscoveryAccountPlan,
55 DiscoveryContext, Seed64, SignOptions, SyncRequestType, SyncSleeper, WalletBuilder,
56 ZincBalance, ZincPersistence, ZincSyncRequest, ZincWallet,
57};
58pub use error::{ZincError, ZincResult};
59pub use history::TxItem;
60pub use keys::{taproot_descriptors, DescriptorPair, ZincMnemonic};
61pub use offer::OfferEnvelopeV1;
62pub use offer_accept::{prepare_offer_acceptance, OfferAcceptancePlanV1};
63pub use offer_create::{CreateOfferRequest, OfferCreateResultV1};
64pub use offer_nostr::{NostrOfferEvent, OFFER_EVENT_KIND};
65#[cfg(not(target_arch = "wasm32"))]
66pub use offer_relay::{NostrRelayClient, RelayPublishResult, RelayQueryOptions};
67pub use ordinals::client::OrdClient;
68pub use ordinals::types::{Inscription, Satpoint};
69
70pub use bdk_wallet::bitcoin::Network;
72use bdk_wallet::KeychainKind;
73
74#[doc(hidden)]
79pub struct WalletResult {
81 pub phrase: String,
83 pub words: Vec<String>,
85}
86
87#[doc(hidden)]
88pub fn generate_wallet_internal(word_count: u8) -> Result<WalletResult, ZincError> {
90 let mnemonic = ZincMnemonic::generate(word_count)?;
91 Ok(WalletResult {
92 phrase: mnemonic.phrase(),
93 words: mnemonic.words(),
94 })
95}
96
97#[doc(hidden)]
98pub fn validate_mnemonic_internal(phrase: &str) -> bool {
100 ZincMnemonic::parse(phrase).is_ok()
101}
102
103#[doc(hidden)]
104pub fn derive_address_internal(phrase: &str, network: Network) -> Result<String, ZincError> {
106 let mnemonic = ZincMnemonic::parse(phrase)?;
107 let descriptors = crate::keys::taproot_descriptors(&mnemonic, network)?;
108
109 let wallet = bdk_wallet::Wallet::create(
110 descriptors.external.to_string(),
111 descriptors.internal.to_string(),
112 )
113 .network(network)
114 .create_wallet_no_persist()
115 .map_err(|e| ZincError::BdkError(e.to_string()))?;
116
117 let address = wallet.peek_address(KeychainKind::External, 0);
118 Ok(address.address.to_string())
119}
120
121#[doc(hidden)]
122pub fn encrypt_wallet_internal(mnemonic: &str, password: &str) -> Result<String, ZincError> {
124 let m = ZincMnemonic::parse(mnemonic)?;
125 let encrypted = crypto::encrypt_seed(m.phrase().as_bytes(), password)?;
126 serde_json::to_string(&encrypted).map_err(|e| ZincError::SerializationError(e.to_string()))
127}
128
129#[doc(hidden)]
130pub fn decrypt_wallet_internal(
132 encrypted_json: &str,
133 password: &str,
134) -> Result<WalletResult, ZincError> {
135 let encrypted: crypto::EncryptedWallet = serde_json::from_str(encrypted_json)
136 .map_err(|e| ZincError::SerializationError(e.to_string()))?;
137
138 let plaintext = crypto::decrypt_seed(&encrypted, password)?;
139
140 let phrase = String::from_utf8(plaintext.to_vec())
141 .map_err(|e| ZincError::SerializationError(format!("Invalid UTF-8: {}", e)))?;
142
143 let mnemonic = ZincMnemonic::parse(&phrase)?;
144
145 Ok(WalletResult {
146 phrase: mnemonic.phrase(),
147 words: mnemonic.words(),
148 })
149}
150
151use std::sync::Once;
156
157static INIT: Once = Once::new();
158const LOG_TARGET_WASM: &str = "zinc_core::wasm";
159
160async fn account_is_active_from_receive_scan<F, Fut>(
161 address_scan_depth: u32,
162 mut has_activity_at: F,
163) -> bool
164where
165 F: FnMut(u32) -> Fut,
166 Fut: Future<Output = bool>,
167{
168 let depth = address_scan_depth.max(1);
169 const ADDRESS_SCAN_BATCH_SIZE: u32 = 20;
170
171 let mut batch_start = 0;
172 while batch_start < depth {
173 let batch_end = (batch_start + ADDRESS_SCAN_BATCH_SIZE).min(depth);
174 let mut checks = Vec::with_capacity((batch_end - batch_start) as usize);
175 for address_index in batch_start..batch_end {
176 checks.push(has_activity_at(address_index));
177 }
178
179 let results = futures_util::future::join_all(checks).await;
180 if results.into_iter().any(|is_active| is_active) {
181 return true;
182 }
183
184 batch_start = batch_end;
185 }
186
187 false
188}
189
190#[wasm_bindgen(start)]
192pub fn init() {
193 zinc_log_trace!(target: LOG_TARGET_WASM, "init invoked");
194 INIT.call_once(|| {
195 console_error_panic_hook::set_once();
197 zinc_log_info!(target: LOG_TARGET_WASM, "WASM module initialized");
198 });
199}
200
201#[wasm_bindgen]
203pub fn set_log_level(level: &str) -> Result<(), JsValue> {
204 let Some(parsed) = logging::parse_level(level) else {
205 zinc_log_warn!(
206 target: LOG_TARGET_WASM,
207 "rejected invalid log level request ({})",
208 logging::redacted_field("requested_level", level)
209 );
210 zinc_log_error!(
211 target: LOG_TARGET_WASM,
212 "invalid runtime log level request rejected"
213 );
214 return Err(JsValue::from_str(
215 "Invalid log level. Use one of: off, error, warn, info, debug, trace",
216 ));
217 };
218
219 logging::set_log_level(parsed);
220 zinc_log_info!(
221 target: LOG_TARGET_WASM,
222 "runtime log level updated to {}",
223 parsed.as_str()
224 );
225 Ok(())
226}
227
228#[wasm_bindgen]
230pub fn set_logging_enabled(enabled: bool) {
231 logging::set_logging_enabled(enabled);
232 zinc_log_info!(
233 target: LOG_TARGET_WASM,
234 "runtime logging {}",
235 if enabled { "enabled" } else { "disabled" }
236 );
237}
238
239#[wasm_bindgen]
241pub fn get_log_level() -> String {
242 logging::get_log_level().as_str().to_string()
243}
244
245#[wasm_bindgen]
247pub fn generate_wallet(word_count: u8) -> Result<JsValue, JsValue> {
248 let result =
249 generate_wallet_internal(word_count).map_err(|e| JsValue::from_str(&e.to_string()))?;
250
251 let js_result = serde_json::json!({
252 "words": result.words,
253 "phrase": result.phrase,
254 });
255
256 serde_wasm_bindgen::to_value(&js_result).map_err(|e| JsValue::from_str(&e.to_string()))
257}
258
259#[wasm_bindgen]
261pub fn validate_mnemonic(phrase: &str) -> bool {
262 validate_mnemonic_internal(phrase)
263}
264
265#[wasm_bindgen]
267pub fn derive_address(phrase: &str, network: &str) -> Result<String, JsValue> {
268 let network = match network {
269 "mainnet" | "bitcoin" => Network::Bitcoin,
270 "signet" => Network::Signet,
271 "testnet" => Network::Testnet,
272 "regtest" => Network::Regtest,
273 _ => return Err(JsValue::from_str("Invalid network")),
274 };
275
276 derive_address_internal(phrase, network).map_err(|e| JsValue::from_str(&e.to_string()))
277}
278
279#[wasm_bindgen]
281pub fn encrypt_wallet(mnemonic: &str, password: &str) -> Result<String, JsValue> {
282 encrypt_wallet_internal(mnemonic, password).map_err(|e| JsValue::from_str(&e.to_string()))
283}
284
285#[derive(Serialize)]
286pub struct DecryptResponse {
288 pub success: bool,
290 pub phrase: String,
292 pub words: Vec<String>,
294}
295
296#[wasm_bindgen]
298pub fn decrypt_wallet(encrypted_json: &str, password: &str) -> Result<JsValue, JsValue> {
299 zinc_log_debug!(target: LOG_TARGET_WASM,
300 "decrypt_wallet called. Encrypted length: {}, Password length: {}",
301 encrypted_json.len(),
302 password.len()
303 );
304
305 let result = match decrypt_wallet_internal(encrypted_json, password) {
306 Ok(res) => {
307 zinc_log_debug!(target: LOG_TARGET_WASM,
308 "Internal decryption success. Phrase length: {}",
309 res.phrase.len()
310 );
311 res
312 }
313 Err(e) => {
314 zinc_log_debug!(target: LOG_TARGET_WASM, "Internal decryption failed: {:?}", e);
315 return Err(JsValue::from_str(&e.to_string()));
316 }
317 };
318
319 let response = DecryptResponse {
320 success: true,
321 phrase: result.phrase,
322 words: result.words,
323 };
324
325 zinc_log_debug!(target: LOG_TARGET_WASM, "Serializing response...");
326 match serde_wasm_bindgen::to_value(&response) {
327 Ok(val) => {
328 zinc_log_debug!(target: LOG_TARGET_WASM, "Serialization success.");
329 Ok(val)
330 }
331 Err(e) => {
332 zinc_log_debug!(target: LOG_TARGET_WASM, "Serialization failed: {:?}", e);
333 Err(JsValue::from_str(&e.to_string()))
334 }
335 }
336}
337
338use std::cell::{Cell, RefCell};
343use std::rc::Rc;
344
345const VITALITY_MAGIC: u32 = 0x005a_11ad;
346#[cfg(target_arch = "wasm32")]
347const SYNC_STALE_ERROR: &str = "Wallet state changed during sync; stale result discarded";
348#[cfg(target_arch = "wasm32")]
349const ORD_SYNC_STALE_ERROR: &str =
350 "Wallet state changed during ordinals sync; stale result discarded";
351
352#[derive(Clone, Copy)]
353struct WalletState {
354 network: Network,
355 scheme: AddressScheme,
356 account_index: u32,
357}
358
359#[wasm_bindgen]
360pub struct ZincWasmWallet {
362 inner: Rc<RefCell<ZincWallet>>,
363 phrase: String, state: Cell<WalletState>,
365 vitality: u32,
366}
367
368#[wasm_bindgen]
369impl ZincWasmWallet {
370 #[wasm_bindgen(constructor)]
371 #[allow(clippy::needless_pass_by_value)]
372 pub fn new(
376 network: &str,
377 phrase: &str,
378 scheme_str: Option<String>,
379 persistence_json: Option<String>,
380 account_index: Option<u32>,
381 ) -> Result<ZincWasmWallet, JsValue> {
382 let network_enum = match network {
383 "mainnet" | "bitcoin" => Network::Bitcoin,
384 "signet" => Network::Signet,
385 "testnet" => Network::Testnet,
386 "regtest" => Network::Regtest,
387 _ => return Err(JsValue::from_str("Invalid network")),
388 };
389
390 let mnemonic =
391 ZincMnemonic::parse(phrase).map_err(|e| JsValue::from_str(&e.to_string()))?;
392
393 Self::init_wallet(
394 network_enum,
395 phrase,
396 mnemonic,
397 scheme_str,
398 persistence_json,
399 account_index,
400 )
401 }
402
403 #[wasm_bindgen]
405 pub fn new_encrypted(
406 network: &str,
407 encrypted_json: &str,
408 password: &str,
409 scheme_str: Option<String>,
410 persistence_json: Option<String>,
411 account_index: Option<u32>,
412 ) -> Result<ZincWasmWallet, JsValue> {
413 let network_enum = match network {
414 "mainnet" | "bitcoin" => Network::Bitcoin,
415 "signet" => Network::Signet,
416 "testnet" => Network::Testnet,
417 "regtest" => Network::Regtest,
418 _ => return Err(JsValue::from_str("Invalid network")),
419 };
420
421 let result = decrypt_wallet_internal(encrypted_json, password)
422 .map_err(|e| JsValue::from_str(&e.to_string()))?;
423
424 let mnemonic =
425 ZincMnemonic::parse(&result.phrase).map_err(|e| JsValue::from_str(&e.to_string()))?;
426
427 Self::init_wallet(
428 network_enum,
429 &result.phrase,
430 mnemonic,
431 scheme_str,
432 persistence_json,
433 account_index,
434 )
435 }
436
437 fn init_wallet(
438 network: Network,
439 phrase: &str,
440 mnemonic: ZincMnemonic,
441 scheme_str: Option<String>,
442 persistence_json: Option<String>,
443 account_index: Option<u32>,
444 ) -> Result<ZincWasmWallet, JsValue> {
445 let scheme = match scheme_str.as_deref() {
447 Some("dual") => AddressScheme::Dual,
448 _ => AddressScheme::Unified,
449 };
450
451 let active_index = account_index.unwrap_or(0);
452
453 let mut builder = WalletBuilder::from_mnemonic(network, &mnemonic);
454 builder = builder.with_scheme(scheme).with_account_index(active_index);
455
456 if let Some(json) = persistence_json {
457 builder = builder
458 .with_persistence(&json)
459 .map_err(|e| JsValue::from_str(&e))?;
460 }
461
462 let wallet = builder.build().map_err(|e| JsValue::from_str(&e))?;
463
464 Ok(ZincWasmWallet {
465 inner: Rc::new(RefCell::new(wallet)),
466 phrase: phrase.to_string(),
467 state: Cell::new(WalletState {
468 network,
469 scheme,
470 account_index: active_index,
471 }),
472 vitality: VITALITY_MAGIC,
473 })
474 }
475
476 fn check_vitality(&self) -> Result<(), JsValue> {
477 if self.vitality != VITALITY_MAGIC {
478 return Err(JsValue::from_str("Wallet handle is stale or corrupted due to context destruction. Please reload the extension."));
479 }
480 let sc = Rc::strong_count(&self.inner);
483 if sc == 0 {
484 return Err(JsValue::from_str(
485 "Internal error: Rc strong count is 0 (memory corruption). Please reload the extension."
486 ));
487 }
488 Ok(())
489 }
490
491 fn state_snapshot(&self) -> WalletState {
492 self.state.get()
493 }
494
495 fn replace_wallet(
496 &self,
497 mut next_wallet: ZincWallet,
498 next_state: WalletState,
499 busy_context: &str,
500 ) -> Result<(), JsValue> {
501 match self.inner.try_borrow_mut() {
502 Ok(mut inner) => {
503 next_wallet.account_generation = inner.account_generation().wrapping_add(1);
504 *inner = next_wallet;
505 self.state.set(next_state);
506 Ok(())
507 }
508 Err(e) => Err(JsValue::from_str(&format!(
509 "Wallet busy ({}): {}",
510 busy_context, e
511 ))),
512 }
513 }
514
515 #[cfg(target_arch = "wasm32")]
516 fn generation_mismatch_error(
517 inner_rc: &Rc<RefCell<ZincWallet>>,
518 expected_generation: u64,
519 message: &str,
520 ) -> Option<JsValue> {
521 match inner_rc.try_borrow() {
522 Ok(inner) if inner.account_generation() != expected_generation => {
523 Some(JsValue::from_str(message))
524 }
525 _ => None,
526 }
527 }
528
529 #[cfg(target_arch = "wasm32")]
530 fn clear_syncing_if_generation_matches(
531 inner_rc: &Rc<RefCell<ZincWallet>>,
532 expected_generation: u64,
533 ) {
534 if let Ok(mut inner) = inner_rc.try_borrow_mut() {
535 if inner.account_generation() == expected_generation {
536 inner.is_syncing = false;
537 }
538 }
539 }
540
541 pub fn export_changeset(&self) -> Result<String, JsValue> {
543 self.check_vitality()?;
544 zinc_log_debug!(target: LOG_TARGET_WASM, "export_changeset called (wrapper)");
545 let res = match self.inner.try_borrow() {
546 Ok(inner) => inner
547 .export_changeset()
548 .map_err(|e| JsValue::from_str(&e))
549 .and_then(|p| {
550 serde_json::to_string(&p).map_err(|e| JsValue::from_str(&e.to_string()))
551 }),
552 Err(e) => {
553 zinc_log_debug!(target: LOG_TARGET_WASM, "export_changeset failed to borrow: {:?}", e);
554 Err(JsValue::from_str(&format!(
555 "Wallet busy (export_changeset): {}",
556 e
557 )))
558 }
559 };
560 zinc_log_debug!(target: LOG_TARGET_WASM, "export_changeset finished (wrapper)");
561 res
562 }
563
564 pub fn set_scheme(&self, scheme_str: &str) -> Result<(), JsValue> {
567 self.check_vitality()?;
568 let new_scheme = match scheme_str {
569 "dual" => AddressScheme::Dual,
570 "unified" => AddressScheme::Unified,
571 _ => return Err(JsValue::from_str("Invalid scheme")),
572 };
573
574 let state = self.state_snapshot();
575 if state.scheme == new_scheme {
576 return Ok(());
577 }
578
579 let mnemonic =
580 ZincMnemonic::parse(&self.phrase).map_err(|e| JsValue::from_str(&e.to_string()))?;
581
582 let mut builder = WalletBuilder::from_mnemonic(state.network, &mnemonic);
583 builder = builder
584 .with_scheme(new_scheme)
585 .with_account_index(state.account_index);
586
587 let next_wallet = builder.build().map_err(|e| JsValue::from_str(&e))?;
588 self.replace_wallet(
589 next_wallet,
590 WalletState {
591 scheme: new_scheme,
592 ..state
593 },
594 "set_scheme",
595 )
596 }
597
598 pub fn set_active_account(&self, account_index: u32) -> Result<(), JsValue> {
602 self.check_vitality()?;
603 let state = self.state_snapshot();
604 if state.account_index == account_index {
605 return Ok(());
606 }
607
608 let mnemonic =
609 ZincMnemonic::parse(&self.phrase).map_err(|e| JsValue::from_str(&e.to_string()))?;
610
611 let mut builder = WalletBuilder::from_mnemonic(state.network, &mnemonic);
612 builder = builder
613 .with_scheme(state.scheme)
614 .with_account_index(account_index);
615
616 let next_wallet = builder.build().map_err(|e| JsValue::from_str(&e))?;
617 self.replace_wallet(
618 next_wallet,
619 WalletState {
620 account_index,
621 ..state
622 },
623 "set_active_account",
624 )
625 }
626
627 pub fn set_network(&self, network_str: &str) -> Result<(), JsValue> {
629 self.check_vitality()?;
630 let new_network = match network_str {
631 "mainnet" => Network::Bitcoin,
632 "testnet" => Network::Testnet,
633 "signet" => Network::Signet,
634 "regtest" => Network::Regtest,
635 _ => return Err(JsValue::from_str("Invalid network")),
636 };
637
638 let state = self.state_snapshot();
639 if state.network == new_network {
640 return Ok(());
641 }
642
643 let mnemonic =
644 ZincMnemonic::parse(&self.phrase).map_err(|e| JsValue::from_str(&e.to_string()))?;
645
646 let mut builder = WalletBuilder::from_mnemonic(new_network, &mnemonic);
647 builder = builder
648 .with_scheme(state.scheme)
649 .with_account_index(state.account_index);
650
651 let next_wallet = builder.build().map_err(|e| JsValue::from_str(&e))?;
652 self.replace_wallet(
653 next_wallet,
654 WalletState {
655 network: new_network,
656 ..state
657 },
658 "set_network",
659 )
660 }
661
662 #[wasm_bindgen(js_name = get_accounts)]
663 pub fn get_accounts(&self, count: u32) -> Result<JsValue, JsValue> {
665 self.check_vitality()?;
666
667 let mnemonic = crate::keys::ZincMnemonic::parse(&self.phrase)
669 .map_err(|e| JsValue::from_str(&e.to_string()))?;
670 let state = self.state_snapshot();
671 let network = state.network;
672 let scheme = state.scheme;
673
674 let mut accounts = Vec::new();
675 for i in 0..count {
676 let mut builder = WalletBuilder::from_mnemonic(network, &mnemonic);
677 builder = builder.with_scheme(scheme).with_account_index(i);
678
679 let zwallet = builder.build().map_err(|e| JsValue::from_str(&e))?;
681
682 let vault_addr = zwallet
684 .vault_wallet
685 .peek_address(KeychainKind::External, 0)
686 .address;
687
688 let vault_pubkey = zwallet
689 .get_taproot_public_key(0)
690 .unwrap_or_else(|_| "".to_string());
691
692 let (payment_addr, payment_pubkey) = if scheme == AddressScheme::Dual {
693 (
694 Some(
695 zwallet
696 .payment_wallet
697 .as_ref()
698 .ok_or_else(|| {
699 JsValue::from_str("Payment wallet missing in dual mode")
700 })?
701 .peek_address(KeychainKind::External, 0)
702 .address
703 .to_string(),
704 ),
705 Some(
706 zwallet
707 .get_payment_public_key(0)
708 .unwrap_or_else(|_| "".to_string()),
709 ),
710 )
711 } else {
712 (None, None)
713 };
714
715 accounts.push(serde_json::json!({
716 "index": i,
717 "label": format!("Account {}", i),
718 "taprootAddress": vault_addr.to_string(),
719 "taprootPublicKey": vault_pubkey,
720 "paymentAddress": payment_addr,
721 "paymentPublicKey": payment_pubkey,
722 "vaultAddress": vault_addr.to_string(),
724 "vaultPublicKey": vault_pubkey,
725 }));
726 }
727
728 serde_wasm_bindgen::to_value(&accounts).map_err(|e| JsValue::from_str(&e.to_string()))
729 }
730
731 pub fn get_inscriptions(&self) -> Result<JsValue, JsValue> {
733 self.check_vitality()?;
734 match self.inner.try_borrow() {
735 Ok(inner) => serde_wasm_bindgen::to_value(&inner.inscriptions).map_err(|e| {
736 JsValue::from_str(&format!("Failed to serialize inscriptions: {}", e))
737 }),
738 Err(e) => Err(JsValue::from_str(&format!(
739 "Wallet busy (get_inscriptions): {}",
740 e
741 ))),
742 }
743 }
744
745 pub fn get_balance(&self) -> Result<JsValue, JsValue> {
747 self.check_vitality()?;
748 match self.inner.try_borrow() {
749 Ok(inner) => {
750 let balance = inner.get_balance();
751 let json = serde_json::json!({
752 "total": {
753 "confirmed": balance.total.confirmed.to_sat(),
754 "trusted_pending": balance.total.trusted_pending.to_sat(),
755 "untrusted_pending": balance.total.untrusted_pending.to_sat(),
756 "immature": balance.total.immature.to_sat(),
757 },
758 "spendable": {
759 "confirmed": balance.spendable.confirmed.to_sat(),
760 "trusted_pending": balance.spendable.trusted_pending.to_sat(),
761 "untrusted_pending": balance.spendable.untrusted_pending.to_sat(),
762 "immature": balance.spendable.immature.to_sat(),
763 },
764 "display_spendable": {
765 "confirmed": balance.display_spendable.confirmed.to_sat(),
766 "trusted_pending": balance.display_spendable.trusted_pending.to_sat(),
767 "untrusted_pending": balance.display_spendable.untrusted_pending.to_sat(),
768 "immature": balance.display_spendable.immature.to_sat(),
769 },
770 "inscribed": balance.inscribed
771 });
772
773 let serializer =
775 serde_wasm_bindgen::Serializer::new().serialize_maps_as_objects(true);
776 json.serialize(&serializer)
777 .map_err(|e| JsValue::from_str(&e.to_string()))
778 }
779 Err(e) => Err(JsValue::from_str(&format!(
780 "Wallet busy (get_balance): {}",
781 e
782 ))),
783 }
784 }
785
786 pub fn get_transactions(&self, limit: usize) -> Result<JsValue, JsValue> {
788 self.check_vitality()?;
789 match self.inner.try_borrow() {
790 Ok(inner) => {
791 let txs = inner.get_transactions(limit);
792 serde_wasm_bindgen::to_value(&txs)
793 .map_err(|e| JsValue::from(format!("Failed to serialize transactions: {e}")))
794 }
795 Err(e) => Err(JsValue::from_str(&format!(
796 "Wallet busy (get_transactions): {}",
797 e
798 ))),
799 }
800 }
801
802 pub fn get_addresses(&self) -> Result<JsValue, JsValue> {
806 self.check_vitality()?;
807 match self.inner.try_borrow() {
808 Ok(inner) => {
809 let account_idx = inner.account_index;
810 let vault_addr = inner
811 .vault_wallet
812 .peek_address(KeychainKind::External, 0)
813 .address;
814 let vault_pubkey = inner
815 .get_taproot_public_key(0)
816 .unwrap_or_else(|_| "".to_string());
817
818 zinc_log_debug!(
819 target: LOG_TARGET_WASM,
820 "get_addresses - account: {}, taproot: {}",
821 account_idx,
822 vault_addr
823 );
824
825 let (payment_addr, payment_pubkey) = if inner.is_unified() {
827 (Some(vault_addr.to_string()), Some(vault_pubkey.clone()))
828 } else {
829 let addr = inner
830 .payment_wallet
831 .as_ref()
832 .ok_or_else(|| JsValue::from_str("Payment wallet missing in dual mode"))?
833 .peek_address(KeychainKind::External, 0)
834 .address;
835 let pubkey = inner
836 .get_payment_public_key(0)
837 .unwrap_or_else(|_| "".to_string());
838 zinc_log_debug!(target: LOG_TARGET_WASM, "get_addresses - payment: {}", addr);
839 (Some(addr.to_string()), Some(pubkey))
840 };
841
842 let json = serde_json::json!({
843 "account_index": account_idx,
844 "taproot": vault_addr.to_string(),
845 "taprootPublicKey": vault_pubkey,
846 "payment": payment_addr,
847 "paymentPublicKey": payment_pubkey,
848 "vault": vault_addr.to_string(),
850 "vaultPublicKey": vault_pubkey
851 });
852 serde_wasm_bindgen::to_value(&json).map_err(|e| JsValue::from(e.to_string()))
853 }
854 Err(e) => Err(JsValue::from_str(&format!(
855 "Wallet busy (get_addresses): {}",
856 e
857 ))),
858 }
859 }
860
861 #[cfg(target_arch = "wasm32")]
862 #[wasm_bindgen(js_name = sync)]
863 pub fn sync(&self, esplora_url: String) -> Result<js_sys::Promise, JsValue> {
864 self.check_vitality()?;
865 use crate::builder::{SyncRequestType, SyncSleeper};
866 use bdk_esplora::EsploraAsyncExt;
867
868 let inner_rc = self.inner.clone();
869
870 Ok(wasm_bindgen_futures::future_to_promise(async move {
871 zinc_log_debug!(
872 target: LOG_TARGET_WASM,
873 "sync start ({})",
874 logging::redacted_field("esplora_url", &esplora_url)
875 );
876
877 let (sync_req, sync_generation) = {
879 match inner_rc.try_borrow_mut() {
880 Ok(mut inner) => {
881 if inner.is_syncing {
882 zinc_log_debug!(target: LOG_TARGET_WASM, "Sync already in progress, skipping.");
883 return Err(JsValue::from_str("Wallet Busy: Sync already in progress"));
884 }
885 inner.is_syncing = true;
886 zinc_log_debug!(target: LOG_TARGET_WASM, "borrow successful, preparing requests");
887 (inner.prepare_requests(), inner.account_generation())
888 }
889 Err(e) => {
890 zinc_log_debug!(target: LOG_TARGET_WASM, "sync: FAILED TO BORROW INNER: {:?}", e);
891 return Err(JsValue::from_str(&format!(
892 "Failed to borrow wallet inner state: {}",
893 e
894 )));
895 }
896 }
897 };
898
899 let client = match esplora_client::Builder::new(&esplora_url)
900 .build_async_with_sleeper::<SyncSleeper>()
901 {
902 Ok(c) => c,
903 Err(e) => {
904 zinc_log_error!(target: LOG_TARGET_WASM, "failed to create esplora client");
905 zinc_log_debug!(
906 target: LOG_TARGET_WASM,
907 "failed to create esplora client: {:?}",
908 e
909 );
910 ZincWasmWallet::clear_syncing_if_generation_matches(&inner_rc, sync_generation);
911 if let Some(stale) = ZincWasmWallet::generation_mismatch_error(
912 &inner_rc,
913 sync_generation,
914 SYNC_STALE_ERROR,
915 ) {
916 return Err(stale);
917 }
918 return Err(JsValue::from(format!("{:?}", e)));
919 }
920 };
921
922 let vault_update_res: Result<bdk_wallet::Update, JsValue> = match sync_req.taproot {
924 SyncRequestType::Full(req) => {
925 zinc_log_info!(target: LOG_TARGET_WASM, "starting taproot full scan");
926 client
927 .full_scan(req, 20, 1)
928 .await
929 .map(|u| u.into())
930 .map_err(|e| {
931 zinc_log_debug!(target: LOG_TARGET_WASM, "Vault full scan failed: {:?}", e);
932 JsValue::from(e.to_string())
933 })
934 }
935 SyncRequestType::Incremental(req) => {
936 zinc_log_info!(target: LOG_TARGET_WASM, "starting taproot incremental sync");
937 client.sync(req, 1).await.map(|u| u.into()).map_err(|e| {
938 zinc_log_debug!(target: LOG_TARGET_WASM, "Vault sync failed: {:?}", e);
939 JsValue::from(e.to_string())
940 })
941 }
942 };
943
944 let vault_update = match vault_update_res {
945 Ok(u) => u,
946 Err(e) => {
947 ZincWasmWallet::clear_syncing_if_generation_matches(&inner_rc, sync_generation);
948 if let Some(stale) = ZincWasmWallet::generation_mismatch_error(
949 &inner_rc,
950 sync_generation,
951 SYNC_STALE_ERROR,
952 ) {
953 return Err(stale);
954 }
955 return Err(e);
956 }
957 };
958
959 let payment_update: Option<bdk_wallet::Update> = if let Some(req_type) =
960 sync_req.payment
961 {
962 let update_res: Result<bdk_wallet::Update, JsValue> = match req_type {
963 SyncRequestType::Full(req) => {
964 zinc_log_info!(target: LOG_TARGET_WASM, "starting payment full scan");
965 client
966 .full_scan(req, 20, 1)
967 .await
968 .map(|u| u.into())
969 .map_err(|e| {
970 zinc_log_debug!(target: LOG_TARGET_WASM, "Payment full scan failed: {:?}", e);
971 JsValue::from(e.to_string())
972 })
973 }
974 SyncRequestType::Incremental(req) => {
975 zinc_log_info!(
976 target: LOG_TARGET_WASM,
977 "starting payment incremental sync"
978 );
979 client.sync(req, 1).await.map(|u| u.into()).map_err(|e| {
980 zinc_log_debug!(target: LOG_TARGET_WASM, "Payment sync failed: {:?}", e);
981 JsValue::from(e.to_string())
982 })
983 }
984 };
985
986 match update_res {
987 Ok(u) => Some(u),
988 Err(e) => {
989 ZincWasmWallet::clear_syncing_if_generation_matches(
990 &inner_rc,
991 sync_generation,
992 );
993 if let Some(stale) = ZincWasmWallet::generation_mismatch_error(
994 &inner_rc,
995 sync_generation,
996 SYNC_STALE_ERROR,
997 ) {
998 return Err(stale);
999 }
1000 return Err(e);
1001 }
1002 }
1003 } else {
1004 None
1005 };
1006 zinc_log_debug!(target: LOG_TARGET_WASM, "sync: chain client returned");
1007
1008 let events = {
1010 match inner_rc.try_borrow_mut() {
1011 Ok(mut inner) => {
1012 if inner.account_generation() != sync_generation {
1013 return Err(JsValue::from_str(SYNC_STALE_ERROR));
1014 }
1015 zinc_log_debug!(target: LOG_TARGET_WASM, "sync: applying updates");
1016 let res = inner
1017 .apply_sync(vault_update, payment_update)
1018 .map_err(|e| {
1019 inner.is_syncing = false;
1020 zinc_log_error!(target: LOG_TARGET_WASM, "failed to apply sync");
1021 zinc_log_debug!(
1022 target: LOG_TARGET_WASM,
1023 "failed to apply sync update: {}",
1024 e
1025 );
1026 JsValue::from(e)
1027 })?;
1028 inner.is_syncing = false;
1029 zinc_log_debug!(target: LOG_TARGET_WASM, "sync: updates applied");
1030 res
1031 }
1032 Err(e) => {
1033 zinc_log_debug!(target: LOG_TARGET_WASM, "FAILED TO BORROW MUT INNER: {:?}", e);
1034 return Err(JsValue::from_str(&format!(
1035 "Failed to borrow wallet inner state (mut): {}",
1036 e
1037 )));
1038 }
1039 }
1040 };
1041 zinc_log_debug!(target: LOG_TARGET_WASM, "sync: finished. events: {:?}", events);
1042
1043 serde_wasm_bindgen::to_value(&events).map_err(|e| JsValue::from(e.to_string()))
1044 }))
1045 }
1046
1047 #[cfg(target_arch = "wasm32")]
1048 #[wasm_bindgen(js_name = discoverAccounts)]
1049 pub fn discover_accounts(
1050 &self,
1051 esplora_url: String,
1052 account_gap_limit: u32,
1053 address_scan_depth: Option<u32>,
1054 timeout_ms: Option<u32>,
1055 ) -> Result<js_sys::Promise, JsValue> {
1056 self.check_vitality()?;
1057
1058 let mnemonic = crate::keys::ZincMnemonic::parse(&self.phrase)
1059 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1060 let seed = crate::builder::Seed64::from_array(*mnemonic.to_seed(""));
1061 let state = self.state_snapshot();
1062 let network = state.network;
1063 let scheme = state.scheme;
1064 let account_gap_limit = account_gap_limit.max(1);
1065 let address_scan_depth = address_scan_depth.unwrap_or(20).max(1);
1066 let timeout_ms = timeout_ms.unwrap_or(120_000).max(1);
1067
1068 Ok(wasm_bindgen_futures::future_to_promise(async move {
1069 zinc_log_debug!(
1070 target: LOG_TARGET_WASM,
1071 "discover_accounts start ({}, account_gap_limit={}, address_scan_depth={}, timeout_ms={})",
1072 logging::redacted_field("esplora_url", &esplora_url),
1073 account_gap_limit,
1074 address_scan_depth,
1075 timeout_ms
1076 );
1077
1078 let client = reqwest::Client::new();
1079 let mut max_active_index: i32 = -1;
1080 let mut current_gap = 0;
1081 let mut account_index: u32 = 0;
1082 let start_ms = js_sys::Date::now();
1083 let deadline_ms = start_ms + f64::from(timeout_ms);
1084
1085 loop {
1086 if js_sys::Date::now() >= deadline_ms {
1087 zinc_log_warn!(
1088 target: LOG_TARGET_WASM,
1089 "discover_accounts reached timeout budget after {}ms (best_so_far_max_active={})",
1090 timeout_ms,
1091 max_active_index
1092 );
1093 break;
1094 }
1095
1096 if current_gap >= account_gap_limit {
1097 break;
1098 }
1099
1100 let mut builder = WalletBuilder::from_seed(network, seed);
1101 builder = builder
1102 .with_scheme(scheme)
1103 .with_account_index(account_index);
1104
1105 let zwallet = builder.build().map_err(|e| JsValue::from_str(&e))?;
1106 let timed_out = std::cell::Cell::new(false);
1107 const ADDRESS_REQUEST_TIMEOUT_MS: u32 = 2_000;
1108
1109 let check_activity = |addr_str: String| {
1110 let client = client.clone();
1111 let url = format!("{}/address/{}", esplora_url, addr_str);
1112 async move {
1113 let request = async {
1114 if let Ok(resp) = client.get(&url).send().await {
1115 if let Ok(json) = resp.json::<serde_json::Value>().await {
1116 let chain_txs =
1117 json["chain_stats"]["tx_count"].as_u64().unwrap_or(0);
1118 let mempool_txs =
1119 json["mempool_stats"]["tx_count"].as_u64().unwrap_or(0);
1120 return chain_txs > 0 || mempool_txs > 0;
1121 }
1122 }
1123 false
1124 };
1125 let timeout = gloo_timers::future::TimeoutFuture::new(ADDRESS_REQUEST_TIMEOUT_MS);
1126 futures_util::pin_mut!(request);
1127 futures_util::pin_mut!(timeout);
1128
1129 match futures_util::future::select(request, timeout).await {
1130 futures_util::future::Either::Left((value, _)) => value,
1131 futures_util::future::Either::Right((_timed_out, _)) => false,
1132 }
1133 }
1134 };
1135
1136 let has_activity =
1139 account_is_active_from_receive_scan(address_scan_depth, |address_index| {
1140 let vault_addr = zwallet
1141 .vault_wallet
1142 .peek_address(KeychainKind::External, address_index)
1143 .address
1144 .to_string();
1145
1146 let payment_addr = if scheme == AddressScheme::Dual {
1147 zwallet.payment_wallet.as_ref().map(|wallet| {
1148 wallet
1149 .peek_address(KeychainKind::External, address_index)
1150 .address
1151 .to_string()
1152 })
1153 } else {
1154 None
1155 };
1156
1157 async {
1158 if js_sys::Date::now() >= deadline_ms {
1159 timed_out.set(true);
1160 return false;
1161 }
1162 if check_activity(vault_addr).await {
1163 return true;
1164 }
1165 if let Some(payment_addr) = payment_addr {
1166 return check_activity(payment_addr).await;
1167 }
1168 false
1169 }
1170 })
1171 .await;
1172
1173 if timed_out.get() {
1174 zinc_log_warn!(
1175 target: LOG_TARGET_WASM,
1176 "discover_accounts stopped mid-account scan due to timeout budget (account_index={})",
1177 account_index
1178 );
1179 break;
1180 }
1181
1182 if has_activity {
1183 max_active_index = account_index as i32;
1184 current_gap = 0;
1185 } else {
1186 current_gap += 1;
1187 }
1188
1189 account_index += 1;
1190 }
1191
1192 let discovered_count = (max_active_index + 1) as u32;
1193 let final_count = if discovered_count > 0 {
1194 discovered_count
1195 } else {
1196 1
1197 }; zinc_log_debug!(target: LOG_TARGET_WASM,
1200 "discover_accounts finished. Found max active = {}, returning discovery count {}",
1201 max_active_index,
1202 final_count
1203 );
1204
1205 Ok(JsValue::from(final_count))
1206 }))
1207 }
1208
1209 #[wasm_bindgen(js_name = loadInscriptions)]
1210 pub fn load_inscriptions(&self, val: JsValue) -> Result<u32, JsValue> {
1215 self.check_vitality()?;
1216 zinc_log_debug!(target: LOG_TARGET_WASM, "load_inscriptions called with JsValue");
1217
1218 let inscriptions: Vec<crate::ordinals::types::Inscription> =
1219 serde_wasm_bindgen::from_value(val).map_err(|e| {
1220 JsValue::from_str(&format!("Failed to parse inscriptions from JsValue: {}", e))
1221 })?;
1222
1223 zinc_log_debug!(target: LOG_TARGET_WASM,
1224 "Parsed {} inscriptions from JsValue. Updating wallet state...",
1225 inscriptions.len()
1226 );
1227
1228 match self.inner.try_borrow_mut() {
1229 Ok(mut inner) => {
1230 let count = inner.apply_unverified_inscriptions_cache(inscriptions);
1231 zinc_log_debug!(target: LOG_TARGET_WASM, "Inscriptions applied. New count: {}", count);
1232 Ok(count as u32)
1233 }
1234 Err(e) => {
1235 zinc_log_debug!(target: LOG_TARGET_WASM, "load_inscriptions FAILED to borrow mutable: {}", e);
1236 Err(JsValue::from_str(&format!(
1237 "Wallet busy (load_inscriptions): {}",
1238 e
1239 )))
1240 }
1241 }
1242 }
1243
1244 #[cfg(target_arch = "wasm32")]
1245 #[wasm_bindgen(js_name = syncOrdinals)]
1246 pub fn sync_ordinals(&self, ord_url: String) -> Result<js_sys::Promise, JsValue> {
1247 self.check_vitality()?;
1248 let inner_rc = self.inner.clone();
1249
1250 Ok(wasm_bindgen_futures::future_to_promise(async move {
1251 zinc_log_debug!(target: LOG_TARGET_WASM, "sync_ordinals start");
1252 let (addresses, wallet_height, sync_generation) = {
1254 match inner_rc.try_borrow_mut() {
1255 Ok(mut inner) => {
1256 if inner.is_syncing {
1257 zinc_log_debug!(target: LOG_TARGET_WASM, "Ord sync skipped: Wallet is busy syncing.");
1258 return Err(JsValue::from_str(
1259 "Wallet Busy: Operation already in progress",
1260 ));
1261 }
1262 inner.is_syncing = true;
1263 zinc_log_debug!(target: LOG_TARGET_WASM, "sync_ordinals: collecting active addresses...");
1264 let addrs = inner.collect_active_addresses();
1265 zinc_log_debug!(target: LOG_TARGET_WASM, "sync_ordinals: collected {} addresses", addrs.len());
1266 for a in &addrs {
1267 zinc_log_debug!(
1268 target: LOG_TARGET_WASM,
1269 "sync_ordinals address queued: {}",
1270 a
1271 );
1272 }
1273 let height = inner.vault_wallet.local_chain().tip().height();
1274 (addrs, height, inner.account_generation())
1275 }
1276 Err(e) => {
1277 zinc_log_debug!(target: LOG_TARGET_WASM, "sync_ordinals: FAILED TO BORROW INNER: {:?}", e);
1278 return Err(JsValue::from_str(&format!("Failed to borrow: {}", e)));
1279 }
1280 }
1281 };
1282
1283 let client = crate::ordinals::OrdClient::new(ord_url.to_string());
1285
1286 let ord_height = match client.get_indexing_height().await {
1288 Ok(h) => h,
1289 Err(e) => {
1290 zinc_log_debug!(target: LOG_TARGET_WASM, "Failed to get ord height: {:?}", e);
1291 ZincWasmWallet::clear_syncing_if_generation_matches(&inner_rc, sync_generation);
1292 if let Some(stale) = ZincWasmWallet::generation_mismatch_error(
1293 &inner_rc,
1294 sync_generation,
1295 ORD_SYNC_STALE_ERROR,
1296 ) {
1297 return Err(stale);
1298 }
1299 return Err(JsValue::from_str(&e.to_string()));
1300 }
1301 };
1302
1303 if ord_height < wallet_height.saturating_sub(1) {
1304 zinc_log_debug!(target: LOG_TARGET_WASM, "sync_ordinals: Ord lagging, setting verified=false");
1306 match inner_rc.try_borrow_mut() {
1307 Ok(mut inner) => {
1308 if inner.account_generation() != sync_generation {
1309 return Err(JsValue::from_str(ORD_SYNC_STALE_ERROR));
1310 }
1311 inner.ordinals_verified = false;
1312 inner.is_syncing = false;
1313 }
1314 Err(e) => {
1315 zinc_log_debug!(target: LOG_TARGET_WASM,
1316 "sync_ordinals: Failed to borrow mut for lag update: {}",
1317 e
1318 );
1319 }
1320 }
1321 return Err(JsValue::from_str(&format!(
1322 "Ord Indexer is lagging! Ord: {}, Wallet: {}. Safety lock engaged.",
1323 ord_height, wallet_height
1324 )));
1325 }
1326
1327 zinc_log_debug!(target: LOG_TARGET_WASM, "sync_ordinals: fetching inscriptions");
1329 let mut all_inscriptions = Vec::new();
1330 let mut protected_outpoints = std::collections::HashSet::new();
1331 for addr_str in addresses {
1332 match client.get_inscriptions(&addr_str).await {
1333 Ok(list) => {
1334 zinc_log_debug!(target: LOG_TARGET_WASM,
1335 "sync_ordinals: found {} inscriptions for {}",
1336 list.len(),
1337 addr_str
1338 );
1339 all_inscriptions.extend(list);
1340 }
1341 Err(e) => {
1342 zinc_log_debug!(target: LOG_TARGET_WASM, "Failed to fetch inscriptions for {}: {}", addr_str, e);
1343 ZincWasmWallet::clear_syncing_if_generation_matches(
1344 &inner_rc,
1345 sync_generation,
1346 );
1347 if let Some(stale) = ZincWasmWallet::generation_mismatch_error(
1348 &inner_rc,
1349 sync_generation,
1350 ORD_SYNC_STALE_ERROR,
1351 ) {
1352 return Err(stale);
1353 }
1354 return Err(JsValue::from_str(&format!(
1355 "Failed to fetch for {}: {}",
1356 addr_str, e
1357 )));
1358 }
1359 }
1360
1361 match client.get_protected_outpoints(&addr_str).await {
1362 Ok(outpoints) => {
1363 zinc_log_debug!(target: LOG_TARGET_WASM,
1364 "sync_ordinals: found {} protected outputs for {}",
1365 outpoints.len(),
1366 addr_str
1367 );
1368 protected_outpoints.extend(outpoints);
1369 }
1370 Err(e) => {
1371 zinc_log_debug!(target: LOG_TARGET_WASM,
1372 "Failed to fetch protected outputs for {}: {}",
1373 addr_str,
1374 e
1375 );
1376 match inner_rc.try_borrow_mut() {
1377 Ok(mut inner) => {
1378 if inner.account_generation() != sync_generation {
1379 return Err(JsValue::from_str(ORD_SYNC_STALE_ERROR));
1380 }
1381 inner.ordinals_verified = false;
1382 inner.is_syncing = false;
1383 }
1384 Err(_) => {}
1385 }
1386 return Err(JsValue::from_str(&format!(
1387 "Failed to fetch protected outputs for {}: {}",
1388 addr_str, e
1389 )));
1390 }
1391 }
1392 }
1393 zinc_log_debug!(target: LOG_TARGET_WASM,
1394 "sync_ordinals: total inscriptions found: {}",
1395 all_inscriptions.len()
1396 );
1397
1398 zinc_log_debug!(target: LOG_TARGET_WASM, "sync_ordinals: applying update (borrow mut)");
1400 let count = {
1401 match inner_rc.try_borrow_mut() {
1402 Ok(mut inner) => {
1403 if inner.account_generation() != sync_generation {
1404 return Err(JsValue::from_str(ORD_SYNC_STALE_ERROR));
1405 }
1406 let c = inner
1407 .apply_verified_ordinals_update(all_inscriptions, protected_outpoints);
1408 inner.is_syncing = false; c
1410 }
1411 Err(e) => {
1412 zinc_log_debug!(target: LOG_TARGET_WASM, "sync_ordinals: FAILED TO BORROW MUT: {:?}", e);
1413 return Err(JsValue::from_str(&format!("Failed to borrow mut: {}", e)));
1414 }
1415 }
1416 };
1417
1418 Ok(JsValue::from(count as u32))
1419 }))
1420 }
1421 fn create_psbt_with_transport(
1426 &self,
1427 transport: crate::builder::CreatePsbtTransportRequest,
1428 busy_label: &str,
1429 ) -> Result<String, JsValue> {
1430 let request = crate::builder::CreatePsbtRequest::try_from(transport)
1431 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1432
1433 match self.inner.try_borrow_mut() {
1434 Ok(mut inner) => inner
1435 .create_psbt_base64(&request)
1436 .map_err(|e| JsValue::from_str(&e.to_string())),
1437 Err(e) => Err(JsValue::from_str(&format!(
1438 "Wallet busy ({}): {}",
1439 busy_label, e
1440 ))),
1441 }
1442 }
1443
1444 #[wasm_bindgen(js_name = createPsbt)]
1451 pub fn create_psbt_request(&self, request: JsValue) -> Result<String, JsValue> {
1452 self.check_vitality()?;
1453
1454 let transport: crate::builder::CreatePsbtTransportRequest =
1455 serde_wasm_bindgen::from_value(request)
1456 .map_err(|e| JsValue::from_str(&format!("Invalid request: {e}")))?;
1457
1458 self.create_psbt_with_transport(transport, "createPsbt")
1459 }
1460
1461 #[doc(hidden)]
1466 pub fn create_psbt(
1467 &self,
1468 recipient: &str,
1469 amount_sats: u64,
1470 fee_rate_sat_vb: u64,
1471 ) -> Result<String, JsValue> {
1472 self.check_vitality()?;
1473 self.create_psbt_with_transport(
1474 crate::builder::CreatePsbtTransportRequest {
1475 recipient: recipient.to_string(),
1476 amount_sats,
1477 fee_rate_sat_vb,
1478 },
1479 "create_psbt",
1480 )
1481 }
1482
1483 #[wasm_bindgen(js_name = signPsbt)]
1486 pub fn sign_psbt(&self, psbt_base64: &str, options: JsValue) -> Result<String, JsValue> {
1487 self.check_vitality()?;
1488
1489 let sign_opts: Option<crate::builder::SignOptions> =
1490 if options.is_null() || options.is_undefined() {
1491 None
1492 } else {
1493 match serde_wasm_bindgen::from_value(options) {
1494 Ok(opts) => Some(opts),
1495 Err(e) => return Err(JsValue::from_str(&format!("Invalid options: {}", e))),
1496 }
1497 };
1498
1499 match self.inner.try_borrow_mut() {
1500 Ok(mut inner) => inner
1501 .sign_psbt(psbt_base64, sign_opts)
1502 .map_err(JsValue::from),
1503 Err(e) => Err(JsValue::from_str(&format!(
1504 "Wallet busy (sign_psbt): {}",
1505 e
1506 ))),
1507 }
1508 }
1509
1510 #[wasm_bindgen(js_name = analyzePsbt)]
1513 pub fn analyze_psbt(&self, psbt_base64: &str) -> Result<String, JsValue> {
1514 self.check_vitality()?;
1515 match self.inner.try_borrow() {
1516 Ok(inner) => inner.analyze_psbt(psbt_base64).map_err(JsValue::from),
1517 Err(e) => Err(JsValue::from_str(&format!(
1518 "Wallet busy (analyze_psbt): {}",
1519 e
1520 ))),
1521 }
1522 }
1523
1524 #[wasm_bindgen(js_name = auditPsbt)]
1527 pub fn audit_psbt(&self, psbt_base64: &str, options: JsValue) -> Result<(), JsValue> {
1528 self.check_vitality()?;
1529
1530 let sign_opts: Option<crate::builder::SignOptions> =
1531 if options.is_null() || options.is_undefined() {
1532 None
1533 } else {
1534 match serde_wasm_bindgen::from_value(options) {
1535 Ok(opts) => Some(opts),
1536 Err(e) => return Err(JsValue::from_str(&format!("Invalid options: {}", e))),
1537 }
1538 };
1539
1540 use base64::Engine;
1541 let psbt_bytes = base64::engine::general_purpose::STANDARD
1542 .decode(psbt_base64)
1543 .map_err(|e| JsValue::from_str(&format!("Invalid base64: {e}")))?;
1544
1545 let psbt = bitcoin::psbt::Psbt::deserialize(&psbt_bytes)
1546 .map_err(|e| JsValue::from_str(&format!("Invalid PSBT: {e}")))?;
1547
1548 let inner = self
1549 .inner
1550 .try_borrow()
1551 .map_err(|e| JsValue::from_str(&format!("Wallet busy (audit_psbt): {}", e)))?;
1552
1553 let mut known_inscriptions: std::collections::HashMap<
1555 (bitcoin::Txid, u32),
1556 Vec<(String, u64)>,
1557 > = std::collections::HashMap::new();
1558 for ins in &inner.inscriptions {
1559 known_inscriptions
1560 .entry((ins.satpoint.outpoint.txid, ins.satpoint.outpoint.vout))
1561 .or_default()
1562 .push((ins.id.clone(), ins.satpoint.offset));
1563 }
1564
1565 let allowed_inputs = sign_opts.as_ref().and_then(|o| o.sign_inputs.as_deref());
1567
1568 crate::ordinals::shield::audit_psbt(
1569 &psbt,
1570 &known_inscriptions,
1571 allowed_inputs,
1572 inner.vault_wallet.network(),
1573 )
1574 .map_err(|e| JsValue::from_str(&e.to_string()))
1575 }
1576
1577 pub fn sign_message(&self, address: &str, message: &str) -> Result<String, JsValue> {
1580 self.check_vitality()?;
1581 match self.inner.try_borrow() {
1582 Ok(inner) => inner
1583 .sign_message(address, message)
1584 .map_err(|e| JsValue::from_str(&e)),
1585 Err(e) => Err(JsValue::from_str(&format!(
1586 "Wallet busy (sign_message): {}",
1587 e
1588 ))),
1589 }
1590 }
1591
1592 #[cfg(target_arch = "wasm32")]
1595 #[wasm_bindgen(js_name = broadcast)]
1596 pub fn broadcast(
1597 &self,
1598 signed_psbt_base64: String,
1599 esplora_url: String,
1600 ) -> Result<js_sys::Promise, JsValue> {
1601 self.check_vitality()?;
1602 use crate::builder::SyncSleeper;
1603
1604 Ok(wasm_bindgen_futures::future_to_promise(async move {
1605 use base64::Engine;
1607 let psbt_bytes = base64::engine::general_purpose::STANDARD
1608 .decode(&signed_psbt_base64)
1609 .map_err(|e| JsValue::from_str(&format!("Invalid base64: {e}")))?;
1610
1611 let psbt = bitcoin::psbt::Psbt::deserialize(&psbt_bytes)
1612 .map_err(|e| JsValue::from_str(&format!("Invalid PSBT: {e}")))?;
1613
1614 let tx = psbt
1616 .extract_tx()
1617 .map_err(|e| JsValue::from_str(&format!("Failed to extract tx: {e}")))?;
1618
1619 let client = esplora_client::Builder::new(&esplora_url)
1621 .build_async_with_sleeper::<SyncSleeper>()
1622 .map_err(|e| JsValue::from_str(&format!("Failed to create client: {e:?}")))?;
1623
1624 client
1625 .broadcast(&tx)
1626 .await
1627 .map_err(|e| JsValue::from_str(&format!("Broadcast failed: {e}")))?;
1628
1629 Ok(JsValue::from(tx.compute_txid().to_string()))
1630 }))
1631 }
1632}
1633
1634#[cfg(test)]
1636pub mod tests;