1use std::time::{Duration, SystemTime, UNIX_EPOCH};
9
10use async_trait::async_trait;
11use bsv::transaction::chain_tracker::ChainTracker;
12use bsv::transaction::Beef;
13use chrono::Utc;
14use tokio::sync::Mutex;
15
16use crate::error::{WalletError, WalletResult};
17use crate::types::Chain;
18
19use super::chaintracker::{ChaintracksChainTracker, ChaintracksServiceClient};
20use super::providers::exchange_rates::{fetch_bsv_exchange_rate, fetch_fiat_exchange_rates};
21use super::providers::{ArcProvider, Bitails, WhatsOnChain};
22use super::service_collection::ServiceCollection;
23use super::traits::{
24 GetMerklePathProvider, GetRawTxProvider, GetScriptHashHistoryProvider,
25 GetStatusForTxidsProvider, GetUtxoStatusProvider, PostBeefProvider, WalletServices,
26};
27use super::types::{
28 BlockHeader, BsvExchangeRate, FiatExchangeRates, GetMerklePathResult, GetRawTxResult,
29 GetScriptHashHistoryResult, GetStatusForTxidsResult, GetUtxoStatusOutputFormat,
30 GetUtxoStatusResult, NLockTimeInput, PostBeefMode, PostBeefResult, ServiceCall,
31 ServicesCallHistory, ServicesConfig,
32};
33use bsv::transaction::beef::BEEF_V1;
34
35pub struct Services {
40 config: ServicesConfig,
41 client: reqwest::Client,
42 get_merkle_path: Mutex<ServiceCollection<dyn GetMerklePathProvider>>,
43 get_raw_tx: Mutex<ServiceCollection<dyn GetRawTxProvider>>,
44 post_beef: Mutex<ServiceCollection<dyn PostBeefProvider>>,
45 get_utxo_status: Mutex<ServiceCollection<dyn GetUtxoStatusProvider>>,
46 get_status_for_txids: Mutex<ServiceCollection<dyn GetStatusForTxidsProvider>>,
47 get_script_hash_history: Mutex<ServiceCollection<dyn GetScriptHashHistoryProvider>>,
48 chain_tracker: ChaintracksChainTracker,
49 post_beef_mode: PostBeefMode,
50 bsv_exchange_rate: Mutex<BsvExchangeRate>,
51 fiat_exchange_rates: Mutex<FiatExchangeRates>,
52}
53
54impl Services {
55 pub fn from_config(config: ServicesConfig) -> Self {
57 let client = reqwest::Client::new();
58 let chain = config.chain.clone();
59
60 let has_bitails = matches!(chain, Chain::Main | Chain::Test);
61
62 let mut get_merkle_path_coll =
64 ServiceCollection::<dyn GetMerklePathProvider>::new("getMerklePath");
65 let woc_merkle = WhatsOnChain::new(
67 chain.clone(),
68 config.whats_on_chain_api_key.clone(),
69 client.clone(),
70 );
71 get_merkle_path_coll.add("WhatsOnChain", Box::new(woc_merkle));
72 if has_bitails {
73 let bitails = Bitails::new(
74 chain.clone(),
75 config.bitails_api_key.clone(),
76 client.clone(),
77 );
78 get_merkle_path_coll.add("Bitails", Box::new(bitails));
79 }
80
81 let mut get_raw_tx_coll = ServiceCollection::<dyn GetRawTxProvider>::new("getRawTx");
83 let woc_raw = WhatsOnChain::new(
84 chain.clone(),
85 config.whats_on_chain_api_key.clone(),
86 client.clone(),
87 );
88 get_raw_tx_coll.add("WhatsOnChain", Box::new(woc_raw));
89
90 let mut post_beef_coll = ServiceCollection::<dyn PostBeefProvider>::new("postBeef");
93
94 if let Some(ref gp_url) = config.arc_gorilla_pool_url {
95 let gp_config = config.arc_gorilla_pool_config.clone().unwrap_or_default();
96 let arc_gp = ArcProvider::new("GorillaPoolArcBeef", gp_url, gp_config, client.clone());
97 post_beef_coll.add("GorillaPoolArcBeef", Box::new(arc_gp));
98 }
99
100 let arc_taal = ArcProvider::new(
101 "TaalArcBeef",
102 &config.arc_url,
103 config.arc_config.clone(),
104 client.clone(),
105 );
106 post_beef_coll.add("TaalArcBeef", Box::new(arc_taal));
107
108 if has_bitails {
109 let bitails_beef = Bitails::new(
110 chain.clone(),
111 config.bitails_api_key.clone(),
112 client.clone(),
113 );
114 post_beef_coll.add("Bitails", Box::new(bitails_beef));
115 }
116
117 let woc_beef = WhatsOnChain::new(
118 chain.clone(),
119 config.whats_on_chain_api_key.clone(),
120 client.clone(),
121 );
122 post_beef_coll.add("WhatsOnChain", Box::new(woc_beef));
123
124 let mut get_utxo_status_coll =
126 ServiceCollection::<dyn GetUtxoStatusProvider>::new("getUtxoStatus");
127 let woc_utxo = WhatsOnChain::new(
128 chain.clone(),
129 config.whats_on_chain_api_key.clone(),
130 client.clone(),
131 );
132 get_utxo_status_coll.add("WhatsOnChain", Box::new(woc_utxo));
133
134 let mut get_status_for_txids_coll =
136 ServiceCollection::<dyn GetStatusForTxidsProvider>::new("getStatusForTxids");
137 let woc_status = WhatsOnChain::new(
138 chain.clone(),
139 config.whats_on_chain_api_key.clone(),
140 client.clone(),
141 );
142 get_status_for_txids_coll.add("WhatsOnChain", Box::new(woc_status));
143
144 let mut get_script_hash_history_coll =
146 ServiceCollection::<dyn GetScriptHashHistoryProvider>::new("getScriptHashHistory");
147 let woc_history = WhatsOnChain::new(
148 chain.clone(),
149 config.whats_on_chain_api_key.clone(),
150 client.clone(),
151 );
152 get_script_hash_history_coll.add("WhatsOnChain", Box::new(woc_history));
153
154 let chaintracks_url = config.chaintracks_url.as_deref();
156 let service_client =
157 ChaintracksServiceClient::new(chain.clone(), chaintracks_url, client.clone());
158 let chain_tracker = ChaintracksChainTracker::new(service_client);
159
160 Services {
161 bsv_exchange_rate: Mutex::new(config.bsv_exchange_rate.clone()),
162 fiat_exchange_rates: Mutex::new(config.fiat_exchange_rates.clone()),
163 config,
164 client,
165 get_merkle_path: Mutex::new(get_merkle_path_coll),
166 get_raw_tx: Mutex::new(get_raw_tx_coll),
167 post_beef: Mutex::new(post_beef_coll),
168 get_utxo_status: Mutex::new(get_utxo_status_coll),
169 get_status_for_txids: Mutex::new(get_status_for_txids_coll),
170 get_script_hash_history: Mutex::new(get_script_hash_history_coll),
171 chain_tracker,
172 post_beef_mode: PostBeefMode::UntilSuccess,
173 }
174 }
175
176 pub fn from_chain(chain: Chain) -> Self {
178 let config = ServicesConfig::from(chain);
179 Self::from_config(config)
180 }
181
182 pub fn config(&self) -> &ServicesConfig {
184 &self.config
185 }
186
187 pub fn client(&self) -> &reqwest::Client {
189 &self.client
190 }
191
192 pub fn set_post_beef_mode(&mut self, mode: PostBeefMode) {
194 self.post_beef_mode = mode;
195 }
196}
197
198impl From<Chain> for Services {
199 fn from(chain: Chain) -> Self {
200 Services::from_chain(chain)
201 }
202}
203
204#[async_trait]
209impl WalletServices for Services {
210 fn chain(&self) -> Chain {
211 self.config.chain.clone()
212 }
213
214 async fn get_chain_tracker(&self) -> WalletResult<Box<dyn ChainTracker>> {
215 let chaintracks_url = self.config.chaintracks_url.as_deref();
216 let service_client = ChaintracksServiceClient::new(
217 self.config.chain.clone(),
218 chaintracks_url,
219 self.client.clone(),
220 );
221 Ok(Box::new(ChaintracksChainTracker::new(service_client)))
222 }
223
224 async fn get_merkle_path(&self, txid: &str, use_next: bool) -> GetMerklePathResult {
225 let mut coll = self.get_merkle_path.lock().await;
226 if use_next {
227 coll.next();
228 }
229
230 let count = coll.len();
231 let mut r0 = GetMerklePathResult::default();
232
233 for _tries in 0..count {
234 let (provider, provider_name) = match coll.service_to_call() {
235 Some(p) => p,
236 None => break,
237 };
238 let provider_name = provider_name.to_string();
239
240 let start = Utc::now();
241 let result = provider.get_merkle_path(txid, self).await;
242 let elapsed = Utc::now().signed_duration_since(start).num_milliseconds();
243
244 if r0.name.is_none() {
245 r0.name = result.name.clone();
246 }
247
248 if result.merkle_path.is_some() {
249 let call = ServiceCall {
250 when: start,
251 msecs: elapsed,
252 success: true,
253 result: None,
254 error: None,
255 };
256 coll.add_service_call_success(&provider_name, call, None);
257 return result;
258 }
259
260 if let Some(ref err_str) = result.error {
261 let call = ServiceCall {
262 when: start,
263 msecs: elapsed,
264 success: false,
265 result: None,
266 error: None,
267 };
268 let err = WalletError::Internal(err_str.clone());
269 coll.add_service_call_error(&provider_name, call, &err);
270 if r0.error.is_none() {
271 r0.error = result.error.clone();
272 }
273 } else {
274 let call = ServiceCall {
275 when: start,
276 msecs: elapsed,
277 success: false,
278 result: None,
279 error: None,
280 };
281 coll.add_service_call_failure(&provider_name, call);
282 }
283
284 coll.next();
285 }
286
287 r0
288 }
289
290 async fn get_raw_tx(&self, txid: &str, use_next: bool) -> GetRawTxResult {
291 let mut coll = self.get_raw_tx.lock().await;
292 if use_next {
293 coll.next();
294 }
295
296 let count = coll.len();
297 let mut r0 = GetRawTxResult {
298 txid: txid.to_string(),
299 ..Default::default()
300 };
301
302 for _tries in 0..count {
303 let (provider, provider_name) = match coll.service_to_call() {
304 Some(p) => p,
305 None => break,
306 };
307 let provider_name = provider_name.to_string();
308
309 let start = Utc::now();
310 let result = provider.get_raw_tx(txid).await;
311 let elapsed = Utc::now().signed_duration_since(start).num_milliseconds();
312
313 if result.raw_tx.is_some() && result.error.is_none() {
314 let call = ServiceCall {
315 when: start,
316 msecs: elapsed,
317 success: true,
318 result: None,
319 error: None,
320 };
321 coll.add_service_call_success(&provider_name, call, None);
322 return result;
323 }
324
325 if let Some(ref err_str) = result.error {
326 let call = ServiceCall {
327 when: start,
328 msecs: elapsed,
329 success: false,
330 result: None,
331 error: None,
332 };
333 let err = WalletError::Internal(err_str.clone());
334 coll.add_service_call_error(&provider_name, call, &err);
335 if r0.error.is_none() {
336 r0.error = result.error.clone();
337 }
338 } else if result.raw_tx.is_none() {
339 let call = ServiceCall {
341 when: start,
342 msecs: elapsed,
343 success: true,
344 result: Some("not found".to_string()),
345 error: None,
346 };
347 coll.add_service_call_success(&provider_name, call, Some("not found".to_string()));
348 } else {
349 let call = ServiceCall {
350 when: start,
351 msecs: elapsed,
352 success: false,
353 result: None,
354 error: None,
355 };
356 coll.add_service_call_failure(&provider_name, call);
357 }
358
359 coll.next();
360 }
361
362 r0
363 }
364
365 async fn post_beef(&self, beef: &[u8], txids: &[String]) -> Vec<PostBeefResult> {
366 self.post_beef_impl(beef, txids).await
368 }
369
370 async fn get_utxo_status(
371 &self,
372 output: &str,
373 output_format: Option<GetUtxoStatusOutputFormat>,
374 outpoint: Option<&str>,
375 use_next: bool,
376 ) -> GetUtxoStatusResult {
377 let mut coll = self.get_utxo_status.lock().await;
378 if use_next {
379 coll.next();
380 }
381
382 let count = coll.len();
383 let mut r0 = GetUtxoStatusResult {
384 name: "<noservices>".to_string(),
385 status: "error".to_string(),
386 error: Some("WERR_INTERNAL: No services available.".to_string()),
387 is_utxo: None,
388 details: Vec::new(),
389 };
390
391 for _retry in 0..2u32 {
393 for _tries in 0..count {
394 let (provider, provider_name) = match coll.service_to_call() {
395 Some(p) => p,
396 None => break,
397 };
398 let provider_name = provider_name.to_string();
399
400 let start = Utc::now();
401 let result = provider
402 .get_utxo_status(output, output_format.clone(), outpoint)
403 .await;
404 let elapsed = Utc::now().signed_duration_since(start).num_milliseconds();
405
406 if result.status == "success" {
407 let call = ServiceCall {
408 when: start,
409 msecs: elapsed,
410 success: true,
411 result: None,
412 error: None,
413 };
414 coll.add_service_call_success(&provider_name, call, None);
415 return result;
416 }
417
418 if let Some(ref err_str) = result.error {
419 let call = ServiceCall {
420 when: start,
421 msecs: elapsed,
422 success: false,
423 result: None,
424 error: None,
425 };
426 let err = WalletError::Internal(err_str.clone());
427 coll.add_service_call_error(&provider_name, call, &err);
428 } else {
429 let call = ServiceCall {
430 when: start,
431 msecs: elapsed,
432 success: false,
433 result: None,
434 error: None,
435 };
436 coll.add_service_call_failure(&provider_name, call);
437 }
438
439 r0 = result;
440 coll.next();
441 }
442
443 if r0.status == "success" {
444 break;
445 }
446 tokio::time::sleep(Duration::from_millis(2000)).await;
447 }
448
449 r0
450 }
451
452 async fn get_status_for_txids(
453 &self,
454 txids: &[String],
455 use_next: bool,
456 ) -> GetStatusForTxidsResult {
457 let mut coll = self.get_status_for_txids.lock().await;
458 if use_next {
459 coll.next();
460 }
461
462 let count = coll.len();
463 let mut r0 = GetStatusForTxidsResult {
464 name: "<noservices>".to_string(),
465 status: "error".to_string(),
466 error: Some("WERR_INTERNAL: No services available.".to_string()),
467 results: Vec::new(),
468 };
469
470 for _tries in 0..count {
471 let (provider, provider_name) = match coll.service_to_call() {
472 Some(p) => p,
473 None => break,
474 };
475 let provider_name = provider_name.to_string();
476
477 let start = Utc::now();
478 let result = provider.get_status_for_txids(txids).await;
479 let elapsed = Utc::now().signed_duration_since(start).num_milliseconds();
480
481 if result.status == "success" {
482 let call = ServiceCall {
483 when: start,
484 msecs: elapsed,
485 success: true,
486 result: None,
487 error: None,
488 };
489 coll.add_service_call_success(&provider_name, call, None);
490 return result;
491 }
492
493 if let Some(ref err_str) = result.error {
494 let call = ServiceCall {
495 when: start,
496 msecs: elapsed,
497 success: false,
498 result: None,
499 error: None,
500 };
501 let err = WalletError::Internal(err_str.clone());
502 coll.add_service_call_error(&provider_name, call, &err);
503 } else {
504 let call = ServiceCall {
505 when: start,
506 msecs: elapsed,
507 success: false,
508 result: None,
509 error: None,
510 };
511 coll.add_service_call_failure(&provider_name, call);
512 }
513
514 r0 = result;
515 coll.next();
516 }
517
518 r0
519 }
520
521 async fn get_script_hash_history(
522 &self,
523 hash: &str,
524 use_next: bool,
525 ) -> GetScriptHashHistoryResult {
526 let mut coll = self.get_script_hash_history.lock().await;
527 if use_next {
528 coll.next();
529 }
530
531 let count = coll.len();
532 let mut r0 = GetScriptHashHistoryResult {
533 name: "<noservices>".to_string(),
534 status: "error".to_string(),
535 error: Some("WERR_INTERNAL: No services available.".to_string()),
536 history: Vec::new(),
537 };
538
539 for _tries in 0..count {
540 let (provider, provider_name) = match coll.service_to_call() {
541 Some(p) => p,
542 None => break,
543 };
544 let provider_name = provider_name.to_string();
545
546 let start = Utc::now();
547 let result = provider.get_script_hash_history(hash).await;
548 let elapsed = Utc::now().signed_duration_since(start).num_milliseconds();
549
550 if result.status == "success" {
551 let call = ServiceCall {
552 when: start,
553 msecs: elapsed,
554 success: true,
555 result: None,
556 error: None,
557 };
558 coll.add_service_call_success(&provider_name, call, None);
559 return result;
560 }
561
562 if let Some(ref err_str) = result.error {
563 let call = ServiceCall {
564 when: start,
565 msecs: elapsed,
566 success: false,
567 result: None,
568 error: None,
569 };
570 let err = WalletError::Internal(err_str.clone());
571 coll.add_service_call_error(&provider_name, call, &err);
572 } else {
573 let call = ServiceCall {
574 when: start,
575 msecs: elapsed,
576 success: false,
577 result: None,
578 error: None,
579 };
580 coll.add_service_call_failure(&provider_name, call);
581 }
582
583 r0 = result;
584 coll.next();
585 }
586
587 r0
588 }
589
590 async fn hash_to_header(&self, hash: &str) -> WalletResult<BlockHeader> {
591 self.chain_tracker.hash_to_header(hash).await
592 }
593
594 async fn get_header_for_height(&self, height: u32) -> WalletResult<Vec<u8>> {
595 let header = self.chain_tracker.get_header_for_height(height).await?;
596 Ok(serialize_block_header(&header))
597 }
598
599 async fn get_height(&self) -> WalletResult<u32> {
600 use bsv::transaction::chain_tracker::ChainTracker as _;
601 self.chain_tracker
602 .current_height()
603 .await
604 .map_err(|e| WalletError::Internal(format!("ChainTracker error: {}", e)))
605 }
606
607 async fn n_lock_time_is_final(&self, input: NLockTimeInput) -> WalletResult<bool> {
608 const MAXINT: u32 = 0xFFFF_FFFF;
609 const BLOCK_LIMIT: u32 = 500_000_000;
610
611 let n_lock_time = match input {
612 NLockTimeInput::Raw(locktime) => locktime,
613 NLockTimeInput::Transaction(tx) => {
614 if tx.inputs.iter().all(|i| i.sequence == MAXINT) {
616 return Ok(true);
617 }
618 tx.lock_time
619 }
620 };
621
622 if n_lock_time == 0 {
623 return Ok(true);
624 }
625
626 if n_lock_time >= BLOCK_LIMIT {
627 let now_secs = SystemTime::now()
629 .duration_since(UNIX_EPOCH)
630 .unwrap_or_default()
631 .as_secs() as u32;
632 return Ok(n_lock_time < now_secs);
633 }
634
635 let height = self.get_height().await?;
637 Ok(n_lock_time < height)
638 }
639
640 async fn get_bsv_exchange_rate(&self) -> WalletResult<BsvExchangeRate> {
641 self.get_bsv_exchange_rate_impl().await
642 }
643
644 async fn get_fiat_exchange_rate(
645 &self,
646 currency: &str,
647 base: Option<&str>,
648 ) -> WalletResult<f64> {
649 self.get_fiat_exchange_rate_impl(currency, base).await
650 }
651
652 async fn get_fiat_exchange_rates(
653 &self,
654 target_currencies: &[String],
655 ) -> WalletResult<FiatExchangeRates> {
656 self.get_fiat_exchange_rates_impl(target_currencies).await
657 }
658
659 fn get_services_call_history(&self, _reset: bool) -> ServicesCallHistory {
660 let mut services = Vec::new();
663
664 if let Ok(mut coll) = self.get_merkle_path.try_lock() {
665 services.push(coll.get_service_call_history(_reset));
666 }
667 if let Ok(mut coll) = self.get_raw_tx.try_lock() {
668 services.push(coll.get_service_call_history(_reset));
669 }
670 if let Ok(mut coll) = self.post_beef.try_lock() {
671 services.push(coll.get_service_call_history(_reset));
672 }
673 if let Ok(mut coll) = self.get_utxo_status.try_lock() {
674 services.push(coll.get_service_call_history(_reset));
675 }
676 if let Ok(mut coll) = self.get_status_for_txids.try_lock() {
677 services.push(coll.get_service_call_history(_reset));
678 }
679 if let Ok(mut coll) = self.get_script_hash_history.try_lock() {
680 services.push(coll.get_service_call_history(_reset));
681 }
682
683 ServicesCallHistory { services }
684 }
685
686 async fn get_beef_for_txid(&self, txid: &str) -> WalletResult<Beef> {
687 let raw_result = self.get_raw_tx(txid, false).await;
688 let raw_tx = raw_result.raw_tx.ok_or_else(|| {
689 WalletError::Internal(format!(
690 "Could not retrieve raw transaction for txid {}",
691 txid
692 ))
693 })?;
694
695 let tx = bsv::transaction::Transaction::from_binary(&mut std::io::Cursor::new(&raw_tx))
697 .map_err(|e| {
698 WalletError::Internal(format!("Failed to parse transaction {}: {}", txid, e))
699 })?;
700
701 let beef_tx = bsv::transaction::beef_tx::BeefTx {
703 txid: txid.to_string(),
704 tx: Some(tx),
705 bump_index: None,
706 input_txids: Vec::new(),
707 };
708 let mut beef = Beef::new(BEEF_V1);
709 beef.txs.push(beef_tx);
710
711 Ok(beef)
712 }
713
714 fn hash_output_script(&self, script: &[u8]) -> String {
715 let hash = bsv::primitives::hash::sha256(script);
717 let mut bytes = hash.to_vec();
718 bytes.reverse();
719 let mut hex = String::with_capacity(bytes.len() * 2);
720 for b in &bytes {
721 hex.push_str(&format!("{:02x}", b));
722 }
723 hex
724 }
725
726 async fn is_utxo(&self, locking_script: &[u8], txid: &str, vout: u32) -> WalletResult<bool> {
727 let hash = self.hash_output_script(locking_script);
728 let outpoint = format!("{}.{}", txid, vout);
729 let result = self
730 .get_utxo_status(&hash, None, Some(&outpoint), false)
731 .await;
732 Ok(result.is_utxo == Some(true))
733 }
734}
735
736impl Services {
741 async fn post_beef_impl(&self, beef: &[u8], txids: &[String]) -> Vec<PostBeefResult> {
743 let soft_timeout_ms = self.config.get_post_beef_soft_timeout_ms(beef.len());
744
745 match self.post_beef_mode {
746 PostBeefMode::UntilSuccess => {
747 self.post_beef_until_success(beef, txids, soft_timeout_ms)
748 .await
749 }
750 PostBeefMode::PromiseAll => self.post_beef_promise_all(beef, txids).await,
751 }
752 }
753
754 async fn post_beef_until_success(
756 &self,
757 beef: &[u8],
758 txids: &[String],
759 soft_timeout_ms: u64,
760 ) -> Vec<PostBeefResult> {
761 let mut results: Vec<PostBeefResult> = Vec::new();
762
763 let provider_names: Vec<String> = {
765 let coll = self.post_beef.lock().await;
766 coll.all_services()
767 .map(|(_, name)| name.to_string())
768 .collect()
769 };
770
771 for _provider_name in &provider_names {
772 let (prov_name, prov_ref_result) = {
774 let coll = self.post_beef.lock().await;
775 match coll.service_to_call() {
776 Some((_provider, name)) => {
777 let name = name.to_string();
778 (name, true)
779 }
780 None => (String::new(), false),
781 }
782 };
783
784 if !prov_ref_result {
785 break;
786 }
787
788 let start = Utc::now();
790 let result = {
791 let coll = self.post_beef.lock().await;
792 match coll.service_to_call() {
793 Some((provider, _)) => {
794 let beef_owned = beef.to_vec();
795 let txids_owned = txids.to_vec();
796 if soft_timeout_ms > 0 {
801 match tokio::time::timeout(
802 Duration::from_millis(soft_timeout_ms),
803 provider.post_beef(&beef_owned, &txids_owned),
804 )
805 .await
806 {
807 Ok(r) => r,
808 Err(_) => {
809 PostBeefResult::timeout(&prov_name, txids, soft_timeout_ms)
810 }
811 }
812 } else {
813 provider.post_beef(&beef_owned, &txids_owned).await
814 }
815 }
816 None => break,
817 }
818 };
819 let elapsed = Utc::now().signed_duration_since(start).num_milliseconds();
820
821 let is_success = result.status == "success";
822 let is_timeout = result
823 .error
824 .as_ref()
825 .map(|e| e.contains("timeout"))
826 .unwrap_or(false);
827
828 {
830 let mut coll = self.post_beef.lock().await;
831 let call = ServiceCall {
832 when: start,
833 msecs: elapsed,
834 success: is_success,
835 result: None,
836 error: None,
837 };
838 if is_success {
839 coll.add_service_call_success(&prov_name, call, None);
840 } else if let Some(ref err_str) = result.error {
841 let err = WalletError::Internal(err_str.clone());
842 coll.add_service_call_error(&prov_name, call, &err);
843 } else {
844 coll.add_service_call_failure(&prov_name, call);
845 }
846 }
847
848 results.push(result);
849
850 if is_success {
851 break;
852 }
853
854 if !is_timeout {
856 let mut coll = self.post_beef.lock().await;
857 let all_service_error = results
858 .last()
859 .map(|r| {
860 r.txid_results
861 .iter()
862 .all(|tr| tr.service_error == Some(true))
863 })
864 .unwrap_or(false);
865 if all_service_error {
866 coll.move_service_to_last(&prov_name);
867 }
868 }
869
870 {
872 let mut coll = self.post_beef.lock().await;
873 coll.next();
874 }
875 }
876
877 if results.is_empty() {
878 vec![PostBeefResult {
879 name: "<noservices>".to_string(),
880 status: "error".to_string(),
881 error: Some("No postBeef services available".to_string()),
882 txid_results: Vec::new(),
883 }]
884 } else {
885 results
886 }
887 }
888
889 async fn post_beef_promise_all(&self, beef: &[u8], txids: &[String]) -> Vec<PostBeefResult> {
891 let provider_count = {
893 let coll = self.post_beef.lock().await;
894 coll.len()
895 };
896
897 if provider_count == 0 {
898 return vec![PostBeefResult {
899 name: "<noservices>".to_string(),
900 status: "error".to_string(),
901 error: Some("No postBeef services available".to_string()),
902 txid_results: Vec::new(),
903 }];
904 }
905
906 let beef_bytes = beef.to_vec();
910 let txids_vec = txids.to_vec();
911
912 let mut results = Vec::new();
922 {
923 let coll = self.post_beef.lock().await;
924 let providers: Vec<(&dyn PostBeefProvider, String)> = coll
925 .all_services()
926 .map(|(p, name)| (p, name.to_string()))
927 .collect();
928
929 for (provider, name) in &providers {
934 let start = Utc::now();
935 let result = provider.post_beef(&beef_bytes, &txids_vec).await;
936 let elapsed = Utc::now().signed_duration_since(start).num_milliseconds();
937 results.push((name.clone(), start, elapsed, result));
938 }
939 }
940
941 {
943 let mut coll = self.post_beef.lock().await;
944 for (name, start, elapsed, ref result) in &results {
945 let call = ServiceCall {
946 when: *start,
947 msecs: *elapsed,
948 success: result.status == "success",
949 result: None,
950 error: None,
951 };
952 if result.status == "success" {
953 coll.add_service_call_success(name, call, None);
954 } else if let Some(ref err_str) = result.error {
955 let err = WalletError::Internal(err_str.clone());
956 coll.add_service_call_error(name, call, &err);
957 } else {
958 coll.add_service_call_failure(name, call);
959 }
960 }
961 }
962
963 results
964 .into_iter()
965 .map(|(_, _, _, result)| result)
966 .collect()
967 }
968
969 async fn get_bsv_exchange_rate_impl(&self) -> WalletResult<BsvExchangeRate> {
973 let update_ms = self.config.bsv_update_msecs;
974
975 {
976 let cached = self.bsv_exchange_rate.lock().await;
977 let age_ms = Utc::now()
978 .signed_duration_since(cached.timestamp)
979 .num_milliseconds() as u64;
980 if cached.rate_usd > 0.0 && age_ms < update_ms {
981 return Ok(cached.clone());
982 }
983 }
984
985 let rate = fetch_bsv_exchange_rate(&self.client).await?;
987 let new_rate = BsvExchangeRate {
988 timestamp: Utc::now(),
989 rate_usd: rate,
990 };
991
992 let mut cached = self.bsv_exchange_rate.lock().await;
993 *cached = new_rate.clone();
994 Ok(new_rate)
995 }
996
997 async fn get_fiat_exchange_rate_impl(
999 &self,
1000 currency: &str,
1001 base: Option<&str>,
1002 ) -> WalletResult<f64> {
1003 let base = base.unwrap_or("USD");
1004 if currency == base {
1005 return Ok(1.0);
1006 }
1007
1008 let required: Vec<String> = if base == "USD" {
1010 vec![currency.to_string()]
1011 } else {
1012 vec![currency.to_string(), base.to_string()]
1013 };
1014
1015 self.update_fiat_exchange_rates(&required).await?;
1017
1018 let cached = self.fiat_exchange_rates.lock().await;
1019 let c = cached
1020 .rates
1021 .get(currency)
1022 .ok_or_else(|| WalletError::InvalidParameter {
1023 parameter: "currency".to_string(),
1024 must_be: format!("valid fiat currency '{}' with an exchange rate", currency),
1025 })?;
1026 let b = cached
1027 .rates
1028 .get(base)
1029 .ok_or_else(|| WalletError::InvalidParameter {
1030 parameter: "base".to_string(),
1031 must_be: format!("valid fiat currency '{}' with an exchange rate", base),
1032 })?;
1033
1034 Ok(c / b)
1035 }
1036
1037 async fn get_fiat_exchange_rates_impl(
1039 &self,
1040 target_currencies: &[String],
1041 ) -> WalletResult<FiatExchangeRates> {
1042 self.update_fiat_exchange_rates(target_currencies).await?;
1043
1044 let cached = self.fiat_exchange_rates.lock().await;
1045 let mut rates = std::collections::HashMap::new();
1046 for c in target_currencies {
1047 if let Some(v) = cached.rates.get(c.as_str()) {
1048 rates.insert(c.clone(), *v);
1049 }
1050 }
1051
1052 Ok(FiatExchangeRates {
1053 timestamp: cached.timestamp,
1054 base: "USD".to_string(),
1055 rates,
1056 })
1057 }
1058
1059 async fn update_fiat_exchange_rates(&self, target_currencies: &[String]) -> WalletResult<()> {
1061 let update_ms = self.config.fiat_update_msecs;
1062 let freshness_cutoff = Utc::now() - chrono::Duration::milliseconds(update_ms as i64);
1063
1064 let to_fetch: Vec<String> = {
1065 let cached = self.fiat_exchange_rates.lock().await;
1066 target_currencies
1067 .iter()
1068 .filter(|c| {
1069 if c.as_str() == "USD" {
1070 return false; }
1072 match cached.rates.get(c.as_str()) {
1073 Some(_) if cached.timestamp > freshness_cutoff => false,
1074 _ => true,
1075 }
1076 })
1077 .cloned()
1078 .collect()
1079 };
1080
1081 if to_fetch.is_empty() {
1082 let mut cached = self.fiat_exchange_rates.lock().await;
1084 cached.rates.entry("USD".to_string()).or_insert(1.0);
1085 return Ok(());
1086 }
1087
1088 let fetched = fetch_fiat_exchange_rates(
1090 &self.client,
1091 self.config.exchangeratesapi_key.as_deref(),
1092 "USD",
1093 &to_fetch,
1094 )
1095 .await?;
1096
1097 let mut cached = self.fiat_exchange_rates.lock().await;
1099 for (currency, rate) in &fetched.rates {
1100 cached.rates.insert(currency.clone(), *rate);
1101 }
1102 cached.rates.entry("USD".to_string()).or_insert(1.0);
1103 if fetched.timestamp > cached.timestamp {
1104 cached.timestamp = fetched.timestamp;
1105 }
1106
1107 Ok(())
1108 }
1109}
1110
1111fn serialize_block_header(header: &BlockHeader) -> Vec<u8> {
1120 let mut buf = Vec::with_capacity(80);
1121
1122 buf.extend_from_slice(&header.version.to_le_bytes());
1124
1125 if let Ok(bytes) = hex_to_bytes_reversed(&header.previous_hash) {
1127 buf.extend_from_slice(&bytes);
1128 } else {
1129 buf.extend_from_slice(&[0u8; 32]);
1130 }
1131
1132 if let Ok(bytes) = hex_to_bytes_reversed(&header.merkle_root) {
1134 buf.extend_from_slice(&bytes);
1135 } else {
1136 buf.extend_from_slice(&[0u8; 32]);
1137 }
1138
1139 buf.extend_from_slice(&header.time.to_le_bytes());
1141
1142 buf.extend_from_slice(&header.bits.to_le_bytes());
1144
1145 buf.extend_from_slice(&header.nonce.to_le_bytes());
1147
1148 buf
1149}
1150
1151fn hex_to_bytes_reversed(hex: &str) -> Result<Vec<u8>, WalletError> {
1154 if hex.len() % 2 != 0 {
1155 return Err(WalletError::InvalidParameter {
1156 parameter: "hex".to_string(),
1157 must_be: "an even-length hex string".to_string(),
1158 });
1159 }
1160 let mut bytes: Vec<u8> = (0..hex.len())
1161 .step_by(2)
1162 .map(|i| {
1163 u8::from_str_radix(&hex[i..i + 2], 16).map_err(|_| WalletError::InvalidParameter {
1164 parameter: "hex".to_string(),
1165 must_be: "valid hex characters".to_string(),
1166 })
1167 })
1168 .collect::<Result<Vec<u8>, _>>()?;
1169 bytes.reverse();
1170 Ok(bytes)
1171}