1use std::collections::{BTreeMap, BTreeSet, HashMap};
47use std::future::Future;
48use std::pin::Pin;
49use std::sync::{Arc, Mutex};
50
51use satd_events_proto::v1 as pb;
52use satd_events_proto::v1::subscribe_control::Msg;
53
54use crate::client::{
55 validate_prefix, AutoClose, EventStream, SilentPaymentTarget, StreamClient, WatchHandle,
56};
57use crate::error::StreamError;
58use crate::event::{Cursor, CursorRejectReason, Event, WatchAddRejectReason, WatchAddRejected};
59use crate::resilience::{Backoff, CursorStore, NoopCursorStore};
60
61type BoxError = Box<dyn std::error::Error + Send + Sync>;
63
64type WatchSetLoaderFn =
69 dyn Fn(WatchSetBuilder) -> Pin<Box<dyn Future<Output = Result<(), BoxError>> + Send>>
70 + Send
71 + Sync;
72
73type Scripthash = [u8; 32];
75type Txid = [u8; 32];
77
78#[derive(Debug, Default, Clone)]
87pub(crate) struct WatchSetMirror {
88 scripts: BTreeMap<Scripthash, Option<u64>>,
90 outpoints: BTreeSet<(Txid, u32)>,
92 tx_lifecycles: BTreeMap<Txid, AutoClose>,
94 depth_alarms: BTreeSet<(Txid, u32)>,
96 descriptors: BTreeMap<String, (u32, u32)>,
98 descriptor_history: BTreeMap<String, Vec<(u32, u32)>>,
102 prefixes: BTreeSet<(u32, Vec<u8>)>,
104 silent_payments: BTreeMap<[u8; 33], SilentPaymentTarget>,
110 categories: Option<u32>,
112 include_raw_tx: Option<bool>,
115}
116
117impl WatchSetMirror {
118 fn add_scripts(&mut self, items: &[(Scripthash, Option<u64>)]) {
119 for (h, floor) in items {
120 self.scripts.insert(*h, *floor);
121 }
122 }
123
124 fn remove_scripts(&mut self, hashes: &[Scripthash]) {
125 for h in hashes {
126 self.scripts.remove(h);
127 }
128 }
129
130 fn add_outpoints(&mut self, items: &[(Txid, u32)]) {
131 self.outpoints.extend(items.iter().copied());
132 }
133
134 fn remove_outpoints(&mut self, items: &[(Txid, u32)]) {
135 for op in items {
136 self.outpoints.remove(op);
137 }
138 }
139
140 fn add_tx_lifecycle(&mut self, txids: &[Txid], auto_close: AutoClose) {
141 for t in txids {
142 self.tx_lifecycles.insert(*t, auto_close);
143 }
144 }
145
146 fn remove_tx_lifecycle(&mut self, txids: &[Txid]) {
147 for t in txids {
148 self.tx_lifecycles.remove(t);
149 }
150 }
151
152 fn add_depth_alarms(&mut self, pairs: &[(Txid, u32)]) {
153 self.depth_alarms.extend(pairs.iter().copied());
154 }
155
156 fn remove_depth_alarms(&mut self, pairs: &[(Txid, u32)]) {
157 for p in pairs {
158 self.depth_alarms.remove(p);
159 }
160 }
161
162 fn add_descriptor(&mut self, descriptor: String, gap_limit: u32, start: u32) {
163 let window = (gap_limit, start);
164 match self.descriptors.insert(descriptor.clone(), window) {
165 Some(prev) if prev != window => {
166 let history = self.descriptor_history.entry(descriptor).or_default();
167 history.retain(|w| *w != window);
168 history.push(prev);
169 if history.len() > DESCRIPTOR_HISTORY {
170 history.remove(0);
171 }
172 }
173 Some(_) => {}
174 None => {
175 self.descriptor_history.remove(&descriptor);
176 }
177 }
178 }
179
180 fn remove_descriptor(&mut self, descriptor: &str) {
181 self.descriptors.remove(descriptor);
182 self.descriptor_history.remove(descriptor);
183 }
184
185 fn subset_for(&self, r: &WatchAddRejected) -> WatchSetMirror {
190 let fixed = |b: &Vec<u8>| <[u8; 32]>::try_from(b.as_slice()).ok();
191 let mut out = WatchSetMirror::default();
192 for sh in r.scripthashes.iter().filter_map(fixed) {
193 if let Some(floor) = self.scripts.get(&sh) {
194 out.scripts.insert(sh, *floor);
195 }
196 }
197 for (t, v) in &r.outpoints {
198 if let Some(op) = fixed(t).map(|t| (t, *v)).filter(|op| self.outpoints.contains(op)) {
199 out.outpoints.insert(op);
200 }
201 }
202 for t in r.txids.iter().filter_map(fixed) {
203 if let Some(close) = self.tx_lifecycles.get(&t) {
204 out.tx_lifecycles.insert(t, *close);
205 }
206 }
207 for (t, d) in &r.depth_alarms {
208 if let Some(alarm) = fixed(t).map(|t| (t, *d)).filter(|a| self.depth_alarms.contains(a)) {
209 out.depth_alarms.insert(alarm);
210 }
211 }
212 for (prefix, bits) in &r.prefixes {
213 for (b, p) in &self.prefixes {
214 if b == bits && mask_prefix(p, *b) == *prefix {
215 out.prefixes.insert((*b, p.clone()));
216 }
217 }
218 }
219 for id in r.scan_pubkeys.iter().filter_map(|b| <[u8; 33]>::try_from(b.as_slice()).ok()) {
220 if let Some(t) = self.silent_payments.get(&id) {
221 out.silent_payments.insert(id, t.clone());
222 }
223 }
224 if let Some(d) = &r.descriptor
225 && self.descriptors.get(&d.descriptor) == Some(&(d.gap_limit, d.start))
226 {
227 out.descriptors.insert(d.descriptor.clone(), (d.gap_limit, d.start));
228 }
229 out
230 }
231
232 fn forget_rejected(&mut self, r: &WatchAddRejected) {
235 let fixed = |b: &Vec<u8>| <[u8; 32]>::try_from(b.as_slice()).ok();
236 let scripts: Vec<Scripthash> = r.scripthashes.iter().filter_map(fixed).collect();
237 self.remove_scripts(&scripts);
238 let outpoints: Vec<(Txid, u32)> =
239 r.outpoints.iter().filter_map(|(t, v)| Some((fixed(t)?, *v))).collect();
240 self.remove_outpoints(&outpoints);
241 let txids: Vec<Txid> = r.txids.iter().filter_map(fixed).collect();
242 self.remove_tx_lifecycle(&txids);
243 let alarms: Vec<(Txid, u32)> =
244 r.depth_alarms.iter().filter_map(|(t, d)| Some((fixed(t)?, *d))).collect();
245 self.remove_depth_alarms(&alarms);
246 for (prefix, bits) in &r.prefixes {
249 self.prefixes.retain(|(b, p)| !(b == bits && mask_prefix(p, *b) == *prefix));
250 }
251 let ids: Vec<[u8; 33]> =
252 r.scan_pubkeys.iter().filter_map(|b| <[u8; 33]>::try_from(b.as_slice()).ok()).collect();
253 self.remove_silent_payments(&ids);
254 if let Some(d) = &r.descriptor {
255 let refused = (d.gap_limit, d.start);
256 if self.descriptors.get(&d.descriptor) == Some(&refused) {
257 let prev = self.descriptor_history.get_mut(&d.descriptor).and_then(Vec::pop);
261 match prev {
262 Some(prev) if d.kept => {
263 self.descriptors.insert(d.descriptor.clone(), prev);
264 }
265 None if d.kept => {}
268 _ => {
269 self.descriptors.remove(&d.descriptor);
270 self.descriptor_history.remove(&d.descriptor);
271 }
272 }
273 } else if let Some(history) = self.descriptor_history.get_mut(&d.descriptor) {
274 history.retain(|w| *w != refused);
277 }
278 if self.descriptor_history.get(&d.descriptor).is_some_and(Vec::is_empty) {
279 self.descriptor_history.remove(&d.descriptor);
280 }
281 }
282 }
283
284 fn add_prefixes(&mut self, items: &[pb::ScriptPrefix]) {
285 for sp in items {
286 self.prefixes.insert((sp.bits, sp.prefix.clone()));
287 }
288 }
289
290 fn remove_prefixes(&mut self, items: &[pb::ScriptPrefix]) {
291 for sp in items {
292 self.prefixes.remove(&(sp.bits, sp.prefix.clone()));
293 }
294 }
295
296 #[cfg(feature = "bitcoin")]
299 fn add_silent_payments(&mut self, entries: &[([u8; 33], SilentPaymentTarget)]) {
300 for (scan_pubkey, target) in entries {
301 self.silent_payments.insert(*scan_pubkey, target.clone());
302 }
303 }
304
305 fn remove_silent_payments(&mut self, scan_pubkeys: &[[u8; 33]]) {
306 for k in scan_pubkeys {
307 self.silent_payments.remove(k);
308 }
309 }
310
311 fn set_categories(&mut self, categories: u32) {
312 self.categories = Some(categories);
313 }
314
315 fn set_watch_options(&mut self, include_raw_tx: bool) {
316 self.include_raw_tx = Some(include_raw_tx);
317 }
318
319 fn control_messages(&self) -> Vec<Msg> {
330 let mut out = Vec::new();
331
332 if let Some(c) = self.categories {
333 out.push(Msg::SetCategories(pb::SetCategories { categories: c }));
334 }
335
336 if let Some(include_raw_tx) = self.include_raw_tx {
339 out.push(Msg::SetWatchOptions(pb::SetWatchOptions { include_raw_tx }));
340 }
341
342 if !self.scripts.is_empty() {
343 let scripthashes: Vec<Vec<u8>> = self.scripts.keys().map(|h| h.to_vec()).collect();
344 let min_values = if self.scripts.values().any(Option::is_some) {
348 self.scripts.values().map(|f| f.unwrap_or(0)).collect()
349 } else {
350 Vec::new()
351 };
352 out.push(Msg::AddScripts(pb::AddScripts { scripthashes, min_values }));
353 }
354
355 if !self.outpoints.is_empty() {
356 let outpoints = self
357 .outpoints
358 .iter()
359 .map(|(t, v)| pb::Outpoint { txid: t.to_vec(), vout: *v })
360 .collect();
361 out.push(Msg::AddOutpoints(pb::AddOutpoints { outpoints }));
362 }
363
364 if !self.tx_lifecycles.is_empty() {
365 let mut by_depth: BTreeMap<u32, Vec<Vec<u8>>> = BTreeMap::new();
366 for (txid, ac) in &self.tx_lifecycles {
367 let depth = match ac {
368 AutoClose::Never => 0,
369 AutoClose::AtDepth(d) => *d,
370 };
371 by_depth.entry(depth).or_default().push(txid.to_vec());
372 }
373 for (auto_close_depth, txids) in by_depth {
374 out.push(Msg::AddTransactions(pb::AddTransactions {
375 txids,
376 min_depths: Vec::new(),
377 auto_close_depth,
378 }));
379 }
380 }
381
382 if !self.depth_alarms.is_empty() {
383 let mut by_txid: BTreeMap<Txid, Vec<u32>> = BTreeMap::new();
384 for (txid, depth) in &self.depth_alarms {
385 by_txid.entry(*txid).or_default().push(*depth);
386 }
387 for (txid, min_depths) in by_txid {
388 out.push(Msg::AddTransactions(pb::AddTransactions {
389 txids: vec![txid.to_vec()],
390 min_depths,
391 auto_close_depth: 0,
392 }));
393 }
394 }
395
396 for (descriptor, (gap_limit, start)) in &self.descriptors {
397 out.push(Msg::AddDescriptor(pb::AddDescriptor {
398 descriptor: descriptor.clone(),
399 gap_limit: *gap_limit,
400 start: *start,
401 }));
402 }
403
404 if !self.prefixes.is_empty() {
405 let prefixes = self
406 .prefixes
407 .iter()
408 .map(|(bits, prefix)| pb::ScriptPrefix { prefix: prefix.clone(), bits: *bits })
409 .collect();
410 out.push(Msg::AddScriptPrefixes(pb::AddScriptPrefixes { prefixes }));
411 }
412
413 if !self.silent_payments.is_empty() {
414 let targets = self.silent_payments.values().map(|t| t.to_proto()).collect();
415 out.push(Msg::AddSilentPayments(pb::AddSilentPayments { targets }));
416 }
417
418 out
419 }
420
421 #[allow(clippy::needless_update)]
431 fn to_set_watch_set(&self) -> pb::SetWatchSet {
432 let scripthashes: Vec<Vec<u8>> = self.scripts.keys().map(|h| h.to_vec()).collect();
433 let min_values = if self.scripts.values().any(Option::is_some) {
434 self.scripts.values().map(|f| f.unwrap_or(0)).collect()
435 } else {
436 Vec::new()
437 };
438 pb::SetWatchSet {
439 categories: self.categories.unwrap_or(0),
440 scripthashes,
441 min_values,
442 outpoints: self
443 .outpoints
444 .iter()
445 .map(|(t, v)| pb::Outpoint { txid: t.to_vec(), vout: *v })
446 .collect(),
447 descriptors: self
448 .descriptors
449 .iter()
450 .map(|(d, (gap_limit, start))| pb::AddDescriptor {
451 descriptor: d.clone(),
452 gap_limit: *gap_limit,
453 start: *start,
454 })
455 .collect(),
456 prefixes: self
457 .prefixes
458 .iter()
459 .map(|(bits, prefix)| pb::ScriptPrefix { prefix: prefix.clone(), bits: *bits })
460 .collect(),
461 lifecycles: self
462 .tx_lifecycles
463 .iter()
464 .map(|(t, ac)| pb::WatchLifecycle {
465 txid: t.to_vec(),
466 auto_close_depth: match ac {
467 AutoClose::Never => 0,
468 AutoClose::AtDepth(d) => *d,
469 },
470 })
471 .collect(),
472 depth_alarms: self
473 .depth_alarms
474 .iter()
475 .map(|(t, d)| pb::WatchDepthAlarm { txid: t.to_vec(), depth: *d })
476 .collect(),
477 silent_payments: self.silent_payments.values().map(|t| t.to_proto()).collect(),
478 ..Default::default()
483 }
484 }
485
486 fn reconcile_to(&self, target: &WatchSetMirror) -> (Vec<Msg>, ReloadCounts) {
499 let mut lead = Vec::new();
500 let mut adds = Vec::new();
501 let mut removes = Vec::new();
502 let mut c = ReloadCounts::default();
503
504 let target_mask = target.categories.unwrap_or(0);
517 let live_mask = self.categories.unwrap_or(0);
518 if target_mask != live_mask {
519 lead.push(Msg::SetCategories(pb::SetCategories { categories: target_mask }));
520 }
521
522 {
524 let mut changed: Vec<(Scripthash, Option<u64>)> = Vec::new();
525 for (h, floor) in &target.scripts {
526 match self.scripts.get(h) {
527 Some(f) if f == floor => c.unchanged += 1,
528 _ => changed.push((*h, *floor)),
529 }
530 }
531 let gone: Vec<Scripthash> =
532 self.scripts.keys().filter(|h| !target.scripts.contains_key(*h)).copied().collect();
533 if !changed.is_empty() {
534 c.added += changed.len();
535 let scripthashes = changed.iter().map(|(h, _)| h.to_vec()).collect();
536 let min_values = if changed.iter().any(|(_, f)| f.is_some()) {
537 changed.iter().map(|(_, f)| f.unwrap_or(0)).collect()
538 } else {
539 Vec::new()
540 };
541 adds.push(Msg::AddScripts(pb::AddScripts { scripthashes, min_values }));
542 }
543 if !gone.is_empty() {
544 c.removed += gone.len();
545 removes.push(Msg::RemoveScripts(pb::RemoveScripts {
546 scripthashes: gone.iter().map(|h| h.to_vec()).collect(),
547 }));
548 }
549 }
550
551 {
553 let add: Vec<_> = target.outpoints.difference(&self.outpoints).copied().collect();
554 let gone: Vec<_> = self.outpoints.difference(&target.outpoints).copied().collect();
555 c.unchanged += target.outpoints.intersection(&self.outpoints).count();
556 if !add.is_empty() {
557 c.added += add.len();
558 adds.push(Msg::AddOutpoints(pb::AddOutpoints {
559 outpoints: add.iter().map(|(t, v)| pb::Outpoint { txid: t.to_vec(), vout: *v }).collect(),
560 }));
561 }
562 if !gone.is_empty() {
563 c.removed += gone.len();
564 removes.push(Msg::RemoveOutpoints(pb::RemoveOutpoints {
565 outpoints: gone.iter().map(|(t, v)| pb::Outpoint { txid: t.to_vec(), vout: *v }).collect(),
566 }));
567 }
568 }
569
570 {
573 let mut by_depth: BTreeMap<u32, Vec<Vec<u8>>> = BTreeMap::new();
574 for (txid, ac) in &target.tx_lifecycles {
575 match self.tx_lifecycles.get(txid) {
576 Some(old) if old == ac => c.unchanged += 1,
577 _ => {
578 let depth = match ac {
579 AutoClose::Never => 0,
580 AutoClose::AtDepth(d) => *d,
581 };
582 by_depth.entry(depth).or_default().push(txid.to_vec());
583 c.added += 1;
584 }
585 }
586 }
587 for (auto_close_depth, txids) in by_depth {
588 adds.push(Msg::AddTransactions(pb::AddTransactions {
589 txids,
590 min_depths: Vec::new(),
591 auto_close_depth,
592 }));
593 }
594 let gone: Vec<Vec<u8>> = self
595 .tx_lifecycles
596 .keys()
597 .filter(|t| !target.tx_lifecycles.contains_key(*t))
598 .map(|t| t.to_vec())
599 .collect();
600 if !gone.is_empty() {
601 c.removed += gone.len();
602 removes.push(Msg::RemoveTransactions(pb::RemoveTransactions {
603 txids: gone,
604 min_depths: Vec::new(),
605 }));
606 }
607 }
608
609 {
611 let add: Vec<_> = target.depth_alarms.difference(&self.depth_alarms).copied().collect();
612 let gone: Vec<_> = self.depth_alarms.difference(&target.depth_alarms).copied().collect();
613 c.unchanged += target.depth_alarms.intersection(&self.depth_alarms).count();
614 if !add.is_empty() {
615 c.added += add.len();
616 let mut by_txid: BTreeMap<Txid, Vec<u32>> = BTreeMap::new();
617 for (t, d) in add {
618 by_txid.entry(t).or_default().push(d);
619 }
620 for (txid, min_depths) in by_txid {
621 adds.push(Msg::AddTransactions(pb::AddTransactions {
622 txids: vec![txid.to_vec()],
623 min_depths,
624 auto_close_depth: 0,
625 }));
626 }
627 }
628 if !gone.is_empty() {
629 c.removed += gone.len();
630 let mut by_txid: BTreeMap<Txid, Vec<u32>> = BTreeMap::new();
631 for (t, d) in gone {
632 by_txid.entry(t).or_default().push(d);
633 }
634 for (txid, min_depths) in by_txid {
635 removes.push(Msg::RemoveTransactions(pb::RemoveTransactions {
636 txids: vec![txid.to_vec()],
637 min_depths,
638 }));
639 }
640 }
641 }
642
643 {
646 for (d, win) in &target.descriptors {
647 match self.descriptors.get(d) {
648 Some(old) if old == win => c.unchanged += 1,
649 _ => {
650 c.added += 1;
651 adds.push(Msg::AddDescriptor(pb::AddDescriptor {
652 descriptor: d.clone(),
653 gap_limit: win.0,
654 start: win.1,
655 }));
656 }
657 }
658 }
659 for d in self.descriptors.keys() {
660 if !target.descriptors.contains_key(d) {
661 c.removed += 1;
662 removes.push(Msg::RemoveDescriptor(pb::RemoveDescriptor { descriptor: d.clone() }));
663 }
664 }
665 }
666
667 {
669 let add: Vec<_> = target.prefixes.difference(&self.prefixes).cloned().collect();
670 let gone: Vec<_> = self.prefixes.difference(&target.prefixes).cloned().collect();
671 c.unchanged += target.prefixes.intersection(&self.prefixes).count();
672 if !add.is_empty() {
673 c.added += add.len();
674 adds.push(Msg::AddScriptPrefixes(pb::AddScriptPrefixes {
675 prefixes: add.iter().map(|(bits, p)| pb::ScriptPrefix { prefix: p.clone(), bits: *bits }).collect(),
676 }));
677 }
678 if !gone.is_empty() {
679 c.removed += gone.len();
680 removes.push(Msg::RemoveScriptPrefixes(pb::RemoveScriptPrefixes {
681 prefixes: gone.iter().map(|(bits, p)| pb::ScriptPrefix { prefix: p.clone(), bits: *bits }).collect(),
682 }));
683 }
684 }
685
686 {
690 let mut changed: Vec<pb::SilentPaymentTarget> = Vec::new();
691 for (id, t) in &target.silent_payments {
692 match self.silent_payments.get(id) {
693 Some(cur)
694 if cur.labels == t.labels && cur.spend_pubkey == t.spend_pubkey =>
695 {
696 c.unchanged += 1;
697 }
698 _ => changed.push(t.to_proto()),
699 }
700 }
701 let gone: Vec<Vec<u8>> = self
702 .silent_payments
703 .keys()
704 .filter(|id| !target.silent_payments.contains_key(*id))
705 .map(|id| id.to_vec())
706 .collect();
707 if !changed.is_empty() {
708 c.added += changed.len();
709 adds.push(Msg::AddSilentPayments(pb::AddSilentPayments { targets: changed }));
710 }
711 if !gone.is_empty() {
712 c.removed += gone.len();
713 removes.push(Msg::RemoveSilentPayments(pb::RemoveSilentPayments {
714 scan_pubkeys: gone,
715 }));
716 }
717 }
718
719 lead.extend(removes);
737 lead.extend(adds);
738 (lead, c)
739 }
740}
741
742#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
744struct ReloadCounts {
745 added: usize,
746 removed: usize,
747 unchanged: usize,
748}
749
750pub struct WatchSetBuilder {
766 mirror: Arc<Mutex<WatchSetMirror>>,
767}
768
769impl WatchSetBuilder {
770 fn with(&self, f: impl FnOnce(&mut WatchSetMirror)) {
771 let mut m = self.mirror.lock().unwrap_or_else(|p| p.into_inner());
775 f(&mut m);
776 }
777
778 pub fn add_scripts(&self, items: impl IntoIterator<Item = (Scripthash, Option<u64>)>) {
781 let items: Vec<_> = items.into_iter().collect();
782 self.with(|m| m.add_scripts(&items));
783 }
784
785 pub fn add_outpoints(&self, outpoints: impl IntoIterator<Item = (Txid, u32)>) {
787 let items: Vec<_> = outpoints.into_iter().collect();
788 self.with(|m| m.add_outpoints(&items));
789 }
790
791 pub fn add_tx_lifecycle(&self, txids: impl IntoIterator<Item = Txid>, auto_close: AutoClose) {
793 let txids: Vec<_> = txids.into_iter().collect();
794 if txids.is_empty() {
795 return;
796 }
797 self.with(|m| m.add_tx_lifecycle(&txids, auto_close));
798 }
799
800 pub fn add_depth_alarms(
803 &self,
804 txids: impl IntoIterator<Item = Txid>,
805 depths: impl IntoIterator<Item = u32>,
806 ) {
807 let txids: Vec<_> = txids.into_iter().collect();
808 let depths: Vec<u32> = depths.into_iter().filter(|d| *d >= 1).collect();
809 if txids.is_empty() || depths.is_empty() {
810 return;
811 }
812 let pairs = cross_product(&txids, &depths);
813 self.with(|m| m.add_depth_alarms(&pairs));
814 }
815
816 pub fn add_descriptor(&self, descriptor: impl Into<String>, gap_limit: u32, start: u32) {
819 let descriptor = descriptor.into();
820 self.with(|m| m.add_descriptor(descriptor, gap_limit, start));
821 }
822
823 pub fn add_script_prefixes(
828 &self,
829 prefixes: impl IntoIterator<Item = (Vec<u8>, u32)>,
830 ) -> Result<(), StreamError> {
831 let validated = validate_prefixes(prefixes)?;
832 if validated.is_empty() {
833 return Ok(());
834 }
835 self.with(|m| m.add_prefixes(&validated));
836 Ok(())
837 }
838
839 #[cfg(feature = "bitcoin")]
844 #[cfg_attr(docsrs, doc(cfg(feature = "bitcoin")))]
845 pub fn add_silent_payments(
846 &self,
847 targets: impl IntoIterator<Item = SilentPaymentTarget>,
848 ) -> Result<(), StreamError> {
849 let entries = sp_mirror_entries(targets)?;
850 if entries.is_empty() {
851 return Ok(());
852 }
853 self.with(|m| m.add_silent_payments(&entries));
854 Ok(())
855 }
856
857 pub fn set_categories(&self, categories: u32) {
859 self.with(|m| m.set_categories(categories));
860 }
861
862 pub fn set_watch_options(&self, include_raw_tx: bool) {
867 self.with(|m| m.set_watch_options(include_raw_tx));
868 }
869}
870
871pub struct ResilientWatchConfig {
875 pub backoff: Backoff,
877 pub cursor_store: Arc<dyn CursorStore>,
879 pub from_cursor: Option<Cursor>,
881 pub(crate) watch_set_loader: Option<Arc<WatchSetLoaderFn>>,
885}
886
887impl Default for ResilientWatchConfig {
888 fn default() -> Self {
889 ResilientWatchConfig {
890 backoff: Backoff::default(),
891 cursor_store: Arc::new(NoopCursorStore),
892 from_cursor: None,
893 watch_set_loader: None,
894 }
895 }
896}
897
898impl std::fmt::Debug for ResilientWatchConfig {
899 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
900 f.debug_struct("ResilientWatchConfig")
901 .field("backoff", &self.backoff)
902 .field("cursor_store", &"<dyn CursorStore>")
903 .field("from_cursor", &self.from_cursor)
904 .field("watch_set_loader", &self.watch_set_loader.as_ref().map(|_| "<loader>"))
905 .finish()
906 }
907}
908
909impl ResilientWatchConfig {
910 pub fn new() -> Self {
913 Self::default()
914 }
915
916 pub fn cursor_store(mut self, store: Arc<dyn CursorStore>) -> Self {
918 self.cursor_store = store;
919 self
920 }
921
922 pub fn backoff(mut self, backoff: Backoff) -> Self {
924 self.backoff = backoff;
925 self
926 }
927
928 pub fn from_cursor(mut self, cursor: Cursor) -> Self {
931 self.from_cursor = Some(cursor);
932 self
933 }
934
935 pub fn watch_set_loader<F, Fut>(mut self, loader: F) -> Self
988 where
989 F: Fn(WatchSetBuilder) -> Fut + Send + Sync + 'static,
990 Fut: Future<Output = Result<(), BoxError>> + Send + 'static,
991 {
992 self.watch_set_loader = Some(Arc::new(move |b| Box::pin(loader(b))));
993 self
994 }
995}
996
997#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
999pub struct ReloadSummary {
1000 pub added: usize,
1003 pub removed: usize,
1005 pub unchanged: usize,
1007 pub applied: bool,
1012}
1013
1014#[derive(Debug, thiserror::Error)]
1016pub enum ReloadError {
1017 #[error("reload requires a watch_set_loader, but none is configured")]
1020 NoLoader,
1021 #[error("watch-set loader failed: {0}")]
1025 Loader(#[source] BoxError),
1026}
1027
1028#[derive(Clone, Copy)]
1043struct PendingReanchor {
1044 deadline: tokio::time::Instant,
1048}
1049
1050struct PendingAddRetry {
1054 deadline: tokio::time::Instant,
1057 rejected: WatchAddRejected,
1059}
1060
1061fn retry_keys(r: &WatchAddRejected) -> Vec<Vec<u8>> {
1064 let tagged = |tag: u8, bytes: &[u8]| {
1065 let mut k = Vec::with_capacity(1 + bytes.len());
1066 k.push(tag);
1067 k.extend_from_slice(bytes);
1068 k
1069 };
1070 let mut keys = Vec::new();
1071 keys.extend(r.scripthashes.iter().map(|b| tagged(1, b)));
1072 keys.extend(r.outpoints.iter().map(|(t, v)| tagged(2, &[t.as_slice(), &v.to_be_bytes()].concat())));
1073 keys.extend(r.txids.iter().map(|b| tagged(3, b)));
1074 keys.extend(r.depth_alarms.iter().map(|(t, d)| tagged(4, &[t.as_slice(), &d.to_be_bytes()].concat())));
1075 keys.extend(r.descriptor.iter().map(|d| tagged(5, d.descriptor.as_bytes())));
1076 keys.extend(r.prefixes.iter().map(|(p, bits)| tagged(6, &[p.as_slice(), &bits.to_be_bytes()].concat())));
1077 keys.extend(r.scan_pubkeys.iter().map(|b| tagged(7, b)));
1078 keys
1079}
1080
1081pub struct ResilientWatch {
1098 client: StreamClient,
1099 config: ResilientWatchConfig,
1100 mirror: WatchSetMirror,
1101 handle: Option<WatchHandle>,
1104 stream: Option<EventStream>,
1105 resume: Option<Cursor>,
1107 desired_cursor: Option<Cursor>,
1110 commit_next: Option<Cursor>,
1112 committed: Option<Cursor>,
1114 seeded: bool,
1116 reconnect_attempts: u32,
1118 reanchor_attempts: u32,
1120 pending_reanchor: Option<PendingReanchor>,
1125 pending_add_retries: Vec<PendingAddRetry>,
1127 add_retry_attempts: HashMap<Vec<u8>, u32>,
1130 last_error: Option<StreamError>,
1132}
1133
1134impl ResilientWatch {
1135 pub(crate) fn new(client: StreamClient, config: ResilientWatchConfig) -> Self {
1136 ResilientWatch {
1137 client,
1138 config,
1139 mirror: WatchSetMirror::default(),
1140 handle: None,
1141 stream: None,
1142 resume: None,
1143 desired_cursor: None,
1144 commit_next: None,
1145 committed: None,
1146 seeded: false,
1147 reconnect_attempts: 0,
1148 reanchor_attempts: 0,
1149 pending_reanchor: None,
1150 pending_add_retries: Vec::new(),
1151 add_retry_attempts: HashMap::new(),
1152 last_error: None,
1153 }
1154 }
1155
1156 pub async fn add_scripts(
1161 &mut self,
1162 items: impl IntoIterator<Item = (Scripthash, Option<u64>)>,
1163 ) -> Result<(), StreamError> {
1164 let items: Vec<_> = items.into_iter().collect();
1165 if items.is_empty() {
1166 return Ok(());
1167 }
1168 self.mirror.add_scripts(&items);
1169 let res = match &self.handle {
1170 Some(h) => Some(h.add_scripts(items).await),
1171 None => None,
1172 };
1173 self.after_send(res)
1174 }
1175
1176 pub async fn remove_scripts(
1178 &mut self,
1179 hashes: impl IntoIterator<Item = Scripthash>,
1180 ) -> Result<(), StreamError> {
1181 let hashes: Vec<_> = hashes.into_iter().collect();
1182 if hashes.is_empty() {
1183 return Ok(());
1184 }
1185 self.mirror.remove_scripts(&hashes);
1186 let res = match &self.handle {
1187 Some(h) => Some(h.remove_scripts(hashes).await),
1188 None => None,
1189 };
1190 self.after_send(res)
1191 }
1192
1193 pub async fn add_outpoints(
1195 &mut self,
1196 outpoints: impl IntoIterator<Item = (Txid, u32)>,
1197 ) -> Result<(), StreamError> {
1198 let items: Vec<_> = outpoints.into_iter().collect();
1199 if items.is_empty() {
1200 return Ok(());
1201 }
1202 self.mirror.add_outpoints(&items);
1203 let res = match &self.handle {
1204 Some(h) => Some(h.add_outpoints(items).await),
1205 None => None,
1206 };
1207 self.after_send(res)
1208 }
1209
1210 pub async fn remove_outpoints(
1212 &mut self,
1213 outpoints: impl IntoIterator<Item = (Txid, u32)>,
1214 ) -> Result<(), StreamError> {
1215 let items: Vec<_> = outpoints.into_iter().collect();
1216 if items.is_empty() {
1217 return Ok(());
1218 }
1219 self.mirror.remove_outpoints(&items);
1220 let res = match &self.handle {
1221 Some(h) => Some(h.remove_outpoints(items).await),
1222 None => None,
1223 };
1224 self.after_send(res)
1225 }
1226
1227 pub async fn add_tx_lifecycle(
1229 &mut self,
1230 txids: impl IntoIterator<Item = Txid>,
1231 auto_close: AutoClose,
1232 ) -> Result<(), StreamError> {
1233 let txids: Vec<_> = txids.into_iter().collect();
1234 if txids.is_empty() {
1235 return Ok(());
1236 }
1237 self.mirror.add_tx_lifecycle(&txids, auto_close);
1238 let res = match &self.handle {
1239 Some(h) => Some(h.add_tx_lifecycle(txids, auto_close).await),
1240 None => None,
1241 };
1242 self.after_send(res)
1243 }
1244
1245 pub async fn remove_tx_lifecycle(
1247 &mut self,
1248 txids: impl IntoIterator<Item = Txid>,
1249 ) -> Result<(), StreamError> {
1250 let txids: Vec<_> = txids.into_iter().collect();
1251 if txids.is_empty() {
1252 return Ok(());
1253 }
1254 self.mirror.remove_tx_lifecycle(&txids);
1255 let res = match &self.handle {
1256 Some(h) => Some(h.remove_tx_lifecycle(txids).await),
1257 None => None,
1258 };
1259 self.after_send(res)
1260 }
1261
1262 pub async fn add_depth_alarms(
1265 &mut self,
1266 txids: impl IntoIterator<Item = Txid>,
1267 depths: impl IntoIterator<Item = u32>,
1268 ) -> Result<(), StreamError> {
1269 let txids: Vec<_> = txids.into_iter().collect();
1270 let depths: Vec<u32> = depths.into_iter().filter(|d| *d >= 1).collect();
1271 if txids.is_empty() || depths.is_empty() {
1272 return Ok(());
1273 }
1274 let pairs = cross_product(&txids, &depths);
1275 self.mirror.add_depth_alarms(&pairs);
1276 let res = match &self.handle {
1277 Some(h) => Some(h.add_depth_alarms(txids, depths).await),
1278 None => None,
1279 };
1280 self.after_send(res)
1281 }
1282
1283 pub async fn remove_depth_alarms(
1286 &mut self,
1287 txids: impl IntoIterator<Item = Txid>,
1288 depths: impl IntoIterator<Item = u32>,
1289 ) -> Result<(), StreamError> {
1290 let txids: Vec<_> = txids.into_iter().collect();
1291 let depths: Vec<u32> = depths.into_iter().filter(|d| *d >= 1).collect();
1292 if txids.is_empty() || depths.is_empty() {
1293 return Ok(());
1294 }
1295 let pairs = cross_product(&txids, &depths);
1296 self.mirror.remove_depth_alarms(&pairs);
1297 let res = match &self.handle {
1298 Some(h) => Some(h.remove_depth_alarms(txids, depths).await),
1299 None => None,
1300 };
1301 self.after_send(res)
1302 }
1303
1304 pub async fn add_descriptor(
1309 &mut self,
1310 descriptor: impl Into<String>,
1311 gap_limit: u32,
1312 start: u32,
1313 ) -> Result<(), StreamError> {
1314 let descriptor = descriptor.into();
1315 self.mirror.add_descriptor(descriptor.clone(), gap_limit, start);
1316 let res = match &self.handle {
1317 Some(h) => Some(h.add_descriptor(descriptor, gap_limit, start).await),
1318 None => None,
1319 };
1320 self.after_send(res)
1321 }
1322
1323 pub async fn remove_descriptor(
1328 &mut self,
1329 descriptor: impl Into<String>,
1330 ) -> Result<(), StreamError> {
1331 let descriptor = descriptor.into();
1332 self.mirror.remove_descriptor(&descriptor);
1333 let res = match &self.handle {
1334 Some(h) => Some(h.remove_descriptor(descriptor).await),
1335 None => None,
1336 };
1337 self.after_send(res)
1338 }
1339
1340 pub async fn add_script_prefixes(
1343 &mut self,
1344 prefixes: impl IntoIterator<Item = (Vec<u8>, u32)>,
1345 ) -> Result<(), StreamError> {
1346 let validated = validate_prefixes(prefixes)?;
1347 if validated.is_empty() {
1348 return Ok(());
1349 }
1350 self.mirror.add_prefixes(&validated);
1351 let res = match &self.handle {
1352 Some(h) => Some(
1353 h.send_control(pb::SubscribeControl {
1354 msg: Some(Msg::AddScriptPrefixes(pb::AddScriptPrefixes {
1355 prefixes: validated,
1356 })),
1357 })
1358 .await,
1359 ),
1360 None => None,
1361 };
1362 self.after_send(res)
1363 }
1364
1365 pub async fn remove_script_prefixes(
1367 &mut self,
1368 prefixes: impl IntoIterator<Item = (Vec<u8>, u32)>,
1369 ) -> Result<(), StreamError> {
1370 let validated = validate_prefixes(prefixes)?;
1371 if validated.is_empty() {
1372 return Ok(());
1373 }
1374 self.mirror.remove_prefixes(&validated);
1375 let res = match &self.handle {
1376 Some(h) => Some(
1377 h.send_control(pb::SubscribeControl {
1378 msg: Some(Msg::RemoveScriptPrefixes(pb::RemoveScriptPrefixes {
1379 prefixes: validated,
1380 })),
1381 })
1382 .await,
1383 ),
1384 None => None,
1385 };
1386 self.after_send(res)
1387 }
1388
1389 #[cfg(feature = "bitcoin")]
1397 #[cfg_attr(docsrs, doc(cfg(feature = "bitcoin")))]
1398 pub async fn add_silent_payments(
1399 &mut self,
1400 targets: impl IntoIterator<Item = SilentPaymentTarget>,
1401 ) -> Result<(), StreamError> {
1402 let entries = sp_mirror_entries(targets)?;
1403 if entries.is_empty() {
1404 return Ok(());
1405 }
1406 let held = self.mirror.silent_payments.len();
1412 let net_new = entries
1413 .iter()
1414 .filter(|(id, _)| !self.mirror.silent_payments.contains_key(id))
1415 .count();
1416 if held + net_new > crate::client::MAX_SP_TARGETS_PER_CONNECTION {
1417 return Err(StreamError::InvalidArgument(format!(
1418 "silent-payment target cap exceeded: {held} held + {net_new} new > {}",
1419 crate::client::MAX_SP_TARGETS_PER_CONNECTION
1420 )));
1421 }
1422 self.mirror.add_silent_payments(&entries);
1423 let targets: Vec<pb::SilentPaymentTarget> =
1424 entries.iter().map(|(_, t)| t.to_proto()).collect();
1425 let res = match &self.handle {
1426 Some(h) => Some(
1427 h.send_control(pb::SubscribeControl {
1428 msg: Some(Msg::AddSilentPayments(pb::AddSilentPayments { targets })),
1429 })
1430 .await,
1431 ),
1432 None => None,
1433 };
1434 self.after_send(res)
1435 }
1436
1437 pub async fn remove_silent_payments(
1440 &mut self,
1441 scan_pubkeys: impl IntoIterator<Item = [u8; 33]>,
1442 ) -> Result<(), StreamError> {
1443 let scan_pubkeys: Vec<[u8; 33]> = scan_pubkeys.into_iter().collect();
1444 if scan_pubkeys.is_empty() {
1445 return Ok(());
1446 }
1447 self.mirror.remove_silent_payments(&scan_pubkeys);
1448 let res = match &self.handle {
1449 Some(h) => Some(
1450 h.send_control(pb::SubscribeControl {
1451 msg: Some(Msg::RemoveSilentPayments(pb::RemoveSilentPayments {
1452 scan_pubkeys: scan_pubkeys.iter().map(|k| k.to_vec()).collect(),
1453 })),
1454 })
1455 .await,
1456 ),
1457 None => None,
1458 };
1459 self.after_send(res)
1460 }
1461
1462 pub async fn set_categories(&mut self, categories: u32) -> Result<(), StreamError> {
1465 self.mirror.set_categories(categories);
1466 let res = match &self.handle {
1467 Some(h) => Some(h.set_categories(categories).await),
1468 None => None,
1469 };
1470 self.after_send(res)
1471 }
1472
1473 pub async fn set_watch_options(&mut self, include_raw_tx: bool) -> Result<(), StreamError> {
1479 self.mirror.set_watch_options(include_raw_tx);
1480 let res = match &self.handle {
1481 Some(h) => Some(h.set_watch_options(include_raw_tx).await),
1482 None => None,
1483 };
1484 self.after_send(res)
1485 }
1486
1487 pub async fn set_cursor(&mut self, cursor: Cursor) -> Result<(), StreamError> {
1492 self.desired_cursor = Some(cursor);
1493 self.reanchor_attempts = 0;
1494 self.pending_reanchor = None;
1498 let res = match &self.handle {
1499 Some(h) => Some(h.set_cursor(cursor).await),
1500 None => None,
1501 };
1502 self.after_send(res)
1503 }
1504
1505 pub async fn rescan(&mut self, from_height: u32, to_height: u32) -> Result<(), StreamError> {
1518 let res = match &self.handle {
1519 Some(h) => Some(h.rescan(from_height, to_height).await),
1520 None => None,
1521 };
1522 self.after_send(res)
1523 }
1524
1525 pub async fn reload(&mut self) -> Result<ReloadSummary, ReloadError> {
1563 let loader = self.config.watch_set_loader.clone().ok_or(ReloadError::NoLoader)?;
1564 let shared = Arc::new(Mutex::new(WatchSetMirror::default()));
1566 loader(WatchSetBuilder { mirror: shared.clone() })
1567 .await
1568 .map_err(ReloadError::Loader)?;
1569 let loaded = std::mem::take(&mut *shared.lock().unwrap_or_else(|p| p.into_inner()));
1570
1571 let (_delta, counts) = self.mirror.reconcile_to(&loaded);
1578
1579 let mut applied = false;
1584 if let Some(h) = &self.handle {
1585 let msg = Msg::SetWatchSet(loaded.to_set_watch_set());
1586 let set_res = h.send_control(pb::SubscribeControl { msg: Some(msg) }).await;
1587 let old_raw_tx = self.mirror.include_raw_tx.unwrap_or(false);
1598 let new_raw_tx = loaded.include_raw_tx.unwrap_or(false);
1599 let opt_res = match &set_res {
1600 Ok(()) if old_raw_tx != new_raw_tx => Some(h.set_watch_options(new_raw_tx).await),
1601 _ => None,
1602 };
1603 if set_res.is_ok() && !matches!(opt_res, Some(Err(_))) {
1604 applied = true;
1605 } else {
1606 self.teardown();
1607 }
1608 }
1609
1610 self.mirror = loaded;
1613
1614 Ok(ReloadSummary {
1615 added: counts.added,
1616 removed: counts.removed,
1617 unchanged: counts.unchanged,
1618 applied,
1619 })
1620 }
1621
1622 pub fn resume_cursor(&self) -> Option<&Cursor> {
1626 self.resume.as_ref()
1627 }
1628
1629 pub fn commit(&mut self) -> Result<(), StreamError> {
1633 self.commit_due()
1634 }
1635
1636 pub async fn next(&mut self) -> Result<Event, StreamError> {
1672 self.commit_due()?;
1673 loop {
1674 if self.pending_reanchor.is_some() {
1677 self.drive_pending_reanchor().await?;
1678 continue;
1679 }
1680 if self.stream.is_none() {
1681 if self.reconnect_attempts > 0 {
1682 if let Some(max) = self.config.backoff.max_retries
1683 && self.reconnect_attempts > max
1684 {
1685 return Err(self.last_error.take().unwrap_or(StreamError::ControlClosed));
1686 }
1687 let delay = self.config.backoff.delay_for(self.reconnect_attempts - 1);
1688 tokio::time::sleep(delay).await;
1689 }
1690 match self.connect().await {
1691 Ok(()) => {}
1692 Err(e) if is_reconnectable(&e) => {
1693 self.teardown();
1694 self.reconnect_attempts = self.reconnect_attempts.saturating_add(1);
1695 self.last_error = Some(e);
1696 continue;
1697 }
1698 Err(e) => return Err(e),
1699 }
1700 }
1701
1702 let now = tokio::time::Instant::now();
1704 if let Some(i) = self.pending_add_retries.iter().position(|p| p.deadline <= now) {
1705 self.send_add_retry(i).await?;
1706 continue;
1707 }
1708 let next_retry = self.pending_add_retries.iter().map(|p| p.deadline).min();
1709 let stream = self.stream.as_mut().expect("connected");
1710 let msg = match next_retry {
1714 Some(deadline) => tokio::select! {
1715 m = stream.message() => m,
1716 _ = tokio::time::sleep_until(deadline) => continue,
1717 },
1718 None => stream.message().await,
1719 };
1720 match msg {
1721 Ok(Some(ev)) => {
1722 self.reconnect_attempts = 0;
1723 self.last_error = None;
1724 let cur = self.stream.as_ref().and_then(|s| s.cursor().copied());
1725 if let Some(out) = self.handle_event(ev, cur).await? {
1726 self.arm_commit();
1727 return Ok(out);
1728 }
1729 }
1731 Ok(None) => {
1732 self.teardown();
1733 self.reconnect_attempts = self.reconnect_attempts.saturating_add(1);
1734 }
1735 Err(e) if e.is_retryable() => {
1736 self.teardown();
1737 self.reconnect_attempts = self.reconnect_attempts.saturating_add(1);
1738 self.last_error = Some(e);
1739 }
1740 Err(e) => return Err(e),
1741 }
1742 }
1743 }
1744
1745 async fn handle_event(
1752 &mut self,
1753 ev: Event,
1754 cur: Option<Cursor>,
1755 ) -> Result<Option<Event>, StreamError> {
1756 if let Some(c) = cur {
1757 self.resume = Some(c);
1758 }
1759
1760 if let Event::WatchAddRejected(r) = &ev
1765 && r.reason == WatchAddRejectReason::RateLimited
1766 {
1767 let keys = retry_keys(r);
1768 let attempt =
1769 keys.iter().filter_map(|k| self.add_retry_attempts.get(k)).copied().max().unwrap_or(0);
1770 let exhausted = self.config.backoff.max_retries.is_some_and(|max| attempt >= max);
1771 if !exhausted {
1772 for k in keys {
1773 self.add_retry_attempts.insert(k, attempt.saturating_add(1));
1774 }
1775 let delay = self
1776 .config
1777 .backoff
1778 .delay_for(attempt)
1779 .max(std::time::Duration::from_secs(r.retry_after_secs.into()));
1780 self.pending_add_retries.push(PendingAddRetry {
1781 deadline: tokio::time::Instant::now() + delay,
1782 rejected: r.clone(),
1783 });
1784 return Ok(None);
1785 }
1786 for k in keys {
1787 self.add_retry_attempts.remove(&k);
1788 }
1789 }
1790
1791 match &ev {
1799 Event::WatchAddRejected(r) => self.mirror.forget_rejected(r),
1802 Event::TxidDepthReached { txid, depth, .. } => {
1803 if let Ok(t) = <[u8; 32]>::try_from(txid.as_slice()) {
1804 self.mirror.remove_depth_alarms(&[(t, *depth)]);
1805 }
1806 }
1807 Event::TxidFinalized { txid, .. } => {
1808 if let Ok(t) = <[u8; 32]>::try_from(txid.as_slice()) {
1809 self.mirror.remove_tx_lifecycle(&[t]);
1810 }
1811 }
1812 _ => {}
1813 }
1814
1815 let transient_reject = matches!(
1816 &ev,
1817 Event::CursorRejected {
1818 reason: CursorRejectReason::RateLimited | CursorRejectReason::ConcurrentReanchor,
1819 ..
1820 }
1821 );
1822 if transient_reject {
1823 if let Some(max) = self.config.backoff.max_retries
1827 && self.reanchor_attempts >= max
1828 {
1829 self.reanchor_attempts = 0;
1830 self.pending_reanchor = None;
1834 return Ok(Some(ev));
1835 }
1836 let delay = self.config.backoff.delay_for(self.reanchor_attempts);
1845 self.reanchor_attempts = self.reanchor_attempts.saturating_add(1);
1846 self.pending_reanchor =
1847 Some(PendingReanchor { deadline: tokio::time::Instant::now() + delay });
1848 return Ok(None);
1849 }
1850
1851 if let Event::CursorAccepted { from, .. } = &ev {
1852 if let Some(c) = from {
1863 self.resume = Some(*c);
1864 self.desired_cursor = Some(*c);
1865 }
1866 self.reanchor_attempts = 0;
1868 }
1869 Ok(Some(ev))
1870 }
1871
1872 async fn drive_pending_reanchor(&mut self) -> Result<(), StreamError> {
1886 let Some(pending) = self.pending_reanchor else {
1887 return Ok(());
1888 };
1889 tokio::time::sleep_until(pending.deadline).await;
1890 let res = match (self.desired_cursor, &self.handle) {
1891 (Some(c), Some(h)) => Some(h.set_cursor(c).await),
1892 _ => None,
1893 };
1894 self.pending_reanchor = None;
1897 self.after_send(res)
1898 }
1899
1900 async fn connect(&mut self) -> Result<(), StreamError> {
1905 self.seed_resume()?;
1906 let (handle, stream) = self.client.watch().await?;
1907 if let Some(loader) = self.config.watch_set_loader.clone() {
1915 let shared = Arc::new(Mutex::new(WatchSetMirror::default()));
1916 loader(WatchSetBuilder { mirror: shared.clone() })
1917 .await
1918 .map_err(StreamError::WatchSetLoader)?;
1919 self.mirror = std::mem::take(&mut *shared.lock().unwrap_or_else(|p| p.into_inner()));
1922 }
1923 for msg in self.mirror.control_messages() {
1924 handle.send_control(pb::SubscribeControl { msg: Some(msg) }).await?;
1925 }
1926 self.desired_cursor = self.resume;
1927 self.reanchor_attempts = 0;
1928 debug_assert!(self.pending_reanchor.is_none());
1931 if let Some(c) = self.resume {
1932 handle.set_cursor(c).await?;
1933 }
1934 self.handle = Some(handle);
1935 self.stream = Some(stream);
1936 Ok(())
1937 }
1938
1939 fn seed_resume(&mut self) -> Result<(), StreamError> {
1944 if !self.seeded {
1945 let loaded = self.config.cursor_store.load()?;
1946 if let Some(c) = loaded {
1947 self.committed.get_or_insert(c);
1948 }
1949 self.resume = self.resume.or(loaded).or(self.config.from_cursor);
1950 self.seeded = true;
1951 }
1952 Ok(())
1953 }
1954
1955 fn teardown(&mut self) {
1957 self.handle = None;
1958 self.stream = None;
1959 self.pending_add_retries.clear();
1962 self.add_retry_attempts.clear();
1963 self.pending_reanchor = None;
1968 }
1969
1970 async fn send_add_retry(&mut self, i: usize) -> Result<(), StreamError> {
1974 let msgs = self.mirror.subset_for(&self.pending_add_retries[i].rejected).control_messages();
1975 let res = match &self.handle {
1976 Some(h) => {
1977 let mut res = Ok(());
1978 for msg in msgs {
1979 res = h.send_control(pb::SubscribeControl { msg: Some(msg) }).await;
1980 if res.is_err() {
1981 break;
1982 }
1983 }
1984 Some(res)
1985 }
1986 None => None,
1987 };
1988 self.pending_add_retries.remove(i);
1989 self.after_send(res)
1990 }
1991
1992 fn after_send(&mut self, res: Option<Result<(), StreamError>>) -> Result<(), StreamError> {
1997 match res {
1998 Some(Err(StreamError::ControlClosed)) => {
1999 self.teardown();
2000 Ok(())
2001 }
2002 Some(Err(e)) => Err(e),
2003 _ => Ok(()),
2004 }
2005 }
2006
2007 fn arm_commit(&mut self) {
2009 self.commit_next = self.resume;
2010 }
2011
2012 fn commit_due(&mut self) -> Result<(), StreamError> {
2015 let Some(c) = self.commit_next else { return Ok(()) };
2016 if self.committed == Some(c) {
2017 self.commit_next = None;
2018 return Ok(());
2019 }
2020 self.config.cursor_store.save(c)?;
2024 if self.commit_next == Some(c) {
2025 self.commit_next = None;
2026 }
2027 self.committed = Some(c);
2028 Ok(())
2029 }
2030}
2031
2032const DESCRIPTOR_HISTORY: usize = 8;
2036
2037fn mask_prefix(prefix: &[u8], bits: u32) -> Vec<u8> {
2039 let mut out = prefix.to_vec();
2040 let rem = bits % 8;
2041 if rem != 0
2042 && let Some(last) = out.get_mut((bits / 8) as usize)
2043 {
2044 *last &= 0xffu8 << (8 - rem);
2045 }
2046 out
2047}
2048
2049fn cross_product(txids: &[Txid], depths: &[u32]) -> Vec<(Txid, u32)> {
2051 let mut out = Vec::with_capacity(txids.len() * depths.len());
2052 for t in txids {
2053 for d in depths {
2054 out.push((*t, *d));
2055 }
2056 }
2057 out
2058}
2059
2060fn validate_prefixes(
2063 prefixes: impl IntoIterator<Item = (Vec<u8>, u32)>,
2064) -> Result<Vec<pb::ScriptPrefix>, StreamError> {
2065 prefixes
2066 .into_iter()
2067 .map(|(prefix, bits)| validate_prefix(prefix, bits))
2068 .collect()
2069}
2070
2071#[cfg(feature = "bitcoin")]
2087fn sp_mirror_entries(
2088 targets: impl IntoIterator<Item = SilentPaymentTarget>,
2089) -> Result<Vec<([u8; 33], SilentPaymentTarget)>, StreamError> {
2090 let mut deduped: BTreeMap<[u8; 33], SilentPaymentTarget> = BTreeMap::new();
2091 for t in targets {
2092 let id = t.validate()?;
2093 deduped.insert(id, t); }
2095 Ok(deduped.into_iter().collect())
2096}
2097
2098fn is_reconnectable(e: &StreamError) -> bool {
2105 e.is_retryable() || matches!(e, StreamError::ControlClosed | StreamError::WatchSetLoader(_))
2106}
2107
2108#[cfg(test)]
2109mod tests {
2110 use super::*;
2111 use std::sync::Mutex;
2112
2113 #[test]
2116 fn mirror_scripts_floor_overwrite_and_remove() {
2117 let mut m = WatchSetMirror::default();
2118 m.add_scripts(&[([1u8; 32], Some(5_000)), ([2u8; 32], None)]);
2119 m.add_scripts(&[([1u8; 32], Some(9_000))]);
2121 m.remove_scripts(&[[2u8; 32]]);
2122
2123 let msgs = m.control_messages();
2124 assert_eq!(msgs.len(), 1);
2125 match &msgs[0] {
2126 Msg::AddScripts(a) => {
2127 assert_eq!(a.scripthashes, vec![[1u8; 32].to_vec()]);
2128 assert_eq!(a.min_values, vec![9_000]);
2130 }
2131 other => panic!("expected AddScripts, got {other:?}"),
2132 }
2133 }
2134
2135 #[test]
2136 fn mirror_unfloored_scripts_emit_empty_min_values() {
2137 let mut m = WatchSetMirror::default();
2138 m.add_scripts(&[([1u8; 32], None), ([2u8; 32], None)]);
2139 match &m.control_messages()[0] {
2140 Msg::AddScripts(a) => assert!(a.min_values.is_empty(), "no floor → empty min_values"),
2141 other => panic!("expected AddScripts, got {other:?}"),
2142 }
2143 }
2144
2145 #[test]
2146 fn mirror_lifecycle_grouped_by_autoclose_depth() {
2147 let mut m = WatchSetMirror::default();
2148 m.add_tx_lifecycle(&[[1u8; 32], [2u8; 32]], AutoClose::AtDepth(6));
2149 m.add_tx_lifecycle(&[[3u8; 32]], AutoClose::Never);
2150 let groups: Vec<_> = m
2151 .control_messages()
2152 .into_iter()
2153 .filter_map(|msg| match msg {
2154 Msg::AddTransactions(a) => Some((a.auto_close_depth, a.min_depths, a.txids.len())),
2155 _ => None,
2156 })
2157 .collect();
2158 assert!(groups.contains(&(0, vec![], 1)));
2161 assert!(groups.contains(&(6, vec![], 2)));
2162 }
2163
2164 #[test]
2165 fn mirror_depth_alarms_grouped_by_txid_with_min_depths() {
2166 let mut m = WatchSetMirror::default();
2167 m.add_depth_alarms(&[([7u8; 32], 3), ([7u8; 32], 6)]);
2168 let alarms: Vec<_> = m
2169 .control_messages()
2170 .into_iter()
2171 .filter_map(|msg| match msg {
2172 Msg::AddTransactions(a) => Some((a.txids.len(), a.min_depths, a.auto_close_depth)),
2173 _ => None,
2174 })
2175 .collect();
2176 assert_eq!(alarms, vec![(1, vec![3, 6], 0)]);
2179 }
2180
2181 #[test]
2182 fn mirror_categories_replayed_first() {
2183 let mut m = WatchSetMirror::default();
2184 m.add_outpoints(&[([1u8; 32], 0)]);
2185 m.set_categories(crate::Categories::CHAIN);
2186 let msgs = m.control_messages();
2187 assert!(matches!(msgs[0], Msg::SetCategories(_)), "categories lead the replay");
2188 }
2189
2190 #[test]
2191 fn mirror_raw_tx_opt_in_replayed() {
2192 let mut m = WatchSetMirror::default();
2196 m.add_scripts(&[([9u8; 32], None)]);
2197 assert!(
2198 !m.control_messages()
2199 .iter()
2200 .any(|msg| matches!(msg, Msg::SetWatchOptions(_))),
2201 "no opt-in → no SetWatchOptions replayed"
2202 );
2203
2204 m.set_watch_options(true);
2205 let msgs = m.control_messages();
2206 let opt = msgs.iter().find_map(|msg| match msg {
2207 Msg::SetWatchOptions(o) => Some(o.include_raw_tx),
2208 _ => None,
2209 });
2210 assert_eq!(opt, Some(true), "opt-in replayed as SetWatchOptions{{true}}");
2211 let opt_pos = msgs
2214 .iter()
2215 .position(|msg| matches!(msg, Msg::SetWatchOptions(_)))
2216 .unwrap();
2217 let add_pos = msgs
2218 .iter()
2219 .position(|msg| matches!(msg, Msg::AddScripts(_)))
2220 .unwrap();
2221 assert!(opt_pos < add_pos, "options precede the watch-set adds");
2222 }
2223
2224 #[test]
2225 fn mirror_descriptor_latest_window_wins() {
2226 let mut m = WatchSetMirror::default();
2227 m.add_descriptor("wpkh(xpub...)".into(), 20, 0);
2228 m.add_descriptor("wpkh(xpub...)".into(), 20, 5); let descs: Vec<_> = m
2230 .control_messages()
2231 .into_iter()
2232 .filter_map(|msg| match msg {
2233 Msg::AddDescriptor(d) => Some((d.gap_limit, d.start)),
2234 _ => None,
2235 })
2236 .collect();
2237 assert_eq!(descs, vec![(20, 5)], "only the latest window replays");
2238 }
2239
2240 #[test]
2241 fn mirror_removed_descriptor_is_not_replayed() {
2242 let mut m = WatchSetMirror::default();
2243 m.add_descriptor("wpkh(a)".into(), 20, 0);
2244 m.add_descriptor("wpkh(b)".into(), 20, 0);
2245 m.remove_descriptor("wpkh(a)");
2246 let descs: Vec<_> = m
2247 .control_messages()
2248 .into_iter()
2249 .filter_map(|msg| match msg {
2250 Msg::AddDescriptor(d) => Some(d.descriptor),
2251 _ => None,
2252 })
2253 .collect();
2254 assert_eq!(descs, vec!["wpkh(b)".to_string()]);
2257 }
2258
2259 #[test]
2260 fn mirror_empty_renders_nothing() {
2261 assert!(WatchSetMirror::default().control_messages().is_empty());
2262 }
2263
2264 #[cfg(feature = "bitcoin")]
2265 #[test]
2266 fn sp_mirror_entries_validates_and_dedups() {
2267 use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey};
2268 let secp = Secp256k1::new();
2269 let key = |b: u8| {
2270 PublicKey::from_secret_key(&secp, &SecretKey::from_slice(&[b; 32]).unwrap()).serialize()
2271 };
2272 let b_scan = [0x11u8; 32];
2273 let (spend_a, spend_b) = (key(0x22), key(0x33));
2274
2275 let t1 = SilentPaymentTarget { scan_secret: b_scan, spend_pubkey: spend_a, labels: vec![0] };
2279 let t2 =
2280 SilentPaymentTarget { scan_secret: b_scan, spend_pubkey: spend_b, labels: vec![1, 2] };
2281 let entries = sp_mirror_entries([t1, t2]).expect("valid targets");
2282 assert_eq!(entries.len(), 1, "same identity collapses to one entry");
2283 assert_eq!(entries[0].1.spend_pubkey, spend_b, "last write wins");
2284 assert_eq!(entries[0].1.labels, vec![1, 2]);
2285
2286 let mut bad_spend = spend_a;
2289 bad_spend[0] = 0x01; let bad =
2291 SilentPaymentTarget { scan_secret: b_scan, spend_pubkey: bad_spend, labels: vec![] };
2292 assert!(matches!(sp_mirror_entries([bad]), Err(StreamError::InvalidArgument(_))));
2293
2294 let bad2 =
2296 SilentPaymentTarget { scan_secret: [0u8; 32], spend_pubkey: spend_a, labels: vec![] };
2297 assert!(matches!(sp_mirror_entries([bad2]), Err(StreamError::InvalidArgument(_))));
2298 }
2299
2300 #[cfg(feature = "bitcoin")]
2301 #[tokio::test]
2302 async fn sp_add_enforces_connection_cap() {
2303 use crate::client::MAX_SP_TARGETS_PER_CONNECTION;
2304 let spend = valid_spend_pubkey();
2305 let target = |i: u8| SilentPaymentTarget {
2306 scan_secret: [i; 32],
2307 spend_pubkey: spend,
2308 labels: vec![0],
2309 };
2310 let store = Arc::new(MemStore::default());
2311 let mut w = watch_with(&store);
2312 let full: Vec<_> = (1..=MAX_SP_TARGETS_PER_CONNECTION as u8).map(target).collect();
2314 w.add_silent_payments(full).await.expect("cap-exact add accepted");
2315 w.add_silent_payments([target(1)]).await.expect("re-assert at cap is free");
2317 let err = w
2320 .add_silent_payments([target(MAX_SP_TARGETS_PER_CONNECTION as u8 + 1)])
2321 .await
2322 .expect_err("over-cap add must be rejected");
2323 assert!(matches!(err, StreamError::InvalidArgument(_)), "got {err:?}");
2324 }
2325
2326 #[cfg(feature = "bitcoin")]
2327 #[tokio::test]
2328 async fn sp_add_enforces_label_cap() {
2329 use crate::client::MAX_SP_LABELS_PER_TARGET;
2330 let spend = valid_spend_pubkey();
2331 let store = Arc::new(MemStore::default());
2332 let mut w = watch_with(&store);
2333 let over = SilentPaymentTarget {
2335 scan_secret: [1u8; 32],
2336 spend_pubkey: spend,
2337 labels: (0..=MAX_SP_LABELS_PER_TARGET as u32).collect(),
2338 };
2339 let err = w.add_silent_payments([over]).await.expect_err("over-label rejected");
2340 assert!(matches!(err, StreamError::InvalidArgument(_)), "got {err:?}");
2341 let ok = SilentPaymentTarget {
2343 scan_secret: [1u8; 32],
2344 spend_pubkey: spend,
2345 labels: (0..MAX_SP_LABELS_PER_TARGET as u32).collect(),
2346 };
2347 w.add_silent_payments([ok]).await.expect("cap-exact labels accepted");
2348 }
2349
2350 #[cfg(feature = "bitcoin")]
2352 fn valid_spend_pubkey() -> [u8; 33] {
2353 use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey};
2354 let secp = Secp256k1::new();
2355 PublicKey::from_secret_key(&secp, &SecretKey::from_slice(&[0x22u8; 32]).unwrap()).serialize()
2356 }
2357
2358 #[derive(Default)]
2361 struct MemStore(Mutex<Option<Cursor>>);
2362 impl CursorStore for MemStore {
2363 fn load(&self) -> Result<Option<Cursor>, StreamError> {
2364 Ok(*self.0.lock().unwrap())
2365 }
2366 fn save(&self, cursor: Cursor) -> Result<(), StreamError> {
2367 *self.0.lock().unwrap() = Some(cursor);
2368 Ok(())
2369 }
2370 }
2371
2372 fn watch_with(store: &Arc<MemStore>) -> ResilientWatch {
2373 ResilientWatch::new(
2374 StreamClient::for_test(),
2375 ResilientWatchConfig::new().cursor_store(store.clone()),
2376 )
2377 }
2378
2379 fn cur(height: u32) -> Cursor {
2380 Cursor { height, tx_index: 0, mempool_seq: 0, instance_id: 1 }
2381 }
2382
2383 #[tokio::test]
2386 async fn offline_edits_accumulate_for_replay() {
2387 let store = Arc::new(MemStore::default());
2388 let mut w = watch_with(&store);
2389 w.add_scripts([([1u8; 32], None)]).await.unwrap();
2391 w.add_outpoints([([9u8; 32], 2)]).await.unwrap();
2392 w.add_depth_alarms([[7u8; 32]], [0]).await.unwrap(); let kinds: Vec<_> = w
2394 .mirror
2395 .control_messages()
2396 .into_iter()
2397 .map(|m| std::mem::discriminant(&m))
2398 .collect();
2399 assert_eq!(kinds.len(), 2);
2401 assert!(w.handle.is_none(), "still disconnected");
2402 }
2403
2404 #[tokio::test]
2407 async fn cursor_accepted_is_surfaced_and_resets_retry_counter() {
2408 let store = Arc::new(MemStore::default());
2409 let mut w = watch_with(&store);
2410 w.reanchor_attempts = 3;
2411 let ev = Event::CursorAccepted { from: Some(cur(100)), clamped: false, earliest_replayed: 100 };
2412 let out = w.handle_event(ev, None).await.unwrap();
2413 assert!(matches!(out, Some(Event::CursorAccepted { .. })), "accept surfaces to caller");
2414 assert_eq!(w.reanchor_attempts, 0, "a landed re-anchor resets the retry counter");
2415 }
2416
2417 #[tokio::test]
2418 async fn cursor_accepted_adopts_anchor_before_any_replayed_event() {
2419 let store = Arc::new(MemStore::default());
2420 let mut w = watch_with(&store);
2421 w.resume = Some(cur(1000));
2424 w.desired_cursor = Some(cur(200));
2425 let ev = Event::CursorAccepted { from: Some(cur(200)), clamped: false, earliest_replayed: 200 };
2428 let out = w.handle_event(ev, None).await.unwrap();
2429 assert!(matches!(out, Some(Event::CursorAccepted { .. })));
2430 assert_eq!(w.resume, Some(cur(200)), "accepted anchor becomes the resume point at once");
2433 assert_eq!(w.desired_cursor, Some(cur(200)));
2434 }
2435
2436 #[tokio::test]
2437 async fn terminal_reject_is_surfaced_for_escalation() {
2438 let store = Arc::new(MemStore::default());
2439 let mut w = watch_with(&store);
2440 let ev = Event::CursorRejected {
2441 reason: CursorRejectReason::NoSource,
2442 current_head: Some(cur(500)),
2443 };
2444 let out = w.handle_event(ev, None).await.unwrap();
2445 assert!(
2446 matches!(out, Some(Event::CursorRejected { reason: CursorRejectReason::NoSource, .. })),
2447 "NoSource is terminal — surfaced so the caller can resnapshot"
2448 );
2449 }
2450
2451 #[tokio::test]
2452 async fn transient_reject_retries_in_place_until_budget_exhausted() {
2453 let store = Arc::new(MemStore::default());
2454 let mut w = ResilientWatch::new(
2455 StreamClient::for_test(),
2456 ResilientWatchConfig::new()
2457 .cursor_store(store.clone())
2458 .backoff(Backoff {
2460 initial: std::time::Duration::from_millis(0),
2461 max: std::time::Duration::from_millis(0),
2462 multiplier: 1.0,
2463 max_retries: Some(2),
2464 }),
2465 );
2466 w.desired_cursor = Some(cur(100));
2467 let reject = || Event::CursorRejected {
2468 reason: CursorRejectReason::RateLimited,
2469 current_head: None,
2470 };
2471 assert!(w.handle_event(reject(), None).await.unwrap().is_none(), "retry 1 consumed");
2476 assert!(w.pending_reanchor.is_some(), "retry 1 deferred to a pending slot");
2477 assert!(w.handle_event(reject(), None).await.unwrap().is_none(), "retry 2 consumed");
2478 let out = w.handle_event(reject(), None).await.unwrap();
2479 assert!(
2480 matches!(out, Some(Event::CursorRejected { .. })),
2481 "budget exhausted → surfaced for the caller to escalate"
2482 );
2483 assert_eq!(w.reanchor_attempts, 0, "counter resets after surfacing");
2484 }
2485
2486 #[tokio::test]
2487 async fn transient_reject_retry_is_cancel_safe() {
2488 let store = Arc::new(MemStore::default());
2494 let mut w = ResilientWatch::new(
2495 StreamClient::for_test(),
2496 ResilientWatchConfig::new().cursor_store(store.clone()).backoff(Backoff {
2497 initial: std::time::Duration::from_secs(30),
2500 max: std::time::Duration::from_secs(30),
2501 multiplier: 1.0,
2502 max_retries: Some(3),
2503 }),
2504 );
2505 w.desired_cursor = Some(cur(100));
2506
2507 let out = w
2510 .handle_event(
2511 Event::CursorRejected { reason: CursorRejectReason::RateLimited, current_head: None },
2512 None,
2513 )
2514 .await
2515 .unwrap();
2516 assert!(out.is_none(), "transient reject consumed");
2517 assert_eq!(w.reanchor_attempts, 1, "one retry charged");
2518 assert!(w.pending_reanchor.is_some(), "retry deferred to a resumable slot");
2519
2520 tokio::select! {
2524 biased;
2525 _ = w.drive_pending_reanchor() => unreachable!("30s backoff cannot have elapsed"),
2526 _ = tokio::time::sleep(std::time::Duration::from_millis(1)) => {}
2527 }
2528 assert!(w.pending_reanchor.is_some(), "a cancelled drive leaves the retry resumable");
2529 assert_eq!(w.reanchor_attempts, 1, "the cancelled drive did not re-charge the budget");
2530
2531 w.pending_reanchor.as_mut().unwrap().deadline = tokio::time::Instant::now();
2534 w.drive_pending_reanchor().await.unwrap();
2535 assert!(w.pending_reanchor.is_none(), "the resumed drive completes the retry");
2536 assert_eq!(w.reanchor_attempts, 1, "resuming does not re-charge the budget");
2537 }
2538
2539 fn drained_set_cursors(
2542 rx: &mut tokio::sync::mpsc::Receiver<pb::SubscribeControl>,
2543 ) -> Vec<Cursor> {
2544 let mut out = Vec::new();
2545 while let Ok(ctrl) = rx.try_recv() {
2546 if let Some(Msg::SetCursor(s)) = ctrl.msg {
2547 out.push(s.cursor.expect("a SetCursor carries a cursor"));
2548 }
2549 }
2550 out
2551 }
2552
2553 #[tokio::test]
2554 async fn set_cursor_supersedes_a_pending_transient_retry() {
2555 let store = Arc::new(MemStore::default());
2560 let (handle, mut rx) = crate::client::WatchHandle::for_test();
2561 let mut w = watch_with(&store);
2562 w.handle = Some(handle);
2563 w.desired_cursor = Some(cur(100));
2564
2565 w.handle_event(
2567 Event::CursorRejected { reason: CursorRejectReason::RateLimited, current_head: None },
2568 None,
2569 )
2570 .await
2571 .unwrap();
2572 assert!(w.pending_reanchor.is_some(), "retry deferred");
2573
2574 w.set_cursor(cur(200)).await.unwrap();
2577 assert!(w.pending_reanchor.is_none(), "explicit re-anchor drops the stale deferred retry");
2578 assert_eq!(drained_set_cursors(&mut rx), vec![cur(200)], "only the fresh anchor is sent");
2579
2580 w.drive_pending_reanchor().await.unwrap();
2583 assert!(drained_set_cursors(&mut rx).is_empty(), "a superseded retry re-sends nothing");
2584 }
2585
2586 #[tokio::test]
2587 async fn a_deferred_retry_re_sends_the_live_desired_cursor() {
2588 let store = Arc::new(MemStore::default());
2592 let (handle, mut rx) = crate::client::WatchHandle::for_test();
2593 let mut w = ResilientWatch::new(
2594 StreamClient::for_test(),
2595 ResilientWatchConfig::new().cursor_store(store.clone()).backoff(Backoff {
2596 initial: std::time::Duration::ZERO,
2597 max: std::time::Duration::ZERO,
2598 multiplier: 1.0,
2599 max_retries: Some(3),
2600 }),
2601 );
2602 w.handle = Some(handle);
2603 w.desired_cursor = Some(cur(100));
2604
2605 w.handle_event(
2607 Event::CursorRejected {
2608 reason: CursorRejectReason::ConcurrentReanchor,
2609 current_head: None,
2610 },
2611 None,
2612 )
2613 .await
2614 .unwrap();
2615 w.desired_cursor = Some(cur(200));
2617
2618 w.drive_pending_reanchor().await.unwrap();
2620 assert_eq!(drained_set_cursors(&mut rx), vec![cur(200)], "the live anchor is re-sent");
2621 assert!(w.pending_reanchor.is_none(), "the retry completed");
2622 }
2623
2624 #[tokio::test]
2627 async fn fired_depth_alarm_is_pruned_from_the_mirror() {
2628 let store = Arc::new(MemStore::default());
2629 let mut w = watch_with(&store);
2630 w.add_depth_alarms([[7u8; 32]], [3, 6]).await.unwrap();
2633 let ev = Event::TxidDepthReached { txid: [7u8; 32].to_vec(), depth: 3, height: 100 };
2634 let out = w.handle_event(ev, None).await.unwrap();
2635 assert!(matches!(out, Some(Event::TxidDepthReached { .. })), "still surfaced to caller");
2636 assert!(!w.mirror.depth_alarms.contains(&([7u8; 32], 3)), "fired alarm pruned");
2639 assert!(w.mirror.depth_alarms.contains(&([7u8; 32], 6)), "unfired alarm retained");
2640 }
2641
2642 #[tokio::test]
2643 async fn finalized_lifecycle_watch_is_pruned_from_the_mirror() {
2644 let store = Arc::new(MemStore::default());
2645 let mut w = watch_with(&store);
2646 w.add_tx_lifecycle([[8u8; 32]], AutoClose::AtDepth(6)).await.unwrap();
2647 let ev = Event::TxidFinalized { txid: [8u8; 32].to_vec(), depth: 6, height: 200 };
2648 let out = w.handle_event(ev, None).await.unwrap();
2649 assert!(matches!(out, Some(Event::TxidFinalized { .. })), "still surfaced to caller");
2650 assert!(
2651 !w.mirror.tx_lifecycles.contains_key(&[8u8; 32]),
2652 "auto-close finalize prunes the lifecycle watch the server evicted"
2653 );
2654 }
2655
2656 fn refused(r: pb::WatchAddRejected) -> Event {
2659 Event::WatchAddRejected(WatchAddRejected::from_proto(r))
2660 }
2661
2662 #[tokio::test]
2663 async fn refused_adds_are_pruned_from_the_mirror() {
2664 use pb::watch_add_rejected::{Kind, Reason};
2665 let store = Arc::new(MemStore::default());
2666 let mut w = watch_with(&store);
2667 w.add_scripts([([1u8; 32], None), ([2u8; 32], None)]).await.unwrap();
2668 w.add_outpoints([([3u8; 32], 0), ([3u8; 32], 1)]).await.unwrap();
2669 w.add_script_prefixes([(vec![0xab, 0xcd], 12)]).await.unwrap();
2672
2673 let out = w
2674 .handle_event(
2675 refused(pb::WatchAddRejected {
2676 kind: Kind::Scripts as i32,
2677 reason: Reason::QuotaExceeded as i32,
2678 scripthashes: vec![vec![2u8; 32]],
2679 ..Default::default()
2680 }),
2681 None,
2682 )
2683 .await
2684 .unwrap();
2685 assert!(matches!(out, Some(Event::WatchAddRejected(_))), "still surfaced to the caller");
2686 assert!(w.mirror.scripts.contains_key(&[1u8; 32]), "an item the server holds stays");
2687 assert!(!w.mirror.scripts.contains_key(&[2u8; 32]), "a refused script is dropped");
2688
2689 w.handle_event(
2690 refused(pb::WatchAddRejected {
2691 kind: Kind::Outpoints as i32,
2692 reason: Reason::PermissionDenied as i32,
2693 outpoints: vec![pb::Outpoint { txid: vec![3u8; 32], vout: 1 }],
2694 ..Default::default()
2695 }),
2696 None,
2697 )
2698 .await
2699 .unwrap();
2700 assert!(w.mirror.outpoints.contains(&([3u8; 32], 0)));
2701 assert!(!w.mirror.outpoints.contains(&([3u8; 32], 1)));
2702
2703 w.handle_event(
2704 refused(pb::WatchAddRejected {
2705 kind: Kind::ScriptPrefixes as i32,
2706 reason: Reason::CapExceeded as i32,
2707 prefixes: vec![pb::ScriptPrefix { prefix: vec![0xab, 0xc0], bits: 12 }],
2708 ..Default::default()
2709 }),
2710 None,
2711 )
2712 .await
2713 .unwrap();
2714 assert!(w.mirror.prefixes.is_empty(), "the masked echo matches the caller's prefix");
2715 }
2716
2717 #[tokio::test]
2718 async fn refused_descriptor_slide_falls_back_to_the_window_the_server_holds() {
2719 use pb::watch_add_rejected::{Kind, Reason};
2720 let store = Arc::new(MemStore::default());
2721 let mut w = watch_with(&store);
2722 w.add_descriptor("wpkh(a)", 20, 0).await.unwrap();
2723 w.add_descriptor("wpkh(a)", 20, 20).await.unwrap();
2724 w.add_descriptor("wpkh(b)", 20, 0).await.unwrap();
2725
2726 w.handle_event(
2728 refused(pb::WatchAddRejected {
2729 kind: Kind::Descriptor as i32,
2730 reason: Reason::QuotaExceeded as i32,
2731 descriptor: "wpkh(a)".into(),
2732 gap_limit: 20,
2733 start: 20,
2734 descriptor_kept: true,
2735 ..Default::default()
2736 }),
2737 None,
2738 )
2739 .await
2740 .unwrap();
2741 assert_eq!(w.mirror.descriptors.get("wpkh(a)"), Some(&(20, 0)), "replays the held window");
2742
2743 w.handle_event(
2745 refused(pb::WatchAddRejected {
2746 kind: Kind::Descriptor as i32,
2747 reason: Reason::CapExceeded as i32,
2748 descriptor: "wpkh(b)".into(),
2749 gap_limit: 20,
2750 start: 0,
2751 descriptor_kept: false,
2752 ..Default::default()
2753 }),
2754 None,
2755 )
2756 .await
2757 .unwrap();
2758 assert!(!w.mirror.descriptors.contains_key("wpkh(b)"));
2759
2760 w.add_descriptor("wpkh(c)", 20, 0).await.unwrap();
2763 w.add_descriptor("wpkh(c)", 20, 20).await.unwrap();
2764 w.add_descriptor("wpkh(c)", 20, 40).await.unwrap();
2765 for start in [20, 40] {
2766 w.handle_event(
2767 refused(pb::WatchAddRejected {
2768 kind: Kind::Descriptor as i32,
2769 reason: Reason::QuotaExceeded as i32,
2770 descriptor: "wpkh(c)".into(),
2771 gap_limit: 20,
2772 start,
2773 descriptor_kept: true,
2774 ..Default::default()
2775 }),
2776 None,
2777 )
2778 .await
2779 .unwrap();
2780 }
2781 assert_eq!(w.mirror.descriptors.get("wpkh(c)"), Some(&(20, 0)), "falls back past both refusals");
2782
2783 w.add_descriptor("wpkh(a)", 20, 40).await.unwrap();
2785 w.handle_event(
2786 refused(pb::WatchAddRejected {
2787 kind: Kind::Descriptor as i32,
2788 reason: Reason::QuotaExceeded as i32,
2789 descriptor: "wpkh(a)".into(),
2790 gap_limit: 20,
2791 start: 20,
2792 descriptor_kept: true,
2793 ..Default::default()
2794 }),
2795 None,
2796 )
2797 .await
2798 .unwrap();
2799 assert_eq!(w.mirror.descriptors.get("wpkh(a)"), Some(&(20, 40)));
2800 }
2801
2802 fn zero_backoff(max_retries: Option<u32>) -> Backoff {
2803 Backoff {
2804 initial: std::time::Duration::ZERO,
2805 max: std::time::Duration::ZERO,
2806 multiplier: 1.0,
2807 max_retries,
2808 }
2809 }
2810
2811 fn rate_limited_scripts(shs: &[u8]) -> Event {
2812 use pb::watch_add_rejected::{Kind, Reason};
2813 refused(pb::WatchAddRejected {
2814 kind: Kind::Scripts as i32,
2815 reason: Reason::RateLimited as i32,
2816 retry_after_secs: 0,
2817 scripthashes: shs.iter().map(|b| vec![*b; 32]).collect(),
2818 ..Default::default()
2819 })
2820 }
2821
2822 fn sent_scripts(rx: &mut tokio::sync::mpsc::Receiver<pb::SubscribeControl>) -> Vec<pb::AddScripts> {
2823 let mut out = Vec::new();
2824 while let Ok(ctrl) = rx.try_recv() {
2825 if let Some(Msg::AddScripts(a)) = ctrl.msg {
2826 out.push(a);
2827 }
2828 }
2829 out
2830 }
2831
2832 #[tokio::test]
2833 async fn rate_limited_add_is_re_sent_from_the_mirror() {
2834 let store = Arc::new(MemStore::default());
2835 let (handle, mut rx) = crate::client::WatchHandle::for_test();
2836 let mut w = ResilientWatch::new(
2837 StreamClient::for_test(),
2838 ResilientWatchConfig::new().cursor_store(store.clone()).backoff(zero_backoff(Some(3))),
2839 );
2840 w.handle = Some(handle);
2841 w.add_scripts([([1u8; 32], None), ([2u8; 32], Some(500)), ([3u8; 32], None)]).await.unwrap();
2842 let _ = sent_scripts(&mut rx);
2843
2844 let out = w.handle_event(rate_limited_scripts(&[2, 3]), None).await.unwrap();
2847 assert!(out.is_none(), "a rate-limited add is retried, not surfaced");
2848 assert!(w.mirror.scripts.contains_key(&[2u8; 32]) && w.mirror.scripts.contains_key(&[3u8; 32]));
2849 assert_eq!(w.pending_add_retries.len(), 1);
2850
2851 w.remove_scripts([[3u8; 32]]).await.unwrap();
2853 while rx.try_recv().is_ok() {}
2854 w.send_add_retry(0).await.unwrap();
2855 let sent = sent_scripts(&mut rx);
2856 assert_eq!(sent.len(), 1, "one re-send: {sent:?}");
2857 assert_eq!(sent[0].scripthashes, vec![vec![2u8; 32]], "only what the caller still holds");
2858 assert_eq!(sent[0].min_values, vec![500], "with its current floor");
2859 assert!(w.pending_add_retries.is_empty());
2860 }
2861
2862 #[tokio::test]
2863 async fn rate_limited_add_is_dropped_and_surfaced_once_the_budget_runs_out() {
2864 let store = Arc::new(MemStore::default());
2865 let mut w = ResilientWatch::new(
2866 StreamClient::for_test(),
2867 ResilientWatchConfig::new().cursor_store(store.clone()).backoff(zero_backoff(Some(1))),
2868 );
2869 w.add_scripts([([2u8; 32], None)]).await.unwrap();
2870 assert!(w.handle_event(rate_limited_scripts(&[2]), None).await.unwrap().is_none(), "retry 1");
2871 let out = w.handle_event(rate_limited_scripts(&[2]), None).await.unwrap();
2873 assert!(matches!(out, Some(Event::WatchAddRejected(_))), "surfaced once the budget is spent");
2874 assert!(!w.mirror.scripts.contains_key(&[2u8; 32]), "and dropped from the mirror");
2875 assert!(w.add_retry_attempts.is_empty(), "its budget is released");
2876 }
2877
2878 #[tokio::test]
2879 async fn a_reconnect_resets_pending_add_retries() {
2880 let store = Arc::new(MemStore::default());
2881 let mut w = ResilientWatch::new(
2882 StreamClient::for_test(),
2883 ResilientWatchConfig::new().cursor_store(store.clone()).backoff(zero_backoff(Some(3))),
2884 );
2885 w.add_scripts([([2u8; 32], None)]).await.unwrap();
2886 w.handle_event(rate_limited_scripts(&[2]), None).await.unwrap();
2887 w.teardown();
2888 assert!(w.pending_add_retries.is_empty() && w.add_retry_attempts.is_empty());
2889 assert!(w.mirror.scripts.contains_key(&[2u8; 32]), "the reconnect re-sends it with the mirror");
2890 }
2891
2892 #[tokio::test]
2895 async fn confirmed_cursor_commits_on_the_following_poll() {
2896 let store = Arc::new(MemStore::default());
2897 let mut w = watch_with(&store);
2898 let c = cur(900);
2899
2900 w.commit_due().unwrap();
2902 let out = w
2903 .handle_event(Event::BlockConnected { hash: vec![0xaa], height: 900 }, Some(c))
2904 .await
2905 .unwrap();
2906 assert!(out.is_some());
2907 w.arm_commit();
2908 assert_eq!(store.load().unwrap(), None);
2910
2911 w.commit_due().unwrap();
2913 assert_eq!(store.load().unwrap(), Some(c));
2914 }
2915
2916 #[derive(Default)]
2919 struct FlakyStore {
2920 inner: Mutex<Option<Cursor>>,
2921 fail_next: Mutex<u32>,
2922 }
2923 impl CursorStore for FlakyStore {
2924 fn load(&self) -> Result<Option<Cursor>, StreamError> {
2925 Ok(*self.inner.lock().unwrap())
2926 }
2927 fn save(&self, cursor: Cursor) -> Result<(), StreamError> {
2928 let mut left = self.fail_next.lock().unwrap();
2929 if *left > 0 {
2930 *left -= 1;
2931 return Err(StreamError::Decode("disk full".into()));
2932 }
2933 *self.inner.lock().unwrap() = Some(cursor);
2934 Ok(())
2935 }
2936 }
2937
2938 #[tokio::test]
2946 async fn commit_retry_after_a_store_failure_actually_writes() {
2947 let store = Arc::new(FlakyStore { fail_next: Mutex::new(1), ..Default::default() });
2948 let mut w = ResilientWatch::new(
2949 StreamClient::for_test(),
2950 ResilientWatchConfig { cursor_store: store.clone(), ..Default::default() },
2951 );
2952 let c = cur(900);
2953
2954 w.handle_event(Event::BlockConnected { hash: vec![0xaa], height: 900 }, Some(c))
2955 .await
2956 .unwrap();
2957 w.arm_commit();
2958
2959 let err = w.commit_due().unwrap_err();
2960 assert!(matches!(err, StreamError::Decode(_)), "the transient failure surfaces");
2961 assert_eq!(store.load().unwrap(), None, "nothing was written");
2962
2963 w.commit_due().expect("retry after a transient failure");
2964 assert_eq!(
2965 store.load().unwrap(),
2966 Some(c),
2967 "the retried commit must persist the cursor the failed one armed; \
2968 returning Ok() with an empty store is a false durability ack"
2969 );
2970 }
2971
2972 #[tokio::test]
2973 async fn seed_resume_prefers_store_over_from_cursor() {
2974 let store = Arc::new(MemStore::default());
2975 store.save(cur(800)).unwrap();
2976 let mut w = ResilientWatch::new(
2977 StreamClient::for_test(),
2978 ResilientWatchConfig::new().cursor_store(store.clone()).from_cursor(cur(1)),
2979 );
2980 w.seed_resume().unwrap();
2981 assert_eq!(w.resume, Some(cur(800)), "persisted cursor wins over from_cursor");
2982 assert_eq!(w.committed, Some(cur(800)), "loaded cursor seeds committed");
2983 }
2984
2985 async fn run_loader(config: &ResilientWatchConfig) -> Result<WatchSetMirror, StreamError> {
2990 let loader = config.watch_set_loader.clone().expect("loader configured");
2991 let shared = Arc::new(Mutex::new(WatchSetMirror::default()));
2992 loader(WatchSetBuilder { mirror: shared.clone() })
2993 .await
2994 .map_err(StreamError::WatchSetLoader)?;
2995 Ok(std::mem::take(&mut *shared.lock().unwrap()))
2996 }
2997
2998 #[test]
2999 fn builder_facade_records_every_watch_kind() {
3000 let m = Arc::new(Mutex::new(WatchSetMirror::default()));
3001 let b = WatchSetBuilder { mirror: m.clone() };
3002 b.set_categories(crate::Categories::CHAIN);
3003 b.add_scripts([([1u8; 32], Some(5_000))]);
3004 b.add_outpoints([([2u8; 32], 0)]);
3005 b.add_tx_lifecycle([[3u8; 32]], AutoClose::AtDepth(6));
3006 b.add_depth_alarms([[4u8; 32]], [3, 6]);
3007 b.add_descriptor("wpkh(xpub...)", 20, 0);
3008 b.add_script_prefixes([(vec![0xab], 8)]).unwrap();
3009
3010 let mirror = std::mem::take(&mut *m.lock().unwrap());
3011 let kinds: Vec<_> = mirror
3012 .control_messages()
3013 .into_iter()
3014 .map(|msg| std::mem::discriminant(&msg))
3015 .collect();
3016 assert_eq!(kinds.len(), 7, "every declared kind renders to a control message");
3019 assert!(
3020 matches!(mirror.control_messages()[0], Msg::SetCategories(_)),
3021 "categories still lead the replay"
3022 );
3023 }
3024
3025 #[test]
3026 fn builder_depth_alarms_filter_and_cross_product() {
3027 let m = Arc::new(Mutex::new(WatchSetMirror::default()));
3028 let b = WatchSetBuilder { mirror: m.clone() };
3029 b.add_depth_alarms([[1u8; 32], [2u8; 32]], [0, 5]);
3032 let mirror = std::mem::take(&mut *m.lock().unwrap());
3033 assert!(mirror.depth_alarms.contains(&([1u8; 32], 5)));
3034 assert!(mirror.depth_alarms.contains(&([2u8; 32], 5)));
3035 assert_eq!(mirror.depth_alarms.len(), 2, "depth 0 dropped, depth 5 kept");
3036 }
3037
3038 #[test]
3039 fn builder_prefix_validation_error_aborts_the_load() {
3040 let m = Arc::new(Mutex::new(WatchSetMirror::default()));
3041 let b = WatchSetBuilder { mirror: m };
3042 assert!(matches!(
3044 b.add_script_prefixes([(vec![], 0)]),
3045 Err(StreamError::InvalidArgument(_))
3046 ));
3047 }
3048
3049 #[tokio::test]
3050 async fn loader_rebuilds_the_canonical_watch_set() {
3051 let config = ResilientWatchConfig::new().watch_set_loader(|b: WatchSetBuilder| async move {
3052 b.add_scripts([([7u8; 32], None), ([8u8; 32], Some(1_000))]);
3054 b.add_outpoints([([9u8; 32], 1)]);
3055 Ok(())
3056 });
3057 let mirror = run_loader(&config).await.expect("loader succeeds");
3058 assert!(mirror.scripts.contains_key(&[7u8; 32]));
3059 assert_eq!(mirror.scripts.get(&[8u8; 32]), Some(&Some(1_000)));
3060 assert!(mirror.outpoints.contains(&([9u8; 32], 1)));
3061 }
3062
3063 #[tokio::test]
3064 async fn loader_canonical_set_replaces_prior_in_process_edits() {
3065 let mut w = ResilientWatch::new(
3068 StreamClient::for_test(),
3069 ResilientWatchConfig::new().watch_set_loader(|b: WatchSetBuilder| async move {
3070 b.add_scripts([([2u8; 32], None)]);
3071 Ok(())
3072 }),
3073 );
3074 w.add_scripts([([1u8; 32], None)]).await.unwrap();
3076 assert!(w.mirror.scripts.contains_key(&[1u8; 32]));
3077 w.mirror = run_loader(&w.config).await.unwrap();
3079 assert!(!w.mirror.scripts.contains_key(&[1u8; 32]), "stale in-process edit dropped");
3080 assert!(w.mirror.scripts.contains_key(&[2u8; 32]), "canonical loaded script present");
3081 }
3082
3083 #[tokio::test]
3084 async fn loader_error_maps_to_watch_set_loader_and_is_reconnectable() {
3085 let config = ResilientWatchConfig::new().watch_set_loader(|_b: WatchSetBuilder| async move {
3086 Err("source-of-truth unreachable".into())
3087 });
3088 let err = run_loader(&config).await.expect_err("loader failure surfaces");
3089 assert!(matches!(err, StreamError::WatchSetLoader(_)));
3090 assert!(
3091 is_reconnectable(&err),
3092 "a loader failure is a transient reconnect-level condition, not caller-facing"
3093 );
3094 assert!(err.to_string().contains("source-of-truth unreachable"));
3096 }
3097
3098 #[test]
3099 fn no_loader_leaves_behavior_unchanged() {
3100 assert!(ResilientWatchConfig::new().watch_set_loader.is_none());
3102 }
3103
3104 #[test]
3107 fn reconcile_to_emits_only_the_delta_removes_before_adds() {
3108 let mut old = WatchSetMirror::default();
3110 old.add_scripts(&[([1u8; 32], None), ([2u8; 32], Some(500))]); old.add_outpoints(&[([9u8; 32], 0)]);
3112 old.add_descriptor("desc-keep".into(), 20, 0);
3113 old.add_descriptor("desc-drop".into(), 20, 0);
3114
3115 let mut new = WatchSetMirror::default();
3116 new.add_scripts(&[([1u8; 32], None), ([2u8; 32], Some(999))]); new.add_scripts(&[([3u8; 32], None)]); new.add_descriptor("desc-keep".into(), 20, 0); new.add_descriptor("desc-new".into(), 20, 0);
3122
3123 let (msgs, counts) = old.reconcile_to(&new);
3124
3125 assert_eq!(counts.added, 3, "sh2(changed) + sh3 + desc-new");
3129 assert_eq!(counts.removed, 2, "outpoint + desc-drop");
3130 assert_eq!(counts.unchanged, 2, "sh1 + desc-keep");
3131
3132 let last_remove = msgs.iter().rposition(|m| {
3136 matches!(
3137 m,
3138 Msg::RemoveScripts(_)
3139 | Msg::RemoveOutpoints(_)
3140 | Msg::RemoveTransactions(_)
3141 | Msg::RemoveScriptPrefixes(_)
3142 | Msg::RemoveDescriptor(_)
3143 )
3144 });
3145 let first_add = msgs.iter().position(|m| {
3146 matches!(
3147 m,
3148 Msg::AddScripts(_)
3149 | Msg::AddOutpoints(_)
3150 | Msg::AddTransactions(_)
3151 | Msg::AddDescriptor(_)
3152 | Msg::AddScriptPrefixes(_)
3153 )
3154 });
3155 if let (Some(lr), Some(fa)) = (last_remove, first_add) {
3156 assert!(lr < fa, "every Remove must precede every quota-consuming Add");
3157 }
3158 assert!(msgs.iter().any(|m| matches!(m, Msg::RemoveDescriptor(d) if d.descriptor == "desc-drop")));
3160 }
3161
3162 #[test]
3163 fn reconcile_disjoint_swap_removes_before_adds_for_quota_safety() {
3164 let mut live = WatchSetMirror::default();
3169 live.add_scripts(&[([1u8; 32], None), ([2u8; 32], None), ([3u8; 32], None)]);
3170 let mut target = WatchSetMirror::default();
3171 target.add_scripts(&[([4u8; 32], None), ([5u8; 32], None), ([6u8; 32], None)]);
3172
3173 let (msgs, counts) = live.reconcile_to(&target);
3174 assert_eq!((counts.added, counts.removed, counts.unchanged), (3, 3, 0));
3175
3176 let remove_pos = msgs.iter().position(|m| matches!(m, Msg::RemoveScripts(_)));
3177 let add_pos = msgs.iter().position(|m| matches!(m, Msg::AddScripts(_)));
3178 assert!(
3179 remove_pos < add_pos,
3180 "RemoveScripts must be sent before AddScripts so freed quota covers the replacements",
3181 );
3182 }
3183
3184 #[test]
3185 fn reconcile_identical_sets_is_empty() {
3186 let mut m = WatchSetMirror::default();
3187 m.add_scripts(&[([1u8; 32], Some(5))]);
3188 m.add_descriptor("d".into(), 20, 0);
3189 let (msgs, counts) = m.reconcile_to(&m.clone());
3190 assert!(msgs.is_empty(), "no churn when nothing changed");
3191 assert_eq!((counts.added, counts.removed), (0, 0));
3192 assert_eq!(counts.unchanged, 2);
3193 }
3194
3195 #[test]
3196 fn reconcile_categories_relaxation_emits_an_explicit_reset() {
3197 let mut live = WatchSetMirror::default();
3201 live.set_categories(crate::Categories::CHAIN);
3202 let target = WatchSetMirror::default(); let (msgs, _) = live.reconcile_to(&target);
3205 assert_eq!(
3206 msgs.iter()
3207 .filter_map(|m| match m {
3208 Msg::SetCategories(s) => Some(s.categories),
3209 _ => None,
3210 })
3211 .collect::<Vec<_>>(),
3212 vec![0],
3213 "relaxing a filter to all-categories emits SetCategories{{0}}",
3214 );
3215
3216 let none = WatchSetMirror::default();
3218 let mut chain = WatchSetMirror::default();
3219 chain.set_categories(crate::Categories::CHAIN);
3220 assert!(
3221 matches!(none.reconcile_to(&chain).0.first(), Some(Msg::SetCategories(s)) if s.categories == crate::Categories::CHAIN),
3222 "setting a filter from unfiltered emits it",
3223 );
3224
3225 let mut all = WatchSetMirror::default();
3227 all.set_categories(0);
3228 assert!(
3229 !none.reconcile_to(&all).0.iter().any(|m| matches!(m, Msg::SetCategories(_))),
3230 "None and Some(0) are the same effective filter — no reset emitted",
3231 );
3232 }
3233
3234 #[tokio::test]
3235 async fn reload_without_loader_errors() {
3236 let store = Arc::new(MemStore::default());
3237 let mut w = watch_with(&store);
3238 assert!(matches!(w.reload().await, Err(ReloadError::NoLoader)));
3239 }
3240
3241 #[tokio::test]
3242 async fn reload_surfaces_loader_error() {
3243 let mut w = ResilientWatch::new(
3244 StreamClient::for_test(),
3245 ResilientWatchConfig::new()
3246 .watch_set_loader(|_b: WatchSetBuilder| async move { Err("truth down".into()) }),
3247 );
3248 let err = w.reload().await.expect_err("loader failure surfaces");
3249 assert!(matches!(err, ReloadError::Loader(_)));
3250 assert!(err.to_string().contains("truth down"));
3251 }
3252
3253 #[tokio::test]
3254 async fn reload_while_disconnected_defers_but_updates_the_mirror() {
3255 let mut w = ResilientWatch::new(
3256 StreamClient::for_test(),
3257 ResilientWatchConfig::new().watch_set_loader(|b: WatchSetBuilder| async move {
3258 b.add_scripts([([5u8; 32], None)]);
3259 Ok(())
3260 }),
3261 );
3262 w.add_scripts([([1u8; 32], None)]).await.unwrap();
3264
3265 let summary = w.reload().await.expect("reload ok");
3266 assert!(!summary.applied, "no live stream → deferred to reconnect loader");
3267 assert_eq!(summary.added, 1, "sh5 added");
3268 assert_eq!(summary.removed, 1, "stale sh1 removed");
3269 assert!(w.mirror.scripts.contains_key(&[5u8; 32]));
3271 assert!(!w.mirror.scripts.contains_key(&[1u8; 32]));
3272 }
3273
3274 #[tokio::test]
3275 async fn reload_while_connected_sends_only_the_delta() {
3276 let mut w = ResilientWatch::new(
3277 StreamClient::for_test(),
3278 ResilientWatchConfig::new().watch_set_loader(|b: WatchSetBuilder| async move {
3279 b.add_scripts([([1u8; 32], None), ([3u8; 32], None)]);
3281 Ok(())
3282 }),
3283 );
3284 w.add_scripts([([1u8; 32], None), ([2u8; 32], None)]).await.unwrap();
3285
3286 let (handle, mut rx) = crate::client::WatchHandle::for_test();
3288 w.handle = Some(handle);
3289
3290 let summary = w.reload().await.expect("reload ok");
3291 assert!(summary.applied, "connected → applied live");
3292 assert_eq!((summary.added, summary.removed, summary.unchanged), (1, 1, 1));
3294
3295 let mut msgs = Vec::new();
3298 while let Ok(ctrl) = rx.try_recv() {
3299 msgs.push(ctrl.msg.unwrap());
3300 }
3301 assert_eq!(msgs.len(), 1, "reload sends exactly one control message");
3302 let Msg::SetWatchSet(sws) = &msgs[0] else {
3303 panic!("expected a SetWatchSet, got {:?}", msgs[0]);
3304 };
3305 let mut got: Vec<Vec<u8>> = sws.scripthashes.clone();
3306 got.sort();
3307 assert_eq!(
3308 got,
3309 vec![[1u8; 32].to_vec(), [3u8; 32].to_vec()],
3310 "the snapshot is the full desired set, not a delta",
3311 );
3312 }
3313
3314 #[tokio::test]
3315 async fn reload_turns_off_raw_tx_opt_in_when_truth_drops_it() {
3316 let mut w = ResilientWatch::new(
3323 StreamClient::for_test(),
3324 ResilientWatchConfig::new().watch_set_loader(|b: WatchSetBuilder| async move {
3325 b.add_scripts([([1u8; 32], None)]);
3326 Ok(())
3327 }),
3328 );
3329 w.add_scripts([([1u8; 32], None)]).await.unwrap();
3330 w.set_watch_options(true).await.unwrap(); assert_eq!(w.mirror.include_raw_tx, Some(true));
3332
3333 let (handle, mut rx) = crate::client::WatchHandle::for_test();
3334 w.handle = Some(handle);
3335
3336 w.reload().await.expect("reload ok");
3337
3338 let mut msgs = Vec::new();
3339 while let Ok(ctrl) = rx.try_recv() {
3340 msgs.push(ctrl.msg.unwrap());
3341 }
3342 let opt = msgs.iter().find_map(|m| match m {
3344 Msg::SetWatchOptions(o) => Some(o.include_raw_tx),
3345 _ => None,
3346 });
3347 assert_eq!(opt, Some(false), "reload turns the opt-in OFF to match truth");
3348 assert_eq!(w.mirror.include_raw_tx, None);
3350 }
3351
3352 #[tokio::test]
3353 async fn reload_does_not_touch_raw_tx_opt_in_when_unchanged() {
3354 let mut w = ResilientWatch::new(
3357 StreamClient::for_test(),
3358 ResilientWatchConfig::new().watch_set_loader(|b: WatchSetBuilder| async move {
3359 b.add_scripts([([1u8; 32], None)]);
3360 Ok(())
3361 }),
3362 );
3363 w.add_scripts([([1u8; 32], None)]).await.unwrap();
3364
3365 let (handle, mut rx) = crate::client::WatchHandle::for_test();
3366 w.handle = Some(handle);
3367
3368 w.reload().await.expect("reload ok");
3369
3370 let mut msgs = Vec::new();
3371 while let Ok(ctrl) = rx.try_recv() {
3372 msgs.push(ctrl.msg.unwrap());
3373 }
3374 assert!(
3375 !msgs.iter().any(|m| matches!(m, Msg::SetWatchOptions(_))),
3376 "no opt-in change → no SetWatchOptions churn, got {msgs:?}",
3377 );
3378 }
3379}