amp_rs/
client.rs

1use std::env;
2use std::sync::Arc;
3use std::time::Duration as StdDuration;
4
5use async_trait::async_trait;
6use chrono::{Duration, Utc};
7
8use reqwest::header::AUTHORIZATION;
9use reqwest::{Client, Method, Url};
10use serde::de::DeserializeOwned;
11use thiserror::Error;
12use tokio::sync::{Mutex, OnceCell, Semaphore};
13use tokio::time::sleep;
14
15use elements::encode::Decodable;
16use secrecy::ExposeSecret;
17use secrecy::Secret;
18use std::str::FromStr;
19
20use crate::model::{
21    Activity, Asset, AssetActivityParams, AssetDistributionAssignment, AssetSummary, Assignment,
22    Balance, BroadcastResponse, CategoriesRequest, CategoryAdd, CategoryEdit, CategoryResponse,
23    ChangePasswordRequest, ChangePasswordResponse, CreateAssetAssignmentRequest, EditAssetRequest,
24    GaidBalanceEntry, GaidRequest, IssuanceRequest, IssuanceResponse, Outpoint, Ownership,
25    Password, TokenData, TokenInfo, TokenRequest, TokenResponse, TransactionDetail, TxInput,
26    Unspent, Utxo,
27};
28use crate::signer::{Signer, SignerError};
29
30/// Environment variables used for token environment detection
31#[derive(Debug)]
32struct EnvironmentVariables {
33    username: String,
34    password: String,
35    amp_tests: String,
36    base_url: String,
37}
38
39/// Token environment detection for automatic strategy selection
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub enum TokenEnvironment {
42    /// Mock environment - use isolated token management without persistence
43    Mock,
44    /// Live environment - use full token management with persistence
45    Live,
46    /// Auto-detect environment based on credentials and settings
47    Auto,
48}
49
50impl TokenEnvironment {
51    /// Detects the current token environment based on environment variables and credential patterns
52    ///
53    /// Detection logic:
54    /// 1. If `AMP_TESTS=live` is set, returns `Live`
55    /// 2. If credentials contain "mock" string, returns `Mock`
56    /// 3. If real credentials are present without live test flag, returns `Live`
57    /// 4. Fallback to `Mock` for safety
58    #[must_use]
59    pub fn detect() -> Self {
60        let env_vars = Self::read_environment_variables();
61        Self::log_detection_start(&env_vars);
62
63        if Self::is_explicit_live_environment(&env_vars.amp_tests) {
64            return Self::Live;
65        }
66
67        if Self::has_mock_credentials(&env_vars.username, &env_vars.password, &env_vars.base_url) {
68            Self::log_detection_result("mock environment via mock credentials");
69            return Self::Mock;
70        }
71
72        if Self::has_real_credentials(&env_vars.username, &env_vars.password) {
73            Self::log_detection_result("live environment via real credentials");
74            return Self::Live;
75        }
76
77        Self::log_detection_result("mock environment via fallback (no credentials)");
78        Self::Mock
79    }
80
81    /// Reads environment variables needed for token environment detection
82    fn read_environment_variables() -> EnvironmentVariables {
83        EnvironmentVariables {
84            username: env::var("AMP_USERNAME").unwrap_or_default(),
85            password: env::var("AMP_PASSWORD").unwrap_or_default(),
86            amp_tests: env::var("AMP_TESTS").unwrap_or_default(),
87            base_url: env::var("AMP_API_BASE_URL").unwrap_or_default(),
88        }
89    }
90
91    /// Logs the start of environment detection with current variable values
92    fn log_detection_start(env_vars: &EnvironmentVariables) {
93        tracing::debug!(
94            "Detecting token environment - AMP_TESTS: '{}', username: '{}', base_url: '{}'",
95            env_vars.amp_tests,
96            env_vars.username,
97            env_vars.base_url
98        );
99    }
100
101    /// Checks if the environment is explicitly set to live testing
102    fn is_explicit_live_environment(amp_tests: &str) -> bool {
103        if amp_tests == "live" {
104            Self::log_detection_result("live environment via AMP_TESTS=live");
105            true
106        } else {
107            false
108        }
109    }
110
111    /// Checks if real (non-empty) credentials are present
112    const fn has_real_credentials(username: &str, password: &str) -> bool {
113        !username.is_empty() && !password.is_empty()
114    }
115
116    /// Logs the final detection result
117    fn log_detection_result(reason: &str) {
118        tracing::info!("Detected {}", reason);
119    }
120
121    /// Checks if the provided credentials indicate a mock environment
122    ///
123    /// Mock credentials are detected by:
124    /// - Username containing "mock" (case-insensitive)
125    /// - Password containing "mock" (case-insensitive)
126    /// - Base URL containing localhost, 127.0.0.1, or "mock"
127    #[must_use]
128    pub fn has_mock_credentials(username: &str, password: &str, base_url: &str) -> bool {
129        let username_lower = username.to_lowercase();
130        let password_lower = password.to_lowercase();
131        let base_url_lower = base_url.to_lowercase();
132
133        let has_mock_username = username_lower.contains("mock");
134        let has_mock_password = password_lower.contains("mock");
135        let has_mock_url = base_url_lower.contains("localhost")
136            || base_url_lower.contains("127.0.0.1")
137            || base_url_lower.contains("mock");
138
139        let is_mock = has_mock_username || has_mock_password || has_mock_url;
140
141        tracing::debug!(
142            "Mock credential check - username: {}, password: {}, url: {}, result: {}",
143            has_mock_username,
144            has_mock_password,
145            has_mock_url,
146            is_mock
147        );
148
149        is_mock
150    }
151
152    /// Creates a token strategy based on the environment type
153    ///
154    /// # Arguments
155    /// * `mock_token` - Optional mock token to use for mock environments
156    ///
157    /// # Errors
158    /// Returns an error if strategy creation fails
159    pub async fn create_strategy(
160        &self,
161        mock_token: Option<String>,
162    ) -> Result<Box<dyn TokenStrategy>, Error> {
163        match self {
164            Self::Mock => Ok(Self::create_mock_strategy(mock_token)),
165            Self::Live => Self::create_live_strategy().await,
166            Self::Auto => Self::create_auto_detected_strategy(mock_token).await,
167        }
168    }
169
170    /// Creates a mock token strategy with the provided or default token
171    fn create_mock_strategy(mock_token: Option<String>) -> Box<dyn TokenStrategy> {
172        let token = mock_token.unwrap_or_else(|| "default_mock_token".to_string());
173        tracing::debug!("Creating mock token strategy with token");
174        Box::new(MockTokenStrategy::new(token))
175    }
176
177    /// Creates a live token strategy
178    async fn create_live_strategy() -> Result<Box<dyn TokenStrategy>, Error> {
179        tracing::debug!("Creating live token strategy");
180        let strategy = LiveTokenStrategy::new().await?;
181        Ok(Box::new(strategy))
182    }
183
184    /// Creates a strategy based on auto-detected environment
185    async fn create_auto_detected_strategy(
186        mock_token: Option<String>,
187    ) -> Result<Box<dyn TokenStrategy>, Error> {
188        tracing::debug!("Auto-detecting environment for strategy creation");
189        let detected = Self::detect();
190
191        match detected {
192            Self::Mock => Ok(Self::create_auto_detected_mock_strategy(mock_token)),
193            Self::Live => Self::create_auto_detected_live_strategy().await,
194            Self::Auto => Self::handle_unexpected_auto_detection(),
195        }
196    }
197
198    /// Creates a mock strategy for auto-detected mock environment
199    fn create_auto_detected_mock_strategy(mock_token: Option<String>) -> Box<dyn TokenStrategy> {
200        let token = mock_token.unwrap_or_else(|| "default_mock_token".to_string());
201        tracing::debug!("Auto-detected mock environment, creating mock strategy");
202        Box::new(MockTokenStrategy::new(token))
203    }
204
205    /// Creates a live strategy for auto-detected live environment
206    async fn create_auto_detected_live_strategy() -> Result<Box<dyn TokenStrategy>, Error> {
207        tracing::debug!("Auto-detected live environment, creating live strategy");
208        let strategy = LiveTokenStrategy::new().await?;
209        Ok(Box::new(strategy))
210    }
211
212    /// Handles the unexpected case where `detect()` returns Auto
213    fn handle_unexpected_auto_detection() -> Result<Box<dyn TokenStrategy>, Error> {
214        tracing::error!("Unexpected Auto environment from detect()");
215        Err(Error::Token(TokenError::validation(
216            "Environment detection returned Auto, which should not happen".to_string(),
217        )))
218    }
219
220    /// Creates a token strategy with automatic environment detection
221    ///
222    /// This is a convenience method that combines environment detection with strategy creation.
223    ///
224    /// # Arguments
225    /// * `mock_token` - Optional mock token to use if mock environment is detected
226    ///
227    /// # Errors
228    /// Returns an error if strategy creation fails
229    pub async fn create_auto_strategy(
230        mock_token: Option<String>,
231    ) -> Result<Box<dyn TokenStrategy>, Error> {
232        let environment = Self::detect();
233        environment.create_strategy(mock_token).await
234    }
235
236    /// Determines if token persistence should be enabled for this environment
237    #[must_use]
238    pub fn should_persist_tokens(&self) -> bool {
239        match self {
240            Self::Mock => false,
241            Self::Live => true,
242            Self::Auto => Self::detect().should_persist_tokens(),
243        }
244    }
245
246    /// Returns true if this is a mock environment
247    #[must_use]
248    pub fn is_mock(&self) -> bool {
249        matches!(self, Self::Mock) || (matches!(self, Self::Auto) && Self::detect().is_mock())
250    }
251
252    /// Returns true if this is a live environment
253    #[must_use]
254    pub fn is_live(&self) -> bool {
255        matches!(self, Self::Live) || (matches!(self, Self::Auto) && Self::detect().is_live())
256    }
257}
258
259/// Token management strategy trait for different token handling approaches
260#[async_trait]
261pub trait TokenStrategy: Send + Sync + std::fmt::Debug {
262    /// Gets a valid authentication token
263    async fn get_token(&self) -> Result<String, Error>;
264
265    /// Clears stored token (for testing)
266    async fn clear_token(&self) -> Result<(), Error>;
267
268    /// Returns whether this strategy should persist tokens
269    fn should_persist(&self) -> bool;
270
271    /// Returns the strategy type for debugging
272    fn strategy_type(&self) -> &'static str;
273
274    /// Returns self as Any for downcasting (used internally)
275    fn as_any(&self) -> &dyn std::any::Any;
276}
277
278/// Mock token strategy for isolated testing without persistence
279#[derive(Debug, Clone)]
280pub struct MockTokenStrategy {
281    token: String,
282}
283
284impl MockTokenStrategy {
285    /// Creates a new mock token strategy with the provided token
286    #[must_use]
287    pub const fn new(token: String) -> Self {
288        Self { token }
289    }
290
291    /// Creates a mock token strategy with a default test token
292    #[must_use]
293    pub fn with_default_token() -> Self {
294        Self::new("mock_token_default".to_string())
295    }
296
297    /// Creates a mock token strategy for a specific test case
298    #[must_use]
299    pub fn for_test(test_name: &str) -> Self {
300        Self::new(format!("mock_token_{test_name}"))
301    }
302}
303
304#[async_trait]
305impl TokenStrategy for MockTokenStrategy {
306    async fn get_token(&self) -> Result<String, Error> {
307        tracing::debug!("Using mock token strategy - returning pre-set token");
308        Ok(self.token.clone())
309    }
310
311    async fn clear_token(&self) -> Result<(), Error> {
312        tracing::debug!("Mock token strategy - clear_token is a no-op");
313        Ok(())
314    }
315
316    fn should_persist(&self) -> bool {
317        false
318    }
319
320    fn strategy_type(&self) -> &'static str {
321        "mock"
322    }
323
324    fn as_any(&self) -> &dyn std::any::Any {
325        self
326    }
327}
328
329/// Live token strategy that wraps the existing `TokenManager` for full token management
330#[derive(Debug)]
331pub struct LiveTokenStrategy {
332    token_manager: Arc<TokenManager>,
333}
334
335impl LiveTokenStrategy {
336    /// Creates a new live token strategy using the global `TokenManager` instance
337    ///
338    /// # Errors
339    /// Returns an error if the `TokenManager` cannot be initialized
340    pub async fn new() -> Result<Self, Error> {
341        let token_manager = TokenManager::get_global_instance().await?;
342        Ok(Self { token_manager })
343    }
344
345    /// Creates a new live token strategy with a custom `TokenManager`
346    #[must_use]
347    pub const fn with_token_manager(token_manager: Arc<TokenManager>) -> Self {
348        Self { token_manager }
349    }
350
351    /// Creates a live token strategy with custom retry configuration
352    ///
353    /// # Errors
354    /// Returns an error if the `TokenManager` cannot be initialized
355    pub async fn with_config(config: RetryConfig) -> Result<Self, Error> {
356        let base_url = get_amp_api_base_url()?;
357        let token_manager =
358            Arc::new(TokenManager::with_config_and_base_url(config, base_url).await?);
359        Ok(Self { token_manager })
360    }
361
362    /// Creates a live token strategy optimized for testing
363    ///
364    /// # Errors
365    /// Returns an error if the `TokenManager` cannot be initialized
366    pub async fn for_testing() -> Result<Self, Error> {
367        let config = RetryConfig::for_tests();
368        Self::with_config(config).await
369    }
370
371    /// Gets current token information for debugging and monitoring
372    ///
373    /// # Errors
374    /// Returns an error if token information retrieval fails
375    pub async fn get_token_info(&self) -> Result<Option<TokenInfo>, Error> {
376        self.token_manager.get_token_info().await
377    }
378}
379
380#[async_trait]
381impl TokenStrategy for LiveTokenStrategy {
382    async fn get_token(&self) -> Result<String, Error> {
383        tracing::debug!("Using live token strategy - full token management");
384        self.token_manager.get_token().await
385    }
386
387    async fn clear_token(&self) -> Result<(), Error> {
388        self.token_manager.clear_token().await
389    }
390
391    fn should_persist(&self) -> bool {
392        true
393    }
394
395    fn strategy_type(&self) -> &'static str {
396        "live"
397    }
398
399    fn as_any(&self) -> &dyn std::any::Any {
400        self
401    }
402}
403
404#[derive(Error, Debug)]
405pub enum Error {
406    #[error("Missing {0} environment variable")]
407    MissingEnvVar(String),
408    #[error("AMP request failed: {0}")]
409    RequestFailed(String),
410    #[error("Failed to parse AMP response: {0}")]
411    ResponseParsingFailed(String),
412    #[error("AMP token request failed with status {status}: {error_text}")]
413    TokenRequestFailed {
414        status: reqwest::StatusCode,
415        error_text: String,
416    },
417    #[error("Failed to parse url: {0}")]
418    UrlParse(#[from] url::ParseError),
419    #[error("Reqwest error: {0}")]
420    Reqwest(#[from] reqwest::Error),
421    #[error("Invalid retry configuration: {0}")]
422    InvalidRetryConfig(String),
423    #[error("Token management error: {0}")]
424    Token(#[from] TokenError),
425}
426
427/// Enhanced error enum for distribution operations and `ElementsRpc`
428#[derive(Error, Debug)]
429pub enum AmpError {
430    #[error("API error: {0}")]
431    Api(String),
432
433    #[error("RPC error: {0}")]
434    Rpc(String),
435
436    #[error("Signer error: {0}")]
437    Signer(#[from] SignerError),
438
439    #[error("Timeout waiting for confirmations: {0}")]
440    Timeout(String),
441
442    #[error("Validation error: {0}")]
443    Validation(String),
444
445    #[error("Network error: {0}")]
446    Network(#[from] reqwest::Error),
447
448    #[error("Serialization error: {0}")]
449    Serialization(#[from] serde_json::Error),
450
451    #[error(transparent)]
452    Existing(#[from] Error),
453}
454
455impl AmpError {
456    /// Creates a new API error
457    pub fn api<S: Into<String>>(message: S) -> Self {
458        Self::Api(message.into())
459    }
460
461    /// Creates a new RPC error
462    pub fn rpc<S: Into<String>>(message: S) -> Self {
463        Self::Rpc(message.into())
464    }
465
466    /// Creates a new timeout error
467    pub fn timeout<S: Into<String>>(message: S) -> Self {
468        Self::Timeout(message.into())
469    }
470
471    /// Creates a new validation error
472    pub fn validation<S: Into<String>>(message: S) -> Self {
473        Self::Validation(message.into())
474    }
475
476    /// Adds context to an error
477    #[must_use]
478    pub fn with_context<S: Into<String>>(self, context: S) -> Self {
479        let context_str = context.into();
480        match self {
481            Self::Api(msg) => Self::Api(format!("{context_str}: {msg}")),
482            Self::Rpc(msg) => Self::Rpc(format!("{context_str}: {msg}")),
483            Self::Timeout(msg) => Self::Timeout(format!("{context_str}: {msg}")),
484            Self::Validation(msg) => Self::Validation(format!("{context_str}: {msg}")),
485            other => other, // Don't modify other error types
486        }
487    }
488
489    /// Returns true if this error indicates a retryable condition
490    #[must_use]
491    pub const fn is_retryable(&self) -> bool {
492        match self {
493            Self::Network(_) | Self::Rpc(_) => true, // RPC errors might be transient
494            Self::Existing(Error::Token(token_err)) => token_err.is_retryable(),
495            _ => false,
496        }
497    }
498
499    /// Provides user-friendly retry instructions when applicable
500    #[must_use]
501    pub fn retry_instructions(&self) -> Option<String> {
502        match self {
503            Self::Network(_) => Some("Check network connection and retry".to_string()),
504            Self::Rpc(_) => Some("Check Elements node connection and retry".to_string()),
505            Self::Timeout(msg) if msg.contains("txid") => {
506                Some("Use the transaction ID to manually confirm the distribution".to_string())
507            }
508            Self::Existing(Error::Token(TokenError::RateLimited {
509                retry_after_seconds,
510            })) => Some(format!(
511                "Rate limited. Retry after {retry_after_seconds} seconds"
512            )),
513            _ => None,
514        }
515    }
516}
517
518/// Detailed error types for token management operations
519#[derive(Error, Debug, Clone, PartialEq, Eq)]
520pub enum TokenError {
521    #[error("Token refresh failed: {0}")]
522    RefreshFailed(String),
523    #[error("Token obtain failed after {attempts} attempts: {last_error}")]
524    ObtainFailed { attempts: u32, last_error: String },
525    #[error("Rate limited: retry after {retry_after_seconds} seconds")]
526    RateLimited { retry_after_seconds: u64 },
527    #[error("Request timeout after {timeout_seconds} seconds")]
528    Timeout { timeout_seconds: u64 },
529    #[error("Serialization error: {0}")]
530    Serialization(String),
531    #[error("Token storage error: {0}")]
532    Storage(String),
533    #[error("Token validation error: {0}")]
534    Validation(String),
535}
536
537impl TokenError {
538    /// Creates a new `RefreshFailed` error
539    #[must_use]
540    pub fn refresh_failed<S: Into<String>>(message: S) -> Self {
541        Self::RefreshFailed(message.into())
542    }
543
544    /// Creates a new `ObtainFailed` error
545    #[must_use]
546    pub const fn obtain_failed(attempts: u32, last_error: String) -> Self {
547        Self::ObtainFailed {
548            attempts,
549            last_error,
550        }
551    }
552
553    /// Creates a new `RateLimited` error
554    #[must_use]
555    pub const fn rate_limited(retry_after_seconds: u64) -> Self {
556        Self::RateLimited {
557            retry_after_seconds,
558        }
559    }
560
561    /// Creates a new Timeout error
562    #[must_use]
563    pub const fn timeout(timeout_seconds: u64) -> Self {
564        Self::Timeout { timeout_seconds }
565    }
566
567    /// Creates a new Serialization error
568    #[must_use]
569    pub fn serialization<S: Into<String>>(message: S) -> Self {
570        Self::Serialization(message.into())
571    }
572
573    /// Creates a new Storage error
574    #[must_use]
575    pub fn storage<S: Into<String>>(message: S) -> Self {
576        Self::Storage(message.into())
577    }
578
579    /// Creates a new Validation error
580    #[must_use]
581    pub fn validation<S: Into<String>>(message: S) -> Self {
582        Self::Validation(message.into())
583    }
584
585    /// Returns true if this error indicates a retryable condition
586    #[must_use]
587    pub const fn is_retryable(&self) -> bool {
588        matches!(
589            self,
590            Self::RefreshFailed(_) | Self::RateLimited { .. } | Self::Timeout { .. }
591        )
592    }
593
594    /// Returns true if this error indicates a rate limiting condition
595    #[must_use]
596    pub const fn is_rate_limited(&self) -> bool {
597        matches!(self, Self::RateLimited { .. })
598    }
599
600    /// Returns the retry delay in seconds if this is a rate limited error
601    #[must_use]
602    pub const fn retry_after_seconds(&self) -> Option<u64> {
603        match self {
604            Self::RateLimited {
605                retry_after_seconds,
606            } => Some(*retry_after_seconds),
607            _ => None,
608        }
609    }
610}
611
612// Conversion from serde_json::Error for serialization errors
613impl From<serde_json::Error> for TokenError {
614    fn from(err: serde_json::Error) -> Self {
615        Self::Serialization(err.to_string())
616    }
617}
618
619#[cfg(test)]
620mod amp_error_tests {
621    use super::*;
622
623    #[test]
624    fn test_amp_error_creation_helpers() {
625        let api_error = AmpError::api("Failed to create distribution");
626        assert!(matches!(api_error, AmpError::Api(_)));
627
628        let rpc_error = AmpError::rpc("Elements node connection failed");
629        assert!(matches!(rpc_error, AmpError::Rpc(_)));
630
631        let validation_error = AmpError::validation("Invalid asset UUID format");
632        assert!(matches!(validation_error, AmpError::Validation(_)));
633
634        let timeout_error = AmpError::timeout("Confirmation timeout");
635        assert!(matches!(timeout_error, AmpError::Timeout(_)));
636    }
637
638    #[test]
639    fn test_amp_error_with_context() {
640        let api_error = AmpError::api("Failed to create distribution");
641        let contextual_error = api_error.with_context("During distribution creation");
642
643        match contextual_error {
644            AmpError::Api(msg) => {
645                assert!(msg.contains("During distribution creation"));
646                assert!(msg.contains("Failed to create distribution"));
647            }
648            _ => panic!("Expected Api error variant"),
649        }
650
651        // Test that context doesn't modify errors that already have good context
652        let signer_error = AmpError::Signer(SignerError::Lwk("Test error".to_string()));
653        let contextual_signer = signer_error.with_context("Additional context");
654        assert!(matches!(contextual_signer, AmpError::Signer(_)));
655    }
656
657    #[test]
658    fn test_amp_error_retryability() {
659        let api_error = AmpError::api("Failed to create distribution");
660        assert!(!api_error.is_retryable());
661
662        let rpc_error = AmpError::rpc("Elements node connection failed");
663        assert!(rpc_error.is_retryable());
664
665        let validation_error = AmpError::validation("Invalid asset UUID format");
666        assert!(!validation_error.is_retryable());
667
668        let timeout_error = AmpError::timeout("Confirmation timeout");
669        assert!(!timeout_error.is_retryable());
670
671        let signer_error = AmpError::Signer(SignerError::Lwk("Test error".to_string()));
672        assert!(!signer_error.is_retryable());
673    }
674
675    #[test]
676    fn test_amp_error_retry_instructions() {
677        let rpc_error = AmpError::rpc("Elements node connection failed");
678        let instructions = rpc_error.retry_instructions();
679        assert!(instructions.is_some());
680        assert!(instructions.unwrap().contains("Elements node"));
681
682        let validation_error = AmpError::validation("Invalid asset UUID format");
683        assert!(validation_error.retry_instructions().is_none());
684
685        let timeout_with_txid = AmpError::timeout("Confirmation timeout for txid abc123");
686        let timeout_instructions = timeout_with_txid.retry_instructions();
687        assert!(timeout_instructions.is_some());
688        assert!(timeout_instructions.unwrap().contains("transaction ID"));
689    }
690
691    #[test]
692    fn test_amp_error_display() {
693        let api_error = AmpError::api("Test API error");
694        assert_eq!(format!("{}", api_error), "API error: Test API error");
695
696        let rpc_error = AmpError::rpc("Test RPC error");
697        assert_eq!(format!("{}", rpc_error), "RPC error: Test RPC error");
698
699        let validation_error = AmpError::validation("Test validation error");
700        assert_eq!(
701            format!("{}", validation_error),
702            "Validation error: Test validation error"
703        );
704
705        let timeout_error = AmpError::timeout("Test timeout error");
706        assert_eq!(
707            format!("{}", timeout_error),
708            "Timeout waiting for confirmations: Test timeout error"
709        );
710    }
711
712    #[test]
713    fn test_amp_error_from_conversions() {
714        // Test conversion from SignerError
715        let signer_error = SignerError::Lwk("Test LWK error".to_string());
716        let amp_error = AmpError::from(signer_error);
717        assert!(matches!(amp_error, AmpError::Signer(_)));
718
719        // Test conversion from existing Error
720        let existing_error = Error::MissingEnvVar("TEST_VAR".to_string());
721        let amp_error = AmpError::from(existing_error);
722        assert!(matches!(amp_error, AmpError::Existing(_)));
723
724        // Test conversion from serde_json::Error
725        let json_error = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
726        let amp_error = AmpError::from(json_error);
727        assert!(matches!(amp_error, AmpError::Serialization(_)));
728    }
729}
730
731/// Elements RPC client for blockchain operations
732#[derive(Debug)]
733pub struct ElementsRpc {
734    client: reqwest::Client,
735    base_url: String,
736    username: String,
737    password: String,
738}
739
740/// Network information from Elements node
741#[derive(Debug, serde::Deserialize)]
742pub struct NetworkInfo {
743    pub version: i64,
744    pub subversion: String,
745    pub protocolversion: i64,
746    pub localservices: String,
747    pub localrelay: bool,
748    pub timeoffset: i64,
749    pub networkactive: bool,
750    pub connections: i64,
751    pub networks: Vec<serde_json::Value>,
752    pub relayfee: f64,
753    pub incrementalfee: f64,
754    pub localaddresses: Vec<serde_json::Value>,
755    pub warnings: String,
756}
757
758/// Blockchain information from Elements node
759#[derive(Debug, serde::Deserialize)]
760pub struct BlockchainInfo {
761    pub chain: String,
762    pub blocks: i64,
763    pub headers: i64,
764    pub bestblockhash: String,
765    #[serde(default)]
766    pub difficulty: Option<f64>,
767    #[serde(default)]
768    pub mediantime: Option<i64>,
769    #[serde(default)]
770    pub verificationprogress: Option<f64>,
771    #[serde(default)]
772    pub initialblockdownload: Option<bool>,
773    #[serde(default)]
774    pub chainwork: Option<String>,
775    #[serde(default)]
776    pub size_on_disk: Option<i64>,
777    #[serde(default)]
778    pub pruned: Option<bool>,
779    #[serde(default)]
780    pub softforks: Option<serde_json::Value>,
781    #[serde(default)]
782    pub warnings: Option<String>,
783}
784
785/// RPC request structure for Elements node
786#[derive(Debug, serde::Serialize)]
787struct RpcRequest {
788    jsonrpc: String,
789    id: String,
790    method: String,
791    params: serde_json::Value,
792}
793
794/// RPC response structure from Elements node
795#[derive(Debug, serde::Deserialize)]
796struct RpcResponse<T> {
797    #[allow(dead_code)]
798    jsonrpc: Option<String>, // Optional for JSON-RPC 1.0 compatib
799    #[allow(dead_code)]
800    id: String,
801    result: Option<T>,
802    error: Option<RpcError>,
803}
804
805/// RPC error structure from Elements node
806#[derive(Debug, serde::Deserialize)]
807struct RpcError {
808    code: i32,
809    message: String,
810}
811
812impl ElementsRpc {
813    /// Creates a new `ElementsRpc` client with connection parameters
814    ///
815    /// # Arguments
816    /// * `url` - The RPC endpoint URL (e.g., <http://localhost:18884>)
817    /// * `username` - RPC authentication username
818    /// * `password` - RPC authentication password
819    ///
820    /// # Examples
821    /// ```
822    /// use amp_rs::ElementsRpc;
823    ///
824    /// let rpc = ElementsRpc::new(
825    ///     "http://localhost:18884".to_string(),
826    ///     "user".to_string(),
827    ///     "pass".to_string()
828    /// );
829    /// ```
830    /// # Panics
831    ///
832    /// Panics if the HTTP client cannot be created.
833    #[must_use]
834    pub fn new(url: String, username: String, password: String) -> Self {
835        let client = reqwest::Client::builder()
836            .timeout(std::time::Duration::from_secs(30))
837            .build()
838            .expect("Failed to create HTTP client");
839
840        Self {
841            client,
842            base_url: url,
843            username,
844            password,
845        }
846    }
847
848    /// Creates a new `ElementsRpc` client from environment variables
849    ///
850    /// Expected environment variables:
851    /// - `ELEMENTS_RPC_URL`: RPC endpoint URL
852    /// - `ELEMENTS_RPC_USER`: RPC username
853    /// - `ELEMENTS_RPC_PASSWORD`: RPC password
854    ///
855    /// # Errors
856    /// Returns an error if any required environment variable is missing
857    ///
858    /// # Examples
859    /// ```no_run
860    /// use amp_rs::ElementsRpc;
861    ///
862    /// let rpc = ElementsRpc::from_env().unwrap();
863    /// ```
864    pub fn from_env() -> Result<Self, AmpError> {
865        let url = env::var("ELEMENTS_RPC_URL")
866            .map_err(|_| AmpError::validation("Missing ELEMENTS_RPC_URL environment variable"))?;
867        let username = env::var("ELEMENTS_RPC_USER")
868            .map_err(|_| AmpError::validation("Missing ELEMENTS_RPC_USER environment variable"))?;
869        let password = env::var("ELEMENTS_RPC_PASSWORD").map_err(|_| {
870            AmpError::validation("Missing ELEMENTS_RPC_PASSWORD environment variable")
871        })?;
872
873        Ok(Self::new(url, username, password))
874    }
875
876    /// Makes an RPC call to the Elements node
877    ///
878    /// # Arguments
879    /// * `method` - The RPC method name
880    /// * `params` - The parameters for the RPC call
881    ///
882    /// # Errors
883    /// Returns an error if the RPC call fails or returns an error
884    async fn rpc_call<T: serde::de::DeserializeOwned>(
885        &self,
886        method: &str,
887        params: serde_json::Value,
888    ) -> Result<T, AmpError> {
889        tracing::debug!("Making RPC call: {} with params: {:?}", method, params);
890
891        let request = RpcRequest {
892            jsonrpc: "1.0".to_string(),
893            id: "amp-client".to_string(),
894            method: method.to_string(),
895            params,
896        };
897
898        let response = self
899            .client
900            .post(&self.base_url)
901            .basic_auth(&self.username, Some(&self.password))
902            .json(&request)
903            .send()
904            .await
905            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
906
907        if !response.status().is_success() {
908            let status = response.status();
909            let error_body = response
910                .text()
911                .await
912                .unwrap_or_else(|_| "Unable to read error body".to_string());
913            return Err(AmpError::rpc(format!(
914                "RPC request failed with status: {status} - Body: {error_body}"
915            )));
916        }
917
918        let rpc_response: RpcResponse<T> = response
919            .json()
920            .await
921            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
922
923        if let Some(error) = rpc_response.error {
924            return Err(AmpError::rpc(format!(
925                "RPC error {}: {}",
926                error.code, error.message
927            )));
928        }
929
930        rpc_response
931            .result
932            .ok_or_else(|| AmpError::rpc("RPC response missing result field".to_string()))
933    }
934
935    /// Retrieves network information from the Elements node
936    ///
937    /// # Errors
938    /// Returns an error if the RPC call fails
939    ///
940    /// # Examples
941    /// ```no_run
942    /// # use amp_rs::ElementsRpc;
943    /// # #[tokio::main]
944    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
945    /// let rpc = ElementsRpc::from_env()?;
946    /// let network_info = rpc.get_network_info().await?;
947    /// println!("Node version: {}", network_info.version);
948    /// # Ok(())
949    /// # }
950    /// ```
951    pub async fn get_network_info(&self) -> Result<NetworkInfo, AmpError> {
952        self.rpc_call("getnetworkinfo", serde_json::Value::Array(vec![]))
953            .await
954    }
955
956    /// Retrieves blockchain information from the Elements node
957    ///
958    /// # Errors
959    /// Returns an error if the RPC call fails
960    ///
961    /// # Examples
962    /// ```no_run
963    /// # use amp_rs::ElementsRpc;
964    /// # #[tokio::main]
965    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
966    /// let rpc = ElementsRpc::from_env()?;
967    /// let blockchain_info = rpc.get_blockchain_info().await?;
968    /// println!("Current block height: {}", blockchain_info.blocks);
969    /// # Ok(())
970    /// # }
971    /// ```
972    pub async fn get_blockchain_info(&self) -> Result<BlockchainInfo, AmpError> {
973        self.rpc_call("getblockchaininfo", serde_json::Value::Array(vec![]))
974            .await
975    }
976
977    /// Unlocks the wallet with a passphrase for the specified timeout
978    ///
979    /// # Arguments
980    /// * `passphrase` - The wallet passphrase
981    /// * `timeout` - Timeout in seconds for the unlock
982    ///
983    /// # Errors
984    /// Returns an error if the RPC call fails
985    ///
986    /// # Examples
987    /// ```no_run
988    /// # use amp_rs::ElementsRpc;
989    /// # #[tokio::main]
990    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
991    /// let rpc = ElementsRpc::from_env()?;
992    /// rpc.wallet_passphrase("my_passphrase", 300).await?;
993    /// # Ok(())
994    /// # }
995    /// ```
996    pub async fn wallet_passphrase(&self, passphrase: &str, timeout: u64) -> Result<(), AmpError> {
997        let params = serde_json::json!([passphrase, timeout]);
998
999        // wallet_passphrase returns null on success, so we need to handle this specially
1000        let request = RpcRequest {
1001            jsonrpc: "1.0".to_string(),
1002            id: "amp-client".to_string(),
1003            method: "walletpassphrase".to_string(),
1004            params,
1005        };
1006
1007        let response = self
1008            .client
1009            .post(&self.base_url)
1010            .basic_auth(&self.username, Some(&self.password))
1011            .json(&request)
1012            .send()
1013            .await
1014            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
1015
1016        if !response.status().is_success() {
1017            return Err(AmpError::rpc(format!(
1018                "RPC request failed with status: {}",
1019                response.status()
1020            )));
1021        }
1022
1023        let rpc_response: RpcResponse<serde_json::Value> = response
1024            .json()
1025            .await
1026            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
1027
1028        if let Some(error) = rpc_response.error {
1029            return Err(AmpError::rpc(format!(
1030                "RPC error {}: {}",
1031                error.code, error.message
1032            )));
1033        }
1034
1035        // For wallet_passphrase, null result is success
1036        Ok(())
1037    }
1038
1039    /// Validates the connection to the Elements node
1040    ///
1041    /// This method performs basic connectivity and authentication checks by
1042    /// retrieving network information from the node.
1043    ///
1044    /// # Errors
1045    /// Returns an error if the connection validation fails
1046    ///
1047    /// # Examples
1048    /// ```no_run
1049    /// # use amp_rs::ElementsRpc;
1050    /// # #[tokio::main]
1051    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1052    /// let rpc = ElementsRpc::from_env()?;
1053    /// rpc.validate_connection().await?;
1054    /// println!("Connection to Elements node is valid");
1055    /// # Ok(())
1056    /// # }
1057    /// ```
1058    pub async fn validate_connection(&self) -> Result<(), AmpError> {
1059        tracing::info!(
1060            "Validating connection to Elements node at {}",
1061            self.base_url
1062        );
1063
1064        let network_info = self
1065            .get_network_info()
1066            .await
1067            .map_err(|e| e.with_context("Failed to validate Elements node connection"))?;
1068
1069        tracing::info!(
1070            "Successfully connected to Elements node - Version: {}, Connections: {}",
1071            network_info.version,
1072            network_info.connections
1073        );
1074
1075        Ok(())
1076    }
1077
1078    /// Retrieves comprehensive node status including network and blockchain information
1079    ///
1080    /// This method combines network and blockchain information to provide a complete
1081    /// status overview of the Elements node.
1082    ///
1083    /// # Errors
1084    /// Returns an error if any RPC call fails
1085    ///
1086    /// # Examples
1087    /// ```no_run
1088    /// # use amp_rs::ElementsRpc;
1089    /// # #[tokio::main]
1090    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1091    /// let rpc = ElementsRpc::from_env()?;
1092    /// let (network_info, blockchain_info) = rpc.get_node_status().await?;
1093    /// println!("Node version: {}, Block height: {}", network_info.version, blockchain_info.blocks);
1094    /// # Ok(())
1095    /// # }
1096    /// ```
1097    pub async fn get_node_status(&self) -> Result<(NetworkInfo, BlockchainInfo), AmpError> {
1098        let network_info = self.get_network_info().await?;
1099        let blockchain_info = self.get_blockchain_info().await?;
1100
1101        Ok((network_info, blockchain_info))
1102    }
1103
1104    /// Lists unspent transaction outputs (UTXOs) for a specific asset
1105    ///
1106    /// # Arguments
1107    /// * `asset_id` - Optional asset ID to filter UTXOs. If None, returns all UTXOs
1108    ///
1109    /// # Errors
1110    /// Returns an error if the RPC call fails
1111    ///
1112    /// # Panics
1113    /// May panic if `asset_id` is `Some` but the warning log message attempts to unwrap it.
1114    /// This is a known logging issue and does not affect normal operation.
1115    ///
1116    /// # Examples
1117    /// ```no_run
1118    /// # use amp_rs::ElementsRpc;
1119    /// # #[tokio::main]
1120    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1121    /// let rpc = ElementsRpc::from_env()?;
1122    /// let utxos = rpc.list_unspent(Some("asset_id_hex")).await?;
1123    /// println!("Found {} UTXOs", utxos.len());
1124    /// # Ok(())
1125    /// # }
1126    /// ```
1127    pub async fn list_unspent(&self, asset_id: Option<&str>) -> Result<Vec<Unspent>, AmpError> {
1128        tracing::debug!("Listing unspent outputs for asset: {:?}", asset_id);
1129
1130        let params = asset_id.map_or_else(
1131            || serde_json::json!([1, 9_999_999, [], true]),
1132            |asset| serde_json::json!([1, 9_999_999, [], true, {"asset": asset}]),
1133        );
1134
1135        let utxos: Vec<Unspent> = self
1136            .rpc_call("listunspent", params)
1137            .await
1138            .map_err(|e| {
1139                if let Some(asset) = asset_id {
1140                    e.with_context(format!(
1141                        "Failed to list unspent outputs for asset {asset}. \
1142                        This may indicate that the treasury address is not imported in the Elements node. \
1143                        Ensure the treasury address is properly imported as a watch-only address."
1144                    ))
1145                } else {
1146                    e.with_context("Failed to list unspent outputs")
1147                }
1148            })?;
1149
1150        tracing::debug!("Found {} unspent outputs", utxos.len());
1151
1152        // If we're looking for a specific asset and found no UTXOs, provide helpful context
1153        if utxos.is_empty() && asset_id.is_some() {
1154            tracing::warn!(
1155                "No UTXOs found for asset {}. This may indicate:\n\
1156                1. The treasury address is not imported in the Elements node\n\
1157                2. The asset issuance transaction hasn't been confirmed yet\n\
1158                3. The UTXOs have already been spent",
1159                asset_id.unwrap()
1160            );
1161        }
1162
1163        Ok(utxos)
1164    }
1165
1166    /// List unspent outputs for a specific wallet
1167    ///
1168    /// This method lists unspent transaction outputs (UTXOs) for a specific wallet,
1169    /// optionally filtered by asset ID.
1170    ///
1171    /// # Arguments
1172    ///
1173    /// * `wallet_name` - Name of the Elements wallet to query
1174    /// * `asset_id` - Optional asset ID to filter UTXOs by
1175    ///
1176    /// # Returns
1177    ///
1178    /// Returns a vector of unspent outputs
1179    ///
1180    /// # Errors
1181    /// Returns an error if the RPC call fails or the wallet cannot be loaded
1182    ///
1183    /// # Panics
1184    /// May panic when processing UTXO blinding data if scriptpubkey is unexpectedly missing.
1185    /// This should not occur under normal operation with valid Elements node responses.
1186    ///
1187    /// # Example
1188    ///
1189    /// ```no_run
1190    /// # use amp_rs::ElementsRpc;
1191    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1192    /// let rpc = ElementsRpc::from_env()?;
1193    /// // Note: This would need to be called in an async context
1194    /// // let utxos = rpc.list_unspent_for_wallet("test_wallet", None).await?;
1195    /// // println!("Found {} UTXOs", utxos.len());
1196    /// # Ok(())
1197    /// # }
1198    /// ```
1199    #[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
1200    pub async fn list_unspent_for_wallet(
1201        &self,
1202        wallet_name: &str,
1203        asset_id: Option<&str>,
1204    ) -> Result<Vec<Unspent>, AmpError> {
1205        tracing::debug!(
1206            "Listing unspent outputs for wallet {} and asset: {:?}",
1207            wallet_name,
1208            asset_id
1209        );
1210
1211        // First load the wallet to ensure it's available
1212        self.load_wallet(wallet_name).await?;
1213
1214        let params = asset_id.map_or_else(
1215            || serde_json::json!([1, 9_999_999, [], true]),
1216            |asset| serde_json::json!([1, 9_999_999, [], true, {"asset": asset}]),
1217        );
1218
1219        let request = RpcRequest {
1220            jsonrpc: "1.0".to_string(),
1221            id: "amp-client".to_string(),
1222            method: "listunspent".to_string(),
1223            params,
1224        };
1225
1226        // Use the wallet-specific RPC endpoint
1227        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
1228
1229        let response = self
1230            .client
1231            .post(&wallet_url)
1232            .basic_auth(&self.username, Some(&self.password))
1233            .json(&request)
1234            .send()
1235            .await
1236            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
1237
1238        if !response.status().is_success() {
1239            let status = response.status();
1240            let error_body = response
1241                .text()
1242                .await
1243                .unwrap_or_else(|_| "Unable to read error body".to_string());
1244            return Err(AmpError::rpc(format!(
1245                "RPC request failed with status: {status} - Body: {error_body}"
1246            )));
1247        }
1248
1249        let rpc_response: RpcResponse<Vec<Unspent>> = response
1250            .json()
1251            .await
1252            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
1253
1254        if let Some(error) = rpc_response.error {
1255            return Err(AmpError::rpc(format!(
1256                "RPC error listing unspent outputs: {} (code: {})",
1257                error.message, error.code
1258            )));
1259        }
1260
1261        let mut utxos = rpc_response.result.unwrap_or_default();
1262
1263        // Enrich UTXOs with scriptpubkey information if missing
1264        for utxo in &mut utxos {
1265            if utxo.scriptpubkey.is_none() {
1266                tracing::debug!(
1267                    "UTXO {}:{} missing scriptpubkey, attempting to derive from address",
1268                    utxo.txid,
1269                    utxo.vout
1270                );
1271
1272                // Try to derive scriptpubkey from the address
1273                if let Ok(address) = elements::Address::from_str(&utxo.address) {
1274                    let script_pubkey = address.script_pubkey();
1275                    utxo.scriptpubkey = Some(hex::encode(script_pubkey.as_bytes()));
1276                    tracing::info!(
1277                        "Derived scriptpubkey for UTXO {}:{} from address {}: {}",
1278                        utxo.txid,
1279                        utxo.vout,
1280                        utxo.address,
1281                        utxo.scriptpubkey.as_ref().unwrap()
1282                    );
1283                } else {
1284                    tracing::error!(
1285                        "Failed to parse address {} for UTXO {}:{}",
1286                        utxo.address,
1287                        utxo.txid,
1288                        utxo.vout
1289                    );
1290
1291                    // Fallback: try to get transaction details
1292                    match self.get_transaction(&utxo.txid).await {
1293                        Ok(tx_detail) => {
1294                            tracing::debug!(
1295                                "Retrieved transaction details for {} as fallback",
1296                                utxo.txid
1297                            );
1298                            // Parse the transaction hex to extract the scriptpubkey for this output
1299                            match hex::decode(&tx_detail.hex) {
1300                                Ok(tx_bytes) => {
1301                                    match elements::Transaction::consensus_decode(&tx_bytes[..]) {
1302                                        Ok(tx) => {
1303                                            if let Some(output) = tx.output.get(utxo.vout as usize)
1304                                            {
1305                                                utxo.scriptpubkey = Some(hex::encode(
1306                                                    output.script_pubkey.as_bytes(),
1307                                                ));
1308                                                tracing::info!("Enriched UTXO {}:{} with scriptpubkey from transaction: {}", 
1309                                                    utxo.txid, utxo.vout, utxo.scriptpubkey.as_ref().unwrap());
1310                                            } else {
1311                                                tracing::error!(
1312                                                    "Output {} not found in transaction {}",
1313                                                    utxo.vout,
1314                                                    utxo.txid
1315                                                );
1316                                            }
1317                                        }
1318                                        Err(e) => {
1319                                            tracing::error!(
1320                                                "Failed to decode transaction {}: {}",
1321                                                utxo.txid,
1322                                                e
1323                                            );
1324                                        }
1325                                    }
1326                                }
1327                                Err(e) => {
1328                                    tracing::error!(
1329                                        "Failed to decode hex for transaction {}: {}",
1330                                        utxo.txid,
1331                                        e
1332                                    );
1333                                }
1334                            }
1335                        }
1336                        Err(e) => {
1337                            tracing::error!(
1338                                "Failed to get transaction details for {}: {}",
1339                                utxo.txid,
1340                                e
1341                            );
1342                        }
1343                    }
1344                }
1345            } else {
1346                tracing::debug!("UTXO {}:{} already has scriptpubkey", utxo.txid, utxo.vout);
1347            }
1348        }
1349
1350        tracing::debug!(
1351            "Found {} unspent outputs for wallet {}",
1352            utxos.len(),
1353            wallet_name
1354        );
1355
1356        // If we're looking for a specific asset and found no UTXOs, provide helpful context
1357        if utxos.is_empty() && asset_id.is_some() {
1358            tracing::warn!(
1359                "No UTXOs found for asset {} in wallet {}. This may indicate:\n\
1360                1. The asset issuance transaction hasn't been confirmed yet\n\
1361                2. The UTXOs have already been spent\n\
1362                3. The wallet doesn't contain the expected addresses",
1363                asset_id.unwrap(),
1364                wallet_name
1365            );
1366        }
1367
1368        Ok(utxos)
1369    }
1370
1371    /// Creates a raw transaction with the specified inputs and outputs
1372    ///
1373    /// # Arguments
1374    /// * `inputs` - Vector of transaction inputs (UTXOs to spend)
1375    /// * `outputs` - Map of addresses to amounts for regular outputs
1376    /// * `assets` - Map of addresses to asset IDs for Liquid-specific outputs
1377    ///
1378    /// # Errors
1379    /// Returns an error if the RPC call fails or transaction creation fails
1380    ///
1381    /// # Examples
1382    /// ```no_run
1383    /// # use amp_rs::{ElementsRpc, model::{TxInput}};
1384    /// # use std::collections::HashMap;
1385    /// # #[tokio::main]
1386    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1387    /// let rpc = ElementsRpc::from_env()?;
1388    /// let inputs = vec![TxInput {
1389    ///     txid: "abc123".to_string(),
1390    ///     vout: 0,
1391    ///     sequence: None,
1392    /// }];
1393    /// let mut outputs = HashMap::new();
1394    /// outputs.insert("address1".to_string(), 100.0);
1395    /// let mut assets = HashMap::new();
1396    /// assets.insert("address1".to_string(), "asset_id".to_string());
1397    /// let raw_tx = rpc.create_raw_transaction(inputs, outputs, assets).await?;
1398    /// # Ok(())
1399    /// # }
1400    /// ```
1401    #[allow(clippy::cognitive_complexity)]
1402    pub async fn create_raw_transaction(
1403        &self,
1404        inputs: Vec<TxInput>,
1405        outputs: std::collections::HashMap<String, f64>,
1406        assets: std::collections::HashMap<String, String>,
1407    ) -> Result<String, AmpError> {
1408        tracing::debug!(
1409            "Creating raw transaction with {} inputs and {} outputs",
1410            inputs.len(),
1411            outputs.len()
1412        );
1413
1414        // Elements RPC createrawtransaction expects:
1415        // createrawtransaction inputs outputs locktime replaceable assets
1416        let params = serde_json::json!([
1417            inputs,  // inputs as TxInput array
1418            outputs, // outputs as address->amount map
1419            0,       // locktime (0 = no locktime)
1420            false,   // replaceable (false = not replaceable)
1421            assets   // assets as address->asset_id map
1422        ]);
1423
1424        // Debug: Log the exact parameters being sent to createrawtransaction
1425        tracing::error!("createrawtransaction parameters:");
1426        tracing::error!(
1427            "  inputs: {}",
1428            serde_json::to_string_pretty(&inputs).unwrap_or_default()
1429        );
1430        tracing::error!(
1431            "  outputs: {}",
1432            serde_json::to_string_pretty(&outputs).unwrap_or_default()
1433        );
1434        tracing::error!(
1435            "  assets: {}",
1436            serde_json::to_string_pretty(&assets).unwrap_or_default()
1437        );
1438
1439        let raw_tx: String = self
1440            .rpc_call("createrawtransaction", params)
1441            .await
1442            .map_err(|e| {
1443                tracing::error!("createrawtransaction RPC call failed: {}", e);
1444                e.with_context("Failed to create raw transaction")
1445            })?;
1446
1447        tracing::debug!("Created raw transaction: {}", raw_tx);
1448        Ok(raw_tx)
1449    }
1450
1451    /// Imports an address into a specific wallet as watch-only
1452    ///
1453    /// # Arguments
1454    /// * `wallet_name` - Name of the wallet to import into
1455    /// * `address` - The address to import
1456    /// * `label` - Optional label for the address
1457    /// * `rescan` - Whether to rescan the blockchain for transactions
1458    ///
1459    /// # Errors
1460    /// Returns an error if the RPC call fails
1461    async fn import_address_to_wallet(
1462        &self,
1463        wallet_name: &str,
1464        address: &str,
1465        label: Option<&str>,
1466        rescan: bool,
1467    ) -> Result<(), AmpError> {
1468        tracing::debug!("Importing address {} into wallet {}", address, wallet_name);
1469
1470        // First load the wallet to ensure it's available
1471        self.load_wallet(wallet_name).await?;
1472
1473        let params = serde_json::json!([address, label.unwrap_or(""), rescan]);
1474
1475        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
1476
1477        let request = RpcRequest {
1478            jsonrpc: "1.0".to_string(),
1479            id: "amp-client".to_string(),
1480            method: "importaddress".to_string(),
1481            params,
1482        };
1483
1484        let response = self
1485            .client
1486            .post(&wallet_url)
1487            .basic_auth(&self.username, Some(&self.password))
1488            .json(&request)
1489            .send()
1490            .await
1491            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
1492
1493        if !response.status().is_success() {
1494            let status = response.status();
1495            let error_body = response
1496                .text()
1497                .await
1498                .unwrap_or_else(|_| "Unable to read error body".to_string());
1499            return Err(AmpError::rpc(format!(
1500                "RPC request failed with status: {status} - Body: {error_body}"
1501            )));
1502        }
1503
1504        let rpc_response: RpcResponse<serde_json::Value> = response
1505            .json()
1506            .await
1507            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
1508
1509        if let Some(error) = rpc_response.error {
1510            // Ignore "already imported" errors
1511            if error.code != -4 {
1512                return Err(AmpError::rpc(format!(
1513                    "RPC error importing address: {} (code: {})",
1514                    error.message, error.code
1515                )));
1516            }
1517        }
1518
1519        tracing::debug!(
1520            "Successfully imported address {} into wallet {}",
1521            address,
1522            wallet_name
1523        );
1524        Ok(())
1525    }
1526
1527    /// Creates a raw transaction using a specific wallet context
1528    ///
1529    /// This method uses the wallet-specific RPC endpoint which is necessary
1530    /// for confidential transactions that require wallet context for blinding keys.
1531    ///
1532    /// # Arguments
1533    /// * `wallet_name` - Name of the wallet to use for transaction creation
1534    /// * `inputs` - Transaction inputs
1535    /// * `outputs` - Map of addresses to amounts
1536    /// * `assets` - Map of addresses to asset IDs
1537    ///
1538    /// # Returns
1539    /// Returns the raw transaction hex
1540    ///
1541    /// # Errors
1542    /// Returns an error if the RPC call fails
1543    #[allow(dead_code)]
1544    #[allow(clippy::cognitive_complexity)]
1545    async fn create_raw_transaction_with_wallet(
1546        &self,
1547        wallet_name: &str,
1548        inputs: Vec<TxInput>,
1549        outputs: std::collections::HashMap<String, f64>,
1550        assets: std::collections::HashMap<String, String>,
1551    ) -> Result<String, AmpError> {
1552        tracing::debug!(
1553            "Creating raw transaction with wallet {} - {} inputs and {} outputs",
1554            wallet_name,
1555            inputs.len(),
1556            outputs.len()
1557        );
1558
1559        // First load the wallet to ensure it's available
1560        self.load_wallet(wallet_name).await?;
1561
1562        // Elements RPC createrawtransaction expects outputs as an array of objects
1563        // Each output object should contain both address, amount, and asset
1564        let mut outputs_array = Vec::new();
1565
1566        for (address, amount) in &outputs {
1567            let asset_id = assets.get(address).ok_or_else(|| {
1568                AmpError::validation(format!("No asset ID found for address {address}"))
1569            })?;
1570
1571            // Convert amount to string with proper precision for Elements
1572            let amount_str = format!("{amount:.8}");
1573
1574            outputs_array.push(serde_json::json!({
1575                address.clone(): amount_str,
1576                "asset": asset_id
1577            }));
1578        }
1579
1580        let params = serde_json::json!([
1581            inputs,        // inputs as TxInput array
1582            outputs_array, // outputs as array of {address: amount, asset: id} objects
1583            0,             // locktime (0 = no locktime)
1584            false,         // replaceable (false = not replaceable)
1585        ]);
1586
1587        // Debug: Log the exact parameters being sent to createrawtransaction
1588        tracing::error!("createrawtransaction parameters (wallet-specific, corrected format):");
1589        tracing::error!("  wallet: {}", wallet_name);
1590        tracing::error!(
1591            "  inputs: {}",
1592            serde_json::to_string_pretty(&inputs).unwrap_or_default()
1593        );
1594        tracing::error!(
1595            "  outputs_array: {}",
1596            serde_json::to_string_pretty(&outputs_array).unwrap_or_default()
1597        );
1598
1599        // Use the wallet-specific RPC endpoint
1600        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
1601
1602        let request = RpcRequest {
1603            jsonrpc: "1.0".to_string(),
1604            id: "amp-client".to_string(),
1605            method: "createrawtransaction".to_string(),
1606            params,
1607        };
1608
1609        let response = self
1610            .client
1611            .post(&wallet_url)
1612            .basic_auth(&self.username, Some(&self.password))
1613            .json(&request)
1614            .send()
1615            .await
1616            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
1617
1618        if !response.status().is_success() {
1619            let status = response.status();
1620            let error_body = response
1621                .text()
1622                .await
1623                .unwrap_or_else(|_| "Unable to read error body".to_string());
1624            return Err(AmpError::rpc(format!(
1625                "RPC request failed with status: {status} - Body: {error_body}"
1626            )));
1627        }
1628
1629        let rpc_response: RpcResponse<String> = response
1630            .json()
1631            .await
1632            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
1633
1634        if let Some(error) = rpc_response.error {
1635            return Err(AmpError::rpc(format!(
1636                "RPC error creating raw transaction: {} (code: {})",
1637                error.message, error.code
1638            )));
1639        }
1640
1641        let raw_tx = rpc_response
1642            .result
1643            .ok_or_else(|| AmpError::rpc("No raw transaction returned".to_string()))?;
1644
1645        tracing::debug!(
1646            "Created raw transaction with wallet {}: {}",
1647            wallet_name,
1648            raw_tx
1649        );
1650        Ok(raw_tx)
1651    }
1652
1653    /// Imports an address into the wallet as watch-only
1654    ///
1655    /// # Arguments
1656    /// * `address` - The address to import
1657    /// * `label` - Optional label for the address
1658    /// * `rescan` - Whether to rescan the blockchain for transactions
1659    ///
1660    /// # Errors
1661    /// Returns an error if the RPC call fails
1662    ///
1663    /// # Examples
1664    /// ```no_run
1665    /// # use amp_rs::ElementsRpc;
1666    /// # #[tokio::main]
1667    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1668    /// let rpc = ElementsRpc::from_env()?;
1669    /// rpc.import_address("vjU8L4dKa1XyyVcPqKBbTgjT1tRC7qYp5VJGwndZSCFk4ntpWey1pQe6hcSGDMVurr9CsZ21EGsqGjWA", Some("test_address"), false).await?;
1670    /// # Ok(())
1671    /// # }
1672    /// ```
1673    pub async fn import_address(
1674        &self,
1675        address: &str,
1676        label: Option<&str>,
1677        rescan: bool,
1678    ) -> Result<(), AmpError> {
1679        tracing::debug!(
1680            "Importing address: {} with label: {:?}, rescan: {}",
1681            address,
1682            label,
1683            rescan
1684        );
1685
1686        let params = serde_json::json!([address, label.unwrap_or(""), rescan]);
1687
1688        // importaddress returns null on success
1689        let request = RpcRequest {
1690            jsonrpc: "1.0".to_string(),
1691            id: "amp-client".to_string(),
1692            method: "importaddress".to_string(),
1693            params,
1694        };
1695
1696        let response = self
1697            .client
1698            .post(&self.base_url)
1699            .basic_auth(&self.username, Some(&self.password))
1700            .json(&request)
1701            .send()
1702            .await
1703            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
1704
1705        if !response.status().is_success() {
1706            return Err(AmpError::rpc(format!(
1707                "RPC request failed with status: {}",
1708                response.status()
1709            )));
1710        }
1711
1712        let rpc_response: RpcResponse<serde_json::Value> = response
1713            .json()
1714            .await
1715            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
1716
1717        if let Some(error) = rpc_response.error {
1718            return Err(AmpError::rpc(format!(
1719                "RPC error {}: {}",
1720                error.code, error.message
1721            )));
1722        }
1723
1724        tracing::debug!("Successfully imported address: {}", address);
1725        Ok(())
1726    }
1727
1728    /// Creates or loads a wallet
1729    ///
1730    /// # Arguments
1731    /// * `wallet_name` - Name of the wallet to create or load
1732    /// * `disable_private_keys` - Whether to disable private keys (watch-only wallet)
1733    ///
1734    /// # Errors
1735    /// Returns an error if the RPC call fails
1736    ///
1737    /// # Examples
1738    /// ```no_run
1739    /// # use amp_rs::ElementsRpc;
1740    /// # #[tokio::main]
1741    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1742    /// let rpc = ElementsRpc::from_env()?;
1743    /// rpc.create_wallet("test_wallet", true).await?;
1744    /// # Ok(())
1745    /// # }
1746    /// ```
1747    pub async fn create_wallet(
1748        &self,
1749        wallet_name: &str,
1750        disable_private_keys: bool,
1751    ) -> Result<(), AmpError> {
1752        tracing::debug!(
1753            "Creating wallet: {} with disable_private_keys: {}",
1754            wallet_name,
1755            disable_private_keys
1756        );
1757
1758        let params = serde_json::json!([wallet_name, disable_private_keys]);
1759
1760        let request = RpcRequest {
1761            jsonrpc: "1.0".to_string(),
1762            id: "amp-client".to_string(),
1763            method: "createwallet".to_string(),
1764            params,
1765        };
1766
1767        let response = self
1768            .client
1769            .post(&self.base_url)
1770            .basic_auth(&self.username, Some(&self.password))
1771            .json(&request)
1772            .send()
1773            .await
1774            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
1775
1776        if !response.status().is_success() {
1777            return Err(AmpError::rpc(format!(
1778                "RPC request failed with status: {}",
1779                response.status()
1780            )));
1781        }
1782
1783        let rpc_response: RpcResponse<serde_json::Value> = response
1784            .json()
1785            .await
1786            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
1787
1788        if let Some(error) = rpc_response.error {
1789            // Ignore "wallet already exists" error
1790            if error.code != -4 {
1791                return Err(AmpError::rpc(format!(
1792                    "RPC error {}: {}",
1793                    error.code, error.message
1794                )));
1795            }
1796            tracing::debug!("Wallet {} already exists", wallet_name);
1797        } else {
1798            tracing::debug!("Successfully created wallet: {}", wallet_name);
1799        }
1800
1801        Ok(())
1802    }
1803
1804    /// Loads an existing wallet
1805    ///
1806    /// # Arguments
1807    /// * `wallet_name` - Name of the wallet to load
1808    ///
1809    /// # Errors
1810    /// Returns an error if the RPC call fails
1811    ///
1812    /// # Examples
1813    /// ```no_run
1814    /// # use amp_rs::ElementsRpc;
1815    /// # #[tokio::main]
1816    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1817    /// let rpc = ElementsRpc::from_env()?;
1818    /// rpc.load_wallet("test_wallet").await?;
1819    /// # Ok(())
1820    /// # }
1821    /// ```
1822    #[allow(clippy::cognitive_complexity)]
1823    pub async fn load_wallet(&self, wallet_name: &str) -> Result<(), AmpError> {
1824        tracing::debug!("Loading wallet: {}", wallet_name);
1825
1826        let params = serde_json::json!([wallet_name]);
1827
1828        let request = RpcRequest {
1829            jsonrpc: "1.0".to_string(),
1830            id: "amp-client".to_string(),
1831            method: "loadwallet".to_string(),
1832            params,
1833        };
1834
1835        let response = self
1836            .client
1837            .post(&self.base_url)
1838            .basic_auth(&self.username, Some(&self.password))
1839            .json(&request)
1840            .send()
1841            .await
1842            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
1843
1844        if !response.status().is_success() {
1845            let status = response.status();
1846            let error_body = response
1847                .text()
1848                .await
1849                .unwrap_or_else(|_| "Unable to read error body".to_string());
1850            tracing::debug!(
1851                "Load wallet failed with status: {} - Body: {}",
1852                status,
1853                error_body
1854            );
1855
1856            // For wallet loading, we want to be more permissive with errors
1857            // since the wallet might already be loaded
1858            if status == 500 && error_body.contains("already loaded") {
1859                tracing::debug!(
1860                    "Wallet {} appears to already be loaded (500 error)",
1861                    wallet_name
1862                );
1863                return Ok(());
1864            }
1865
1866            return Err(AmpError::rpc(format!(
1867                "RPC request failed with status: {status} - Body: {error_body}"
1868            )));
1869        }
1870
1871        let rpc_response: RpcResponse<serde_json::Value> = response
1872            .json()
1873            .await
1874            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
1875
1876        if let Some(error) = rpc_response.error {
1877            // Ignore "wallet already loaded" error
1878            if error.code != -35 {
1879                return Err(AmpError::rpc(format!(
1880                    "RPC error {}: {}",
1881                    error.code, error.message
1882                )));
1883            }
1884            tracing::debug!("Wallet {} already loaded", wallet_name);
1885        } else {
1886            tracing::debug!("Successfully loaded wallet: {}", wallet_name);
1887        }
1888
1889        Ok(())
1890    }
1891
1892    /// Lists all available wallets
1893    ///
1894    /// # Errors
1895    /// Returns an error if the RPC call fails
1896    ///
1897    /// # Examples
1898    /// ```no_run
1899    /// # use amp_rs::ElementsRpc;
1900    /// # #[tokio::main]
1901    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1902    /// let rpc = ElementsRpc::from_env()?;
1903    /// let wallets = rpc.list_wallets().await?;
1904    /// println!("Available wallets: {:?}", wallets);
1905    /// # Ok(())
1906    /// # }
1907    /// ```
1908    pub async fn list_wallets(&self) -> Result<Vec<String>, AmpError> {
1909        tracing::debug!("Listing available wallets");
1910
1911        let params = serde_json::json!([]);
1912
1913        let wallets: Vec<String> = self
1914            .rpc_call("listwallets", params)
1915            .await
1916            .map_err(|e| e.with_context("Failed to list wallets"))?;
1917
1918        tracing::debug!("Found {} wallets", wallets.len());
1919        Ok(wallets)
1920    }
1921
1922    /// Sets up a watch-only wallet with the given address
1923    ///
1924    /// This is a convenience method that creates a watch-only wallet and imports the address
1925    ///
1926    /// # Arguments
1927    /// * `wallet_name` - Name of the wallet to create
1928    /// * `address` - Address to import as watch-only
1929    /// * `label` - Optional label for the address
1930    ///
1931    /// # Errors
1932    /// Returns an error if wallet creation or address import fails
1933    ///
1934    /// # Examples
1935    /// ```no_run
1936    /// # use amp_rs::ElementsRpc;
1937    /// # #[tokio::main]
1938    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1939    /// let rpc = ElementsRpc::from_env()?;
1940    /// rpc.setup_watch_only_wallet("test_wallet", "vjU8L4dKa1XyyVcPqKBbTgjT1tRC7qYp5VJGwndZSCFk4ntpWey1pQe6hcSGDMVurr9CsZ21EGsqGjWA", Some("treasury")).await?;
1941    /// # Ok(())
1942    /// # }
1943    /// ```
1944    #[allow(clippy::cognitive_complexity)]
1945    pub async fn setup_watch_only_wallet(
1946        &self,
1947        wallet_name: &str,
1948        address: &str,
1949        label: Option<&str>,
1950    ) -> Result<(), AmpError> {
1951        tracing::info!(
1952            "Setting up watch-only wallet '{}' with address: {}",
1953            wallet_name,
1954            address
1955        );
1956
1957        // Try the full wallet setup approach first
1958        match self
1959            .setup_wallet_with_address(wallet_name, address, label)
1960            .await
1961        {
1962            Ok(()) => {
1963                tracing::info!(
1964                    "Successfully set up watch-only wallet '{}' with address: {}",
1965                    wallet_name,
1966                    address
1967                );
1968                return Ok(());
1969            }
1970            Err(e) => {
1971                tracing::warn!(
1972                    "Full wallet setup failed: {}, trying direct address import",
1973                    e
1974                );
1975            }
1976        }
1977
1978        // Fallback: Try to import the address directly without wallet operations
1979        match self.import_address_direct(address, label).await {
1980            Ok(()) => {
1981                tracing::info!("Successfully imported address directly: {}", address);
1982                Ok(())
1983            }
1984            Err(e) => {
1985                tracing::error!(
1986                    "Both wallet setup and direct import failed for address: {}",
1987                    address
1988                );
1989                Err(AmpError::rpc(format!(
1990                    "Failed to set up watch-only wallet or import address: wallet setup error: {e}, direct import error: {e}"
1991                )))
1992            }
1993        }
1994    }
1995
1996    /// Attempts to set up a wallet with address using the standard approach
1997    async fn setup_wallet_with_address(
1998        &self,
1999        wallet_name: &str,
2000        address: &str,
2001        label: Option<&str>,
2002    ) -> Result<(), AmpError> {
2003        // Try to create the wallet (will ignore if it already exists)
2004        self.create_wallet(wallet_name, true).await?;
2005
2006        // Try to load the wallet (will ignore if already loaded)
2007        self.load_wallet(wallet_name).await?;
2008
2009        // Import the address without rescanning (for faster setup)
2010        self.import_address(address, label, false).await?;
2011
2012        Ok(())
2013    }
2014
2015    /// Attempts to import an address directly without wallet operations
2016    async fn import_address_direct(
2017        &self,
2018        address: &str,
2019        label: Option<&str>,
2020    ) -> Result<(), AmpError> {
2021        tracing::debug!("Attempting direct address import for: {}", address);
2022
2023        // Try to import the address directly (this might work even if wallet operations fail)
2024        self.import_address(address, label, false).await
2025    }
2026
2027    /// Broadcasts a signed raw transaction to the network
2028    ///
2029    /// # Arguments
2030    /// * `hex` - The signed transaction in hexadecimal format
2031    ///
2032    /// # Errors
2033    /// Returns an error if the RPC call fails or transaction broadcast fails
2034    ///
2035    /// # Examples
2036    /// ```no_run
2037    /// # use amp_rs::ElementsRpc;
2038    /// # #[tokio::main]
2039    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2040    /// let rpc = ElementsRpc::from_env()?;
2041    /// let signed_tx_hex = "0200000000..."; // Signed transaction hex
2042    /// let txid = rpc.send_raw_transaction(signed_tx_hex).await?;
2043    /// println!("Transaction broadcast with ID: {}", txid);
2044    /// # Ok(())
2045    /// # }
2046    /// ```
2047    pub async fn send_raw_transaction(&self, hex: &str) -> Result<String, AmpError> {
2048        tracing::debug!(
2049            "Broadcasting raw transaction: {}",
2050            &hex[..std::cmp::min(hex.len(), 64)]
2051        );
2052
2053        let params = serde_json::json!([hex]);
2054
2055        let txid: String = self
2056            .rpc_call("sendrawtransaction", params)
2057            .await
2058            .map_err(|e| {
2059                tracing::error!("Raw transaction broadcast failed: {}", e);
2060                tracing::error!("Transaction hex (first 200 chars): {}", &hex[..std::cmp::min(hex.len(), 200)]);
2061
2062                // Provide specific guidance for blinding-related errors
2063                if e.to_string().contains("bad-txns-in-ne-out") || e.to_string().contains("value in != value out") {
2064                    AmpError::rpc(format!(
2065                        "Transaction broadcast failed due to confidential transaction blinding error. \
2066                        This indicates that the blinding factors don't balance properly. \
2067                        Possible solutions:\n\
2068                        1. Ensure all addresses have proper blinding keys in the wallet\n\
2069                        2. Verify that blindrawtransaction was called before signing\n\
2070                        3. Check that UTXO blinding factors match between Elements and LWK\n\
2071                        4. Try using unconfidential addresses for testing\n\
2072                        Original error: {e}"
2073                    ))
2074                } else {
2075                    e.with_context("Failed to broadcast raw transaction")
2076                }
2077            })?;
2078
2079        tracing::info!("Successfully broadcast transaction with ID: {}", txid);
2080        Ok(txid)
2081    }
2082
2083    /// Retrieves detailed information about a transaction
2084    ///
2085    /// # Arguments
2086    /// * `txid` - The transaction ID to retrieve
2087    ///
2088    /// # Errors
2089    /// Returns an error if the RPC call fails or transaction is not found
2090    ///
2091    /// # Examples
2092    /// ```no_run
2093    /// # use amp_rs::ElementsRpc;
2094    /// # #[tokio::main]
2095    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2096    /// let rpc = ElementsRpc::from_env()?;
2097    /// let tx_detail = rpc.get_transaction("abc123...").await?;
2098    /// println!("Transaction has {} confirmations", tx_detail.confirmations);
2099    /// # Ok(())
2100    /// # }
2101    /// ```
2102    pub async fn get_transaction(&self, txid: &str) -> Result<TransactionDetail, AmpError> {
2103        tracing::debug!("Retrieving transaction details for: {}", txid);
2104
2105        let params = serde_json::json!([txid, true]); // true for verbose output
2106
2107        let tx_detail: TransactionDetail = self
2108            .rpc_call("gettransaction", params)
2109            .await
2110            .map_err(|e| e.with_context(format!("Failed to get transaction details for {txid}")))?;
2111
2112        tracing::debug!(
2113            "Retrieved transaction {} with {} confirmations",
2114            txid,
2115            tx_detail.confirmations
2116        );
2117
2118        Ok(tx_detail)
2119    }
2120
2121    /// Sends multiple outputs to multiple addresses using Elements' sendmany RPC
2122    ///
2123    /// This method uses Elements' built-in sendmany command which properly handles
2124    /// confidential transactions and blinding. This is the recommended approach for
2125    /// asset distribution as it avoids manual transaction construction issues.
2126    ///
2127    /// # Arguments
2128    /// * `wallet_name` - Name of the Elements wallet to use
2129    /// * `address_amounts` - Map of addresses to amounts to send
2130    /// * `asset_amounts` - Map of addresses to asset IDs for each output
2131    /// * `min_conf` - Minimum confirmations for inputs (default: 1)
2132    /// * `comment` - Optional transaction comment
2133    /// * `subtract_fee_from` - Optional addresses to subtract fees from
2134    /// * `replaceable` - Whether transaction is replaceable (default: false)
2135    /// * `conf_target` - Confirmation target for fee estimation (default: 1)
2136    /// * `estimate_mode` - Fee estimation mode (default: "UNSET")
2137    ///
2138    /// # Returns
2139    /// Returns the transaction ID of the sent transaction
2140    ///
2141    /// # Errors
2142    /// Returns an error if the RPC call fails or transaction creation fails
2143    ///
2144    /// # Examples
2145    /// ```no_run
2146    /// # use amp_rs::ElementsRpc;
2147    /// # use std::collections::HashMap;
2148    /// # #[tokio::main]
2149    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2150    /// let rpc = ElementsRpc::from_env()?;
2151    ///
2152    /// let mut address_amounts = HashMap::new();
2153    /// address_amounts.insert("address1".to_string(), 100.0);
2154    /// address_amounts.insert("address2".to_string(), 50.0);
2155    ///
2156    /// let mut asset_amounts = HashMap::new();
2157    /// asset_amounts.insert("address1".to_string(), "asset_id_hex".to_string());
2158    /// asset_amounts.insert("address2".to_string(), "asset_id_hex".to_string());
2159    ///
2160    /// let txid = rpc.sendmany("wallet_name", address_amounts, asset_amounts, None, None, None, None, None, None).await?;
2161    /// println!("Transaction sent with ID: {}", txid);
2162    /// # Ok(())
2163    /// # }
2164    /// ```
2165    #[allow(clippy::too_many_arguments, clippy::cognitive_complexity)]
2166    pub async fn sendmany(
2167        &self,
2168        wallet_name: &str,
2169        address_amounts: std::collections::HashMap<String, f64>,
2170        asset_amounts: std::collections::HashMap<String, String>,
2171        min_conf: Option<u32>,
2172        comment: Option<&str>,
2173        subtract_fee_from: Option<Vec<String>>,
2174        replaceable: Option<bool>,
2175        conf_target: Option<u32>,
2176        estimate_mode: Option<&str>,
2177    ) -> Result<String, AmpError> {
2178        tracing::debug!(
2179            "Sending to {} addresses using sendmany for wallet {}",
2180            address_amounts.len(),
2181            wallet_name
2182        );
2183
2184        // First load the wallet to ensure it's available
2185        self.load_wallet(wallet_name).await?;
2186
2187        // Elements sendmany parameters:
2188        // 1. dummy (empty string for compatibility)
2189        // 2. amounts (map of address -> amount)
2190        // 3. minconf (minimum confirmations, default 1)
2191        // 4. comment (optional comment)
2192        // 5. subtractfeefrom (array of addresses to subtract fee from)
2193        // 6. replaceable (boolean, default false)
2194        // 7. conf_target (confirmation target for fee estimation)
2195        // 8. estimate_mode (fee estimation mode)
2196        // 9. assetlabel (map of address -> asset_id for multi-asset sends)
2197        let params = serde_json::json!([
2198            "",                                    // dummy (required for compatibility)
2199            address_amounts,                       // amounts map
2200            min_conf.unwrap_or(1),                 // minconf
2201            comment.unwrap_or(""),                 // comment
2202            subtract_fee_from.unwrap_or_default(), // subtractfeefrom
2203            replaceable.unwrap_or(false),          // replaceable
2204            conf_target.unwrap_or(1),              // conf_target
2205            estimate_mode.unwrap_or("UNSET"),      // estimate_mode
2206            asset_amounts                          // assetlabel (asset map)
2207        ]);
2208
2209        // Use the wallet-specific RPC endpoint
2210        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
2211
2212        let request = RpcRequest {
2213            jsonrpc: "1.0".to_string(),
2214            id: "amp-client".to_string(),
2215            method: "sendmany".to_string(),
2216            params,
2217        };
2218
2219        tracing::debug!("Sendmany request parameters:");
2220        tracing::debug!("  wallet: {}", wallet_name);
2221        tracing::debug!("  address_amounts: {:?}", address_amounts);
2222        tracing::debug!("  asset_amounts: {:?}", asset_amounts);
2223
2224        let response = self
2225            .client
2226            .post(&wallet_url)
2227            .basic_auth(&self.username, Some(&self.password))
2228            .json(&request)
2229            .send()
2230            .await
2231            .map_err(|e| AmpError::rpc(format!("Failed to send sendmany RPC request: {e}")))?;
2232
2233        if !response.status().is_success() {
2234            let status = response.status();
2235            let error_body = response
2236                .text()
2237                .await
2238                .unwrap_or_else(|_| "Unable to read error body".to_string());
2239            return Err(AmpError::rpc(format!(
2240                "Sendmany RPC request failed with status: {status} - Body: {error_body}"
2241            )));
2242        }
2243
2244        let rpc_response: RpcResponse<String> = response
2245            .json()
2246            .await
2247            .map_err(|e| AmpError::rpc(format!("Failed to parse sendmany RPC response: {e}")))?;
2248
2249        if let Some(error) = rpc_response.error {
2250            return Err(AmpError::rpc(format!(
2251                "Sendmany RPC error: {} (code: {})",
2252                error.message, error.code
2253            )));
2254        }
2255
2256        let txid = rpc_response.result.unwrap_or_default();
2257        tracing::info!("Successfully sent transaction with sendmany: {}", txid);
2258        Ok(txid)
2259    }
2260
2261    /// Waits for blockchain confirmations with configurable timeout
2262    ///
2263    /// This method polls the blockchain every 15 seconds to check for transaction confirmations.
2264    /// It waits for a minimum number of confirmations (default 2) before returning successfully.
2265    /// The method includes a configurable timeout to prevent indefinite waiting.
2266    ///
2267    /// # Arguments
2268    /// * `txid` - The transaction ID to monitor for confirmations
2269    /// * `min_confirmations` - Minimum number of confirmations required (default: 2)
2270    /// * `timeout_minutes` - Timeout in minutes (default: 10)
2271    ///
2272    /// # Returns
2273    /// Returns the final `TransactionDetail` when sufficient confirmations are reached
2274    ///
2275    /// # Errors
2276    /// Returns `AmpError::Timeout` if the timeout is exceeded before confirmations are received
2277    /// Returns `AmpError::Rpc` if there are issues communicating with the Elements node
2278    ///
2279    /// # Examples
2280    /// ```no_run
2281    /// # use amp_rs::ElementsRpc;
2282    /// # #[tokio::main]
2283    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2284    /// let rpc = ElementsRpc::from_env()?;
2285    /// let tx_detail = rpc.wait_for_confirmations("abc123...", Some(2), Some(10)).await?;
2286    /// println!("Transaction confirmed with {} confirmations", tx_detail.confirmations);
2287    /// # Ok(())
2288    /// # }
2289    /// ```
2290    pub async fn wait_for_confirmations(
2291        &self,
2292        txid: &str,
2293        min_confirmations: Option<u32>,
2294        timeout_minutes: Option<u64>,
2295    ) -> Result<TransactionDetail, AmpError> {
2296        self.wait_for_confirmations_with_interval(txid, min_confirmations, timeout_minutes, None)
2297            .await
2298    }
2299
2300    /// Internal method for waiting for confirmations with configurable poll interval
2301    /// This is primarily used for testing to avoid long waits
2302    ///
2303    /// # Errors
2304    ///
2305    /// Returns an error if:
2306    /// - The timeout is exceeded before confirmations are received
2307    /// - There are issues communicating with the Elements node
2308    /// - The transaction cannot be found or is invalid
2309    #[allow(clippy::cognitive_complexity)]
2310    pub async fn wait_for_confirmations_with_interval(
2311        &self,
2312        txid: &str,
2313        min_confirmations: Option<u32>,
2314        timeout_minutes: Option<u64>,
2315        poll_interval_secs: Option<u64>,
2316    ) -> Result<TransactionDetail, AmpError> {
2317        let min_confirmations = min_confirmations.unwrap_or(2);
2318        let timeout_minutes = timeout_minutes.unwrap_or(10);
2319        let timeout_duration = if timeout_minutes == 0 {
2320            std::time::Duration::from_secs(3) // Minimum 3 seconds for testing
2321        } else {
2322            std::time::Duration::from_secs(timeout_minutes * 60)
2323        };
2324        let poll_interval = std::time::Duration::from_secs(poll_interval_secs.unwrap_or(15));
2325
2326        tracing::info!(
2327            "Starting confirmation monitoring for transaction {} (min_confirmations: {}, timeout: {} minutes)",
2328            txid,
2329            min_confirmations,
2330            timeout_minutes
2331        );
2332
2333        let start_time = std::time::Instant::now();
2334
2335        loop {
2336            // Check if we've exceeded the timeout
2337            if start_time.elapsed() >= timeout_duration {
2338                let error_msg = format!(
2339                    "Timeout waiting for confirmations after {timeout_minutes} minutes. Transaction ID: {txid}. \
2340                    You can retry confirmation by calling the confirmation API with this txid."
2341                );
2342                tracing::error!("{}", error_msg);
2343                return Err(AmpError::Timeout(error_msg));
2344            }
2345
2346            // Get current transaction details
2347            match self.get_transaction(txid).await {
2348                Ok(tx_detail) => {
2349                    tracing::debug!(
2350                        "Transaction {} has {} confirmations (need {})",
2351                        txid,
2352                        tx_detail.confirmations,
2353                        min_confirmations
2354                    );
2355
2356                    if tx_detail.confirmations >= min_confirmations {
2357                        tracing::info!(
2358                            "Transaction {} confirmed with {} confirmations",
2359                            txid,
2360                            tx_detail.confirmations
2361                        );
2362                        return Ok(tx_detail);
2363                    }
2364
2365                    // Log progress every few polls to avoid spam
2366                    if start_time.elapsed().as_secs() % 60 < 15 {
2367                        tracing::info!(
2368                            "Waiting for confirmations: {}/{} (elapsed: {}s)",
2369                            tx_detail.confirmations,
2370                            min_confirmations,
2371                            start_time.elapsed().as_secs()
2372                        );
2373                    }
2374                }
2375                Err(e) => {
2376                    tracing::warn!(
2377                        "Failed to get transaction details for {}: {}. Retrying in {} seconds...",
2378                        txid,
2379                        e,
2380                        poll_interval.as_secs()
2381                    );
2382                    // Continue polling even if individual calls fail, as the transaction
2383                    // might not be visible immediately after broadcasting
2384                }
2385            }
2386
2387            // Wait before next poll
2388            tokio::time::sleep(poll_interval).await;
2389        }
2390    }
2391
2392    /// Selects appropriate UTXOs to cover the required amount plus fees
2393    ///
2394    /// This method implements a simple UTXO selection algorithm that:
2395    /// 1. Filters UTXOs by asset ID and spendability
2396    /// 2. Sorts UTXOs by amount (largest first) for efficiency
2397    /// 3. Selects UTXOs until the target amount plus estimated fees is covered
2398    ///
2399    /// # Arguments
2400    /// * `asset_id` - The asset ID to select UTXOs for
2401    /// * `target_amount` - The total amount needed for distribution
2402    /// * `estimated_fee` - Estimated transaction fee in the same asset
2403    ///
2404    /// # Returns
2405    /// Returns a tuple of (`selected_utxos`, `total_selected_amount`)
2406    ///
2407    /// # Errors
2408    /// Returns an error if insufficient UTXOs are available or RPC calls fail
2409    ///
2410    /// # Examples
2411    /// ```no_run
2412    /// # use amp_rs::ElementsRpc;
2413    /// # #[tokio::main]
2414    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2415    /// let rpc = ElementsRpc::from_env()?;
2416    /// let (selected_utxos, total_amount) = rpc.select_utxos_for_amount(
2417    ///     "wallet_name",
2418    ///     "asset_id_hex",
2419    ///     150.0,
2420    ///     0.001
2421    /// ).await?;
2422    /// println!("Selected {} UTXOs totaling {}", selected_utxos.len(), total_amount);
2423    /// # Ok(())
2424    /// # }
2425    /// ```
2426    pub async fn select_utxos_for_amount(
2427        &self,
2428        wallet_name: &str,
2429        asset_id: &str,
2430        target_amount: f64,
2431        estimated_fee: f64,
2432    ) -> Result<(Vec<Unspent>, f64), AmpError> {
2433        tracing::debug!(
2434            "Selecting UTXOs for asset {} from wallet {} - target: {}, fee: {}",
2435            asset_id,
2436            wallet_name,
2437            target_amount,
2438            estimated_fee
2439        );
2440
2441        // Get all UTXOs for this asset from the specified wallet
2442        let mut utxos = self
2443            .list_unspent_for_wallet(wallet_name, Some(asset_id))
2444            .await?;
2445
2446        // Filter for spendable UTXOs only
2447        utxos.retain(|utxo| utxo.spendable && utxo.asset == asset_id);
2448
2449        if utxos.is_empty() {
2450            return Err(AmpError::validation(format!(
2451                "No spendable UTXOs found for asset {asset_id}. \
2452                This typically means:\n\
2453                1. The treasury address is not imported in the Elements node as a watch-only address\n\
2454                2. The asset issuance transaction hasn't been confirmed yet\n\
2455                3. The UTXOs have already been spent\n\
2456                \n\
2457                To fix this:\n\
2458                - Ensure the treasury address is imported: `elements-cli importaddress <treasury_address> treasury false`\n\
2459                - Wait for the asset issuance transaction to be confirmed\n\
2460                - Check that the treasury address matches the one used for asset issuance"
2461            )));
2462        }
2463
2464        // Sort UTXOs by amount (largest first) for efficient selection
2465        utxos.sort_by(|a, b| {
2466            b.amount
2467                .partial_cmp(&a.amount)
2468                .unwrap_or(std::cmp::Ordering::Equal)
2469        });
2470
2471        let required_amount = target_amount + estimated_fee;
2472        let mut selected_utxos = Vec::new();
2473        let mut total_selected = 0.0;
2474
2475        // Select UTXOs until we have enough to cover the required amount
2476        for utxo in utxos {
2477            selected_utxos.push(utxo.clone());
2478            total_selected += utxo.amount;
2479
2480            if total_selected >= required_amount {
2481                break;
2482            }
2483        }
2484
2485        // Check if we have sufficient funds
2486        if total_selected < required_amount {
2487            return Err(AmpError::validation(format!(
2488                "Insufficient UTXOs: need {required_amount}, have {total_selected} (target: {target_amount}, fee: {estimated_fee})"
2489            )));
2490        }
2491
2492        tracing::info!(
2493            "Selected {} UTXOs totaling {} for target {} + fee {}",
2494            selected_utxos.len(),
2495            total_selected,
2496            target_amount,
2497            estimated_fee
2498        );
2499
2500        Ok((selected_utxos, total_selected))
2501    }
2502
2503    /// Builds a raw transaction for asset distribution with proper change handling
2504    ///
2505    /// This method orchestrates the complete transaction building process:
2506    /// 1. Selects appropriate UTXOs using `select_utxos_for_amount`
2507    /// 2. Creates transaction inputs from selected UTXOs
2508    /// 3. Creates outputs for distribution addresses
2509    /// 4. Calculates and creates change output if necessary
2510    /// 5. Builds the raw transaction using `create_raw_transaction`
2511    ///
2512    /// # Arguments
2513    /// * `asset_id` - The asset ID being distributed
2514    /// * `address_amounts` - Map of recipient addresses to amounts
2515    /// * `change_address` - Address to send change to (if any)
2516    /// * `estimated_fee` - Estimated transaction fee
2517    ///
2518    /// # Returns
2519    /// Returns a tuple of (`raw_transaction_hex`, `selected_utxos`, `change_amount`)
2520    ///
2521    /// # Errors
2522    /// Returns an error if UTXO selection fails or transaction building fails
2523    ///
2524    /// # Examples
2525    /// ```no_run
2526    /// # use amp_rs::ElementsRpc;
2527    /// # use std::collections::HashMap;
2528    /// # #[tokio::main]
2529    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2530    /// let rpc = ElementsRpc::from_env()?;
2531    /// let mut address_amounts = HashMap::new();
2532    /// address_amounts.insert("address1".to_string(), 100.0);
2533    /// address_amounts.insert("address2".to_string(), 50.0);
2534    ///
2535    /// let (raw_tx, utxos, change) = rpc.build_distribution_transaction(
2536    ///     "wallet_name",
2537    ///     "asset_id_hex",
2538    ///     address_amounts,
2539    ///     "change_address",
2540    ///     0.001
2541    /// ).await?;
2542    /// println!("Built transaction with {} inputs, change: {}", utxos.len(), change);
2543    /// # Ok(())
2544    /// # }
2545    /// ```
2546    #[allow(clippy::cognitive_complexity)]
2547    #[allow(clippy::too_many_lines)]
2548    pub async fn build_distribution_transaction(
2549        &self,
2550        wallet_name: &str,
2551        asset_id: &str,
2552        address_amounts: std::collections::HashMap<String, f64>,
2553        change_address: &str,
2554        _estimated_fee: f64,
2555    ) -> Result<(String, Vec<Unspent>, f64), AmpError> {
2556        const DUST_THRESHOLD: f64 = 0.00001;
2557        const LBTC_ASSET_ID: &str =
2558            "144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49"; // L-BTC on Liquid testnet
2559
2560        tracing::debug!(
2561            "Building distribution transaction for asset {} with {} outputs",
2562            asset_id,
2563            address_amounts.len()
2564        );
2565
2566        // Calculate total distribution amount
2567        let total_distribution: f64 = address_amounts.values().sum();
2568
2569        if total_distribution <= 0.0 {
2570            return Err(AmpError::validation(
2571                "Total distribution amount must be greater than zero".to_string(),
2572            ));
2573        }
2574
2575        // Select UTXOs to cover the distribution (custom asset)
2576        let (selected_asset_utxos, total_selected) = self
2577            .select_utxos_for_amount(wallet_name, asset_id, total_distribution, 0.0)
2578            .await?;
2579
2580        // Also select L-BTC UTXOs for transaction fees
2581        // Elements requires L-BTC inputs for fees even when distributing custom assets
2582        let min_lbtc_fee = 0.00001; // Minimum L-BTC needed for fees
2583        let (selected_lbtc_utxos, lbtc_total) = match self
2584            .select_utxos_for_amount(wallet_name, LBTC_ASSET_ID, 0.0, min_lbtc_fee)
2585            .await
2586        {
2587            Ok((utxos, total)) => {
2588                tracing::info!(
2589                    "Selected {} L-BTC UTXOs totaling {} for fees",
2590                    utxos.len(),
2591                    total
2592                );
2593                (utxos, total)
2594            }
2595            Err(e) => {
2596                tracing::warn!(
2597                    "Could not select L-BTC UTXOs for fees: {}. Transaction may fail.",
2598                    e
2599                );
2600                (Vec::new(), 0.0)
2601            }
2602        };
2603
2604        // Combine custom asset UTXOs and L-BTC UTXOs
2605        let mut all_utxos = selected_asset_utxos.clone();
2606        all_utxos.extend(selected_lbtc_utxos.clone());
2607
2608        if selected_lbtc_utxos.is_empty() {
2609            tracing::warn!(
2610                "No L-BTC UTXOs selected for fees. Transaction may fail during broadcast."
2611            );
2612        } else {
2613            tracing::info!(
2614                "Transaction includes {} custom asset UTXOs and {} L-BTC UTXOs for fees",
2615                selected_asset_utxos.len(),
2616                selected_lbtc_utxos.len()
2617            );
2618        }
2619
2620        // Create transaction inputs from all selected UTXOs
2621        let inputs: Vec<TxInput> = all_utxos
2622            .iter()
2623            .map(|utxo| TxInput {
2624                txid: utxo.txid.clone(),
2625                vout: utxo.vout,
2626                sequence: None, // Use default sequence
2627            })
2628            .collect();
2629
2630        // Create outputs for distribution (custom asset)
2631        // We need to track outputs as a vector since we may have multiple outputs to the same address
2632        // (e.g., custom asset change + L-BTC change to the same change address)
2633        let mut output_list = Vec::new();
2634
2635        // Add distribution outputs (custom asset)
2636        for (address, amount) in &address_amounts {
2637            output_list.push((address.clone(), *amount, asset_id.to_string()));
2638        }
2639
2640        // Calculate change amount for custom asset (total selected - distribution)
2641        let asset_change_amount = total_selected - total_distribution;
2642
2643        // Add asset change output if there's a significant amount left
2644        if asset_change_amount > DUST_THRESHOLD {
2645            output_list.push((
2646                change_address.to_string(),
2647                asset_change_amount,
2648                asset_id.to_string(),
2649            ));
2650
2651            tracing::debug!(
2652                "Adding asset change output: {} {} to address {}",
2653                asset_change_amount,
2654                asset_id,
2655                change_address
2656            );
2657        } else if asset_change_amount > 0.0 {
2658            tracing::warn!(
2659                "Asset change amount {} is below dust threshold {}, will be lost",
2660                asset_change_amount,
2661                DUST_THRESHOLD
2662            );
2663        }
2664
2665        // Handle L-BTC change if we selected L-BTC UTXOs for fees
2666        // In Elements, the fee is implicit - it's the difference between L-BTC inputs and outputs
2667        // We should NOT subtract the fee from outputs; Elements calculates it automatically
2668        if !selected_lbtc_utxos.is_empty() {
2669            tracing::debug!(
2670                "L-BTC input total: {}, minimum fee needed: {}",
2671                lbtc_total,
2672                min_lbtc_fee
2673            );
2674
2675            // Check if we have enough L-BTC for the minimum fee
2676            if lbtc_total < min_lbtc_fee {
2677                return Err(AmpError::validation(format!(
2678                    "Insufficient L-BTC for fees: have {lbtc_total}, need at least {min_lbtc_fee}"
2679                )));
2680            }
2681
2682            // For now, let's try NOT adding any L-BTC change output
2683            // and let Elements handle the fee automatically from the input/output difference
2684            tracing::info!(
2685                "Using L-BTC input {} for fees - no explicit L-BTC change output (Elements will handle fee automatically)",
2686                lbtc_total
2687            );
2688
2689            // Note: If this approach works, the entire L-BTC input will become the fee
2690            // If we need change, we'll need to figure out the correct way to handle it
2691        }
2692
2693        // For confidential addresses, we need to import them into the wallet first
2694        // so Elements knows about the blinding keys
2695        for address in address_amounts.keys() {
2696            if address.starts_with('v') {
2697                // Confidential address
2698                tracing::debug!("Importing confidential address into wallet: {}", address);
2699                if let Err(e) = self
2700                    .import_address_to_wallet(wallet_name, address, None, false)
2701                    .await
2702                {
2703                    tracing::warn!("Failed to import confidential address {}: {}", address, e);
2704                    // Continue anyway - the address might already be imported
2705                }
2706            }
2707        }
2708
2709        // Build the raw transaction using wallet-specific endpoint for confidential transactions
2710        // For confidential transactions, we need to use blindrawtransaction to properly handle blinding
2711        let raw_transaction = self
2712            .create_raw_transaction_with_outputs(wallet_name, inputs, output_list)
2713            .await
2714            .map_err(|e| {
2715                // Provide more helpful error message for the common L-BTC fee issue
2716                if e.to_string().contains("bad-txns-in-ne-out") || e.to_string().contains("value in != value out") {
2717                    AmpError::validation(format!(
2718                        "Transaction failed due to confidential transaction blinding mismatch. \
2719                        This occurs when Elements creates blinding factors that don't match LWK's expectations. \
2720                        To fix this:\n\
2721                        1. Ensure the wallet has proper blinding keys for all addresses\n\
2722                        2. Use blindrawtransaction before signing\n\
2723                        3. Verify UTXO blinding factors match between Elements and LWK\n\
2724                        4. Original error: {e}"
2725                    ))
2726                } else {
2727                    e.with_context("Failed to build distribution transaction")
2728                }
2729            })?;
2730
2731        // For confidential transactions, we need to blind the transaction properly
2732        // This ensures the blinding factors are compatible with LWK signing
2733        tracing::debug!("Blinding raw transaction for confidential asset distribution");
2734        let blinded_transaction = self
2735            .blind_raw_transaction(wallet_name, &raw_transaction)
2736            .await
2737            .map_err(|e| {
2738                tracing::warn!(
2739                    "Failed to blind transaction, proceeding with unblinded: {}",
2740                    e
2741                );
2742                // If blinding fails, we'll try to proceed with the unblinded transaction
2743                // This might work for some cases but could fail during broadcast
2744                e.with_context("Transaction blinding failed")
2745            })
2746            .unwrap_or_else(|_| {
2747                tracing::warn!("Using unblinded transaction - this may cause broadcast failures");
2748                raw_transaction.clone()
2749            });
2750
2751        tracing::info!(
2752            "Built distribution transaction: {} inputs, {} outputs, asset change: {}",
2753            all_utxos.len(),
2754            address_amounts.len() + usize::from(asset_change_amount > DUST_THRESHOLD),
2755            if asset_change_amount > DUST_THRESHOLD {
2756                asset_change_amount
2757            } else {
2758                0.0
2759            }
2760        );
2761
2762        Ok((blinded_transaction, all_utxos, asset_change_amount))
2763    }
2764
2765    /// Creates a raw transaction with multiple outputs that can handle multiple assets to the same address
2766    ///
2767    /// This method is similar to `create_raw_transaction_with_wallet` but handles the case where
2768    /// multiple outputs with different assets need to go to the same address (e.g., asset change + L-BTC change).
2769    ///
2770    /// # Arguments
2771    /// * `wallet_name` - Name of the Elements wallet to use
2772    /// * `inputs` - Vector of transaction inputs
2773    /// * `outputs` - Vector of (address, amount, `asset_id`) tuples
2774    ///
2775    /// # Returns
2776    /// Returns the raw transaction hex string
2777    #[allow(clippy::cognitive_complexity)]
2778    async fn create_raw_transaction_with_outputs(
2779        &self,
2780        wallet_name: &str,
2781        inputs: Vec<TxInput>,
2782        outputs: Vec<(String, f64, String)>, // (address, amount, asset_id)
2783    ) -> Result<String, AmpError> {
2784        tracing::debug!(
2785            "Creating raw transaction with wallet {} - {} inputs and {} outputs",
2786            wallet_name,
2787            inputs.len(),
2788            outputs.len()
2789        );
2790
2791        // First load the wallet to ensure it's available
2792        self.load_wallet(wallet_name).await?;
2793
2794        // Elements RPC createrawtransaction expects outputs as an array of objects
2795        // Each output object should contain both address, amount, and asset
2796        let mut outputs_array = Vec::new();
2797
2798        for (address, amount, asset_id) in &outputs {
2799            // Convert amount to string with proper precision for Elements
2800            let amount_str = format!("{amount:.8}");
2801
2802            outputs_array.push(serde_json::json!({
2803                address.clone(): amount_str,
2804                "asset": asset_id
2805            }));
2806        }
2807
2808        let params = serde_json::json!([
2809            inputs,        // inputs as TxInput array
2810            outputs_array, // outputs as array of {address: amount, asset: id} objects
2811            0,             // locktime (0 = no locktime)
2812            false,         // replaceable (false = not replaceable)
2813        ]);
2814
2815        // Debug: Log the exact parameters being sent to createrawtransaction
2816        tracing::error!("createrawtransaction parameters (wallet-specific, corrected format):");
2817        tracing::error!("  wallet: {}", wallet_name);
2818        tracing::error!(
2819            "  inputs: {}",
2820            serde_json::to_string_pretty(&inputs).unwrap_or_default()
2821        );
2822        tracing::error!(
2823            "  outputs_array: {}",
2824            serde_json::to_string_pretty(&outputs_array).unwrap_or_default()
2825        );
2826
2827        // Use the wallet-specific RPC endpoint
2828        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
2829
2830        let request = RpcRequest {
2831            jsonrpc: "1.0".to_string(),
2832            id: "amp-client".to_string(),
2833            method: "createrawtransaction".to_string(),
2834            params,
2835        };
2836
2837        let response = self
2838            .client
2839            .post(&wallet_url)
2840            .basic_auth(&self.username, Some(&self.password))
2841            .json(&request)
2842            .send()
2843            .await
2844            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
2845
2846        if !response.status().is_success() {
2847            let status = response.status();
2848            let error_body = response
2849                .text()
2850                .await
2851                .unwrap_or_else(|_| "Unable to read error body".to_string());
2852            return Err(AmpError::rpc(format!(
2853                "RPC request failed with status: {status} - Body: {error_body}"
2854            )));
2855        }
2856
2857        let rpc_response: RpcResponse<String> = response
2858            .json()
2859            .await
2860            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
2861
2862        if let Some(error) = rpc_response.error {
2863            return Err(AmpError::rpc(format!(
2864                "RPC error creating raw transaction: {} (code: {})",
2865                error.message, error.code
2866            )));
2867        }
2868
2869        Ok(rpc_response.result.unwrap_or_default())
2870    }
2871
2872    /// Blinds a raw transaction for confidential transactions
2873    ///
2874    /// This method uses Elements' blindrawtransaction RPC to properly blind a transaction
2875    /// for confidential asset transfers. This is crucial for Liquid transactions to ensure
2876    /// the blinding factors are properly balanced.
2877    ///
2878    /// # Arguments
2879    /// * `wallet_name` - Name of the Elements wallet to use for blinding
2880    /// * `raw_transaction` - The raw transaction hex to blind
2881    ///
2882    /// # Returns
2883    /// Returns the blinded transaction hex string
2884    ///
2885    /// # Errors
2886    /// Returns an error if the RPC call fails or blinding is not possible
2887    pub async fn blind_raw_transaction(
2888        &self,
2889        wallet_name: &str,
2890        raw_transaction: &str,
2891    ) -> Result<String, AmpError> {
2892        tracing::debug!(
2893            "Blinding raw transaction for wallet {} - tx length: {} chars",
2894            wallet_name,
2895            raw_transaction.len()
2896        );
2897
2898        // First load the wallet to ensure it's available
2899        self.load_wallet(wallet_name).await?;
2900
2901        // Elements blindrawtransaction parameters:
2902        // 1. Raw transaction hex
2903        // 2. Input blinding data (can be empty array for auto-detection)
2904        // 3. Input amounts (can be empty array for auto-detection from UTXOs)
2905        // 4. Input assets (can be empty array for auto-detection from UTXOs)
2906        // 5. Input asset blinders (can be empty array for auto-detection)
2907        // 6. Input amount blinders (can be empty array for auto-detection)
2908        let params = serde_json::json!([
2909            raw_transaction, // Raw transaction hex
2910            [],              // Input blinding data (empty for auto-detection)
2911            [],              // Input amounts (empty for auto-detection)
2912            [],              // Input assets (empty for auto-detection)
2913            [],              // Input asset blinders (empty for auto-detection)
2914            []               // Input amount blinders (empty for auto-detection)
2915        ]);
2916
2917        // Use the wallet-specific RPC endpoint
2918        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
2919
2920        let request = RpcRequest {
2921            jsonrpc: "1.0".to_string(),
2922            id: "amp-client".to_string(),
2923            method: "blindrawtransaction".to_string(),
2924            params,
2925        };
2926
2927        let response = self
2928            .client
2929            .post(&wallet_url)
2930            .basic_auth(&self.username, Some(&self.password))
2931            .json(&request)
2932            .send()
2933            .await
2934            .map_err(|e| {
2935                AmpError::rpc(format!("Failed to send blindrawtransaction request: {e}"))
2936            })?;
2937
2938        if !response.status().is_success() {
2939            let status = response.status();
2940            let error_body = response
2941                .text()
2942                .await
2943                .unwrap_or_else(|_| "Unable to read error body".to_string());
2944            return Err(AmpError::rpc(format!(
2945                "blindrawtransaction failed with status: {status} - Body: {error_body}"
2946            )));
2947        }
2948
2949        let rpc_response: RpcResponse<String> = response.json().await.map_err(|e| {
2950            AmpError::rpc(format!("Failed to parse blindrawtransaction response: {e}"))
2951        })?;
2952
2953        if let Some(error) = rpc_response.error {
2954            return Err(AmpError::rpc(format!(
2955                "RPC error blinding transaction: {} (code: {})",
2956                error.message, error.code
2957            )));
2958        }
2959
2960        let blinded_tx = rpc_response.result.unwrap_or_default();
2961
2962        tracing::info!(
2963            "Successfully blinded transaction - original: {} chars, blinded: {} chars",
2964            raw_transaction.len(),
2965            blinded_tx.len()
2966        );
2967
2968        Ok(blinded_tx)
2969    }
2970
2971    /// Signs a raw transaction using the provided signer callback
2972    ///
2973    /// This method integrates with the Signer trait to sign unsigned transactions.
2974    /// It handles the complete signing workflow including:
2975    /// 1. Validation of the unsigned transaction hex format
2976    /// 2. Calling the signer's `sign_transaction` method
2977    /// 3. Validation of the signed transaction format and structure
2978    /// 4. Proper error handling and context propagation
2979    ///
2980    /// # Arguments
2981    /// * `unsigned_tx_hex` - The unsigned transaction in hexadecimal format
2982    /// * `signer` - Implementation of the Signer trait for transaction signing
2983    ///
2984    /// # Returns
2985    /// Returns the signed transaction as a hex string
2986    ///
2987    /// # Errors
2988    /// Returns an error if:
2989    /// - The unsigned transaction hex is invalid or malformed
2990    /// - The signer fails to sign the transaction
2991    /// - The signed transaction format is invalid
2992    /// - Any validation checks fail
2993    ///
2994    /// # Examples
2995    /// ```no_run
2996    /// # use amp_rs::{ElementsRpc, signer::{Signer, LwkSoftwareSigner}};
2997    /// # #[tokio::main]
2998    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2999    /// let rpc = ElementsRpc::from_env()?;
3000    /// let (_, signer) = LwkSoftwareSigner::generate_new()?;
3001    /// let unsigned_tx = "020000000001..."; // Unsigned transaction hex
3002    /// let signed_tx = rpc.sign_transaction(unsigned_tx, &signer).await?;
3003    /// println!("Transaction signed successfully: {}", signed_tx);
3004    /// # Ok(())
3005    /// # }
3006    /// ```
3007    #[allow(clippy::cognitive_complexity)]
3008    pub async fn sign_transaction(
3009        &self,
3010        unsigned_tx_hex: &str,
3011        signer: &dyn crate::signer::Signer,
3012    ) -> Result<String, AmpError> {
3013        const MIN_TX_SIZE: usize = 10; // Minimum bytes for a valid transaction
3014
3015        tracing::debug!(
3016            "Signing transaction: {}...",
3017            &unsigned_tx_hex[..std::cmp::min(unsigned_tx_hex.len(), 64)]
3018        );
3019
3020        // Validate unsigned transaction hex format
3021        if unsigned_tx_hex.is_empty() {
3022            return Err(AmpError::validation(
3023                "Unsigned transaction hex cannot be empty".to_string(),
3024            ));
3025        }
3026
3027        // Check if hex string has valid format (even length, valid hex characters)
3028        if unsigned_tx_hex.len() % 2 != 0 {
3029            return Err(AmpError::validation(
3030                "Unsigned transaction hex must have even length".to_string(),
3031            ));
3032        }
3033
3034        // Validate hex characters
3035        if !unsigned_tx_hex.chars().all(|c| c.is_ascii_hexdigit()) {
3036            return Err(AmpError::validation(
3037                "Unsigned transaction contains invalid hex characters".to_string(),
3038            ));
3039        }
3040
3041        // Attempt to decode hex to validate transaction structure
3042        let tx_bytes = hex::decode(unsigned_tx_hex).map_err(|e| {
3043            AmpError::validation(format!("Failed to decode unsigned transaction hex: {e}"))
3044        })?;
3045
3046        tracing::debug!("Unsigned transaction validation passed, calling signer");
3047
3048        // Call the signer to sign the transaction
3049        let signed_tx_hex = signer
3050            .sign_transaction(unsigned_tx_hex)
3051            .await
3052            .map_err(|e| {
3053                tracing::error!("Transaction signing failed: {}", e);
3054                AmpError::Signer(e).with_context("Failed to sign transaction")
3055            })?;
3056
3057        tracing::debug!(
3058            "Signer returned signed transaction: {}...",
3059            &signed_tx_hex[..std::cmp::min(signed_tx_hex.len(), 64)]
3060        );
3061
3062        // Validate signed transaction format and structure
3063        if signed_tx_hex.is_empty() {
3064            return Err(AmpError::validation(
3065                "Signed transaction hex cannot be empty".to_string(),
3066            ));
3067        }
3068
3069        // Check if signed transaction has valid hex format
3070        if signed_tx_hex.len() % 2 != 0 {
3071            return Err(AmpError::validation(
3072                "Signed transaction hex must have even length".to_string(),
3073            ));
3074        }
3075
3076        // Validate hex characters in signed transaction
3077        if !signed_tx_hex.chars().all(|c| c.is_ascii_hexdigit()) {
3078            return Err(AmpError::validation(
3079                "Signed transaction contains invalid hex characters".to_string(),
3080            ));
3081        }
3082
3083        // Attempt to decode signed transaction to validate structure
3084        let signed_tx_bytes = hex::decode(&signed_tx_hex).map_err(|e| {
3085            AmpError::validation(format!("Failed to decode signed transaction hex: {e}"))
3086        })?;
3087
3088        // Basic validation: signed transaction should be at least as long as unsigned
3089        // (signatures add data, so signed tx should be larger or equal)
3090        if signed_tx_bytes.len() < tx_bytes.len() {
3091            return Err(AmpError::validation(
3092                "Signed transaction is shorter than unsigned transaction, which is invalid"
3093                    .to_string(),
3094            ));
3095        }
3096
3097        // Additional validation: check that the transaction structure is reasonable
3098        // Minimum transaction size for Elements (very basic check)
3099        if signed_tx_bytes.len() < MIN_TX_SIZE {
3100            return Err(AmpError::validation(format!(
3101                "Signed transaction does not meet minimum size ({} bytes), minimum is {} bytes",
3102                signed_tx_bytes.len(),
3103                MIN_TX_SIZE
3104            )));
3105        }
3106
3107        tracing::info!(
3108            "Transaction signed successfully - unsigned: {} bytes, signed: {} bytes",
3109            tx_bytes.len(),
3110            signed_tx_bytes.len()
3111        );
3112
3113        Ok(signed_tx_hex)
3114    }
3115
3116    /// Signs and broadcasts a transaction in a single operation
3117    ///
3118    /// This is a convenience method that combines transaction signing and broadcasting.
3119    /// It performs the complete workflow of signing an unsigned transaction and
3120    /// immediately broadcasting it to the network.
3121    ///
3122    /// # Arguments
3123    /// * `unsigned_tx_hex` - The unsigned transaction in hexadecimal format
3124    /// * `signer` - Implementation of the Signer trait for transaction signing
3125    ///
3126    /// # Returns
3127    /// Returns the transaction ID of the broadcast transaction
3128    ///
3129    /// # Errors
3130    /// Returns an error if signing or broadcasting fails
3131    ///
3132    /// # Examples
3133    /// ```no_run
3134    /// # use amp_rs::{ElementsRpc, signer::{Signer, LwkSoftwareSigner}};
3135    /// # #[tokio::main]
3136    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3137    /// let rpc = ElementsRpc::from_env()?;
3138    /// let (_, signer) = LwkSoftwareSigner::generate_new()?;
3139    /// let unsigned_tx = "020000000001..."; // Unsigned transaction hex
3140    /// let txid = rpc.sign_and_broadcast_transaction(unsigned_tx, &signer).await?;
3141    /// println!("Transaction broadcast with ID: {}", txid);
3142    /// # Ok(())
3143    /// # }
3144    /// ```
3145    pub async fn sign_and_broadcast_transaction(
3146        &self,
3147        unsigned_tx_hex: &str,
3148        signer: &dyn crate::signer::Signer,
3149    ) -> Result<String, AmpError> {
3150        tracing::info!("Signing and broadcasting transaction");
3151
3152        // Sign the transaction
3153        let signed_tx_hex = self
3154            .sign_transaction(unsigned_tx_hex, signer)
3155            .await
3156            .map_err(|e| e.with_context("Failed during transaction signing phase"))?;
3157
3158        // Broadcast the signed transaction
3159        let txid = self
3160            .send_raw_transaction(&signed_tx_hex)
3161            .await
3162            .map_err(|e| e.with_context("Failed during transaction broadcast phase"))?;
3163
3164        tracing::info!("Successfully signed and broadcast transaction: {}", txid);
3165        Ok(txid)
3166    }
3167
3168    /// Signs and broadcasts a transaction with UTXO information for proper PSBT construction
3169    ///
3170    /// This method provides UTXO information to the signer for proper PSBT construction,
3171    /// which is required for confidential transactions where the signer needs to know
3172    /// the previous transaction outputs being spent.
3173    ///
3174    /// # Arguments
3175    /// * `unsigned_tx_hex` - The unsigned transaction in hexadecimal format
3176    /// * `utxos` - Vector of UTXOs being spent in the transaction
3177    /// * `signer` - Implementation of the Signer trait for transaction signing
3178    ///
3179    /// # Returns
3180    /// Returns the transaction ID of the broadcast transaction
3181    ///
3182    /// # Errors
3183    /// Returns an error if signing or broadcasting fails
3184    #[allow(clippy::cognitive_complexity)]
3185    pub async fn sign_and_broadcast_transaction_with_utxos(
3186        &self,
3187        unsigned_tx_hex: &str,
3188        utxos: &[Unspent],
3189        signer: &dyn crate::signer::Signer,
3190    ) -> Result<String, AmpError> {
3191        tracing::info!(
3192            "Signing and broadcasting transaction with {} UTXOs",
3193            utxos.len()
3194        );
3195
3196        // Try to use the enhanced signing method if the signer supports it
3197        let signed_tx_hex = if let Some(lwk_signer) = signer
3198            .as_any()
3199            .downcast_ref::<crate::signer::LwkSoftwareSigner>(
3200        ) {
3201            // Use the enhanced signing method with UTXO information
3202            tracing::debug!("Using LWK signer with UTXO information");
3203            lwk_signer
3204                .sign_transaction_with_utxos(unsigned_tx_hex, utxos)
3205                .await
3206                .map_err(|e| {
3207                    AmpError::Signer(e)
3208                        .with_context("Failed during enhanced transaction signing phase")
3209                })?
3210        } else {
3211            // Fall back to standard signing method
3212            tracing::debug!("Using standard signing method (no UTXO information)");
3213            self.sign_transaction(unsigned_tx_hex, signer)
3214                .await
3215                .map_err(|e| e.with_context("Failed during transaction signing phase"))?
3216        };
3217
3218        // Broadcast the signed transaction
3219        let txid = self
3220            .send_raw_transaction(&signed_tx_hex)
3221            .await
3222            .map_err(|e| e.with_context("Failed during transaction broadcast phase"))?;
3223
3224        tracing::info!("Successfully signed and broadcast transaction: {}", txid);
3225        Ok(txid)
3226    }
3227
3228    /// Collects change data from a confirmed transaction for distribution confirmation
3229    ///
3230    /// This method queries the Elements node to find change UTXOs from a specific transaction
3231    /// that belong to the specified asset. It's used after a distribution transaction is
3232    /// confirmed to collect the change outputs for the final confirmation API call.
3233    ///
3234    /// # Arguments
3235    /// * `asset_id` - The asset ID to filter change UTXOs for
3236    /// * `txid` - The transaction ID to filter change UTXOs from
3237    ///
3238    /// # Returns
3239    /// Returns a vector of Unspent UTXOs that represent change outputs from the transaction.
3240    /// Returns an empty vector if no change outputs exist for the specified asset and transaction.
3241    ///
3242    /// # Errors
3243    /// Returns an error if the RPC call fails or if there are issues querying the Elements node
3244    ///
3245    /// # Examples
3246    /// ```no_run
3247    /// # use amp_rs::ElementsRpc;
3248    /// # #[tokio::main]
3249    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3250    /// let rpc = ElementsRpc::from_env()?;
3251    /// let change_data = rpc.collect_change_data(
3252    ///     "asset_id_hex",
3253    ///     "transaction_id_hex",
3254    ///     &rpc,
3255    ///     "wallet_name"
3256    /// ).await?;
3257    ///
3258    /// if change_data.is_empty() {
3259    ///     println!("No change outputs found for this transaction");
3260    /// } else {
3261    ///     println!("Found {} change outputs", change_data.len());
3262    /// }
3263    /// # Ok(())
3264    /// # }
3265    /// ```
3266    #[allow(clippy::cognitive_complexity)]
3267    pub async fn collect_change_data(
3268        &self,
3269        asset_id: &str,
3270        txid: &str,
3271        node_rpc: &Self,
3272        wallet_name: &str,
3273    ) -> Result<Vec<Unspent>, AmpError> {
3274        tracing::debug!(
3275            "Collecting change data for asset {} from transaction {}",
3276            asset_id,
3277            txid
3278        );
3279
3280        // Use the raw listunspent RPC call to get full blinding information
3281        // This is essential for confidential transactions as the AMP API requires
3282        // both amountblinder and assetblinder fields
3283        let all_utxos = node_rpc
3284            .list_unspent_with_blinding_data(wallet_name)
3285            .await
3286            .map_err(|e| {
3287                e.with_context(
3288                    "Failed to query unspent outputs with blinding data for change data collection",
3289                )
3290            })?;
3291
3292        // Filter UTXOs to only include those from the specified transaction
3293        let change_utxos: Vec<Unspent> = all_utxos
3294            .into_iter()
3295            .filter(|utxo| {
3296                // Match UTXOs that:
3297                // 1. Come from the specified transaction (txid matches)
3298                // 2. Are for the correct asset
3299                // 3. Are spendable
3300                utxo.txid == txid && utxo.asset == asset_id && utxo.spendable
3301            })
3302            .collect();
3303
3304        tracing::info!(
3305            "Collected {} change UTXOs for asset {} from transaction {}",
3306            change_utxos.len(),
3307            asset_id,
3308            txid
3309        );
3310
3311        // Log details of found change UTXOs for debugging
3312        for (index, utxo) in change_utxos.iter().enumerate() {
3313            tracing::debug!(
3314                "Change UTXO {}: txid={}, vout={}, amount={}, asset={}, amountblinder={:?}, assetblinder={:?}",
3315                index + 1,
3316                utxo.txid,
3317                utxo.vout,
3318                utxo.amount,
3319                utxo.asset,
3320                utxo.amountblinder,
3321                utxo.assetblinder
3322            );
3323        }
3324
3325        // Handle the case where no change outputs exist
3326        if change_utxos.is_empty() {
3327            tracing::info!(
3328                "No change outputs found for asset {} in transaction {} - this is normal if all funds were distributed",
3329                asset_id,
3330                txid
3331            );
3332        }
3333
3334        Ok(change_utxos)
3335    }
3336
3337    /// Lists unspent outputs with full blinding data for confidential transactions
3338    ///
3339    /// This method calls the raw `listunspent` RPC to get complete UTXO information
3340    /// including blinding data (amountblinder and assetblinder) which is required
3341    /// for confidential transaction confirmation with the AMP API.
3342    ///
3343    /// # Arguments
3344    /// * `wallet_name` - Name of the Elements wallet to query
3345    ///
3346    /// # Returns
3347    /// Returns a vector of `Unspent` structs with complete blinding information
3348    ///
3349    /// # Errors
3350    /// Returns an error if the RPC call fails
3351    ///
3352    /// # Examples
3353    /// ```no_run
3354    /// # use amp_rs::ElementsRpc;
3355    /// # #[tokio::main]
3356    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3357    /// let rpc = ElementsRpc::from_env()?;
3358    /// let utxos = rpc.list_unspent_with_blinding_data("wallet_name").await?;
3359    /// for utxo in utxos {
3360    ///     println!("UTXO: {} with blinders: {:?}, {:?}",
3361    ///              utxo.txid, utxo.amountblinder, utxo.assetblinder);
3362    /// }
3363    /// # Ok(())
3364    /// # }
3365    /// ```
3366    pub async fn list_unspent_with_blinding_data(
3367        &self,
3368        wallet_name: &str,
3369    ) -> Result<Vec<Unspent>, AmpError> {
3370        tracing::debug!(
3371            "Listing unspent outputs with blinding data for wallet: {}",
3372            wallet_name
3373        );
3374
3375        // First load the wallet to ensure it's available
3376        self.load_wallet(wallet_name).await?;
3377
3378        // Call listunspent with parameters to get all UTXOs
3379        // Parameters: minconf, maxconf, addresses, include_unsafe, query_options
3380        let params = serde_json::json!([
3381            0,         // minconf: include unconfirmed
3382            9_999_999, // maxconf: include all confirmed
3383            [],        // addresses: empty array means all addresses
3384            true,      // include_unsafe: include unconfirmed transactions
3385            {}         // query_options: empty object for default options
3386        ]);
3387
3388        // Use the wallet-specific RPC endpoint
3389        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
3390
3391        let request = RpcRequest {
3392            jsonrpc: "1.0".to_string(),
3393            id: "amp-client".to_string(),
3394            method: "listunspent".to_string(),
3395            params,
3396        };
3397
3398        let response = self
3399            .client
3400            .post(&wallet_url)
3401            .basic_auth(&self.username, Some(&self.password))
3402            .json(&request)
3403            .send()
3404            .await
3405            .map_err(|e| AmpError::rpc(format!("Failed to send listunspent RPC request: {e}")))?;
3406
3407        if !response.status().is_success() {
3408            let status = response.status();
3409            let error_body = response
3410                .text()
3411                .await
3412                .unwrap_or_else(|_| "Unable to read error body".to_string());
3413            return Err(AmpError::rpc(format!(
3414                "Listunspent RPC request failed with status: {status} - Body: {error_body}"
3415            )));
3416        }
3417
3418        let rpc_response: RpcResponse<Vec<Unspent>> = response
3419            .json()
3420            .await
3421            .map_err(|e| AmpError::rpc(format!("Failed to parse listunspent RPC response: {e}")))?;
3422
3423        if let Some(error) = rpc_response.error {
3424            return Err(AmpError::rpc(format!(
3425                "Listunspent RPC error: {} (code: {})",
3426                error.message, error.code
3427            )));
3428        }
3429
3430        let utxos = rpc_response.result.unwrap_or_default();
3431        tracing::info!(
3432            "Retrieved {} UTXOs with blinding data from wallet {}",
3433            utxos.len(),
3434            wallet_name
3435        );
3436
3437        Ok(utxos)
3438    }
3439
3440    /// Creates a standard wallet in Elements (Elements-first approach)
3441    ///
3442    /// This method creates a new standard wallet in the Elements node that can generate
3443    /// addresses and private keys. This is part of the Elements-first approach where
3444    /// we create the wallet in Elements first, then export keys to LWK.
3445    ///
3446    /// # Arguments
3447    /// * `wallet_name` - Name for the new wallet
3448    ///
3449    /// # Errors
3450    /// Returns an error if the RPC call fails
3451    ///
3452    /// # Examples
3453    /// ```no_run
3454    /// # use amp_rs::ElementsRpc;
3455    /// # #[tokio::main]
3456    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3457    /// let rpc = ElementsRpc::from_env()?;
3458    /// rpc.create_elements_wallet("test_wallet").await?;
3459    /// # Ok(())
3460    /// # }
3461    /// ```
3462    pub async fn create_elements_wallet(&self, wallet_name: &str) -> Result<(), AmpError> {
3463        let params = serde_json::json!([wallet_name]);
3464
3465        let _result: serde_json::Value = self.rpc_call("createwallet", params).await?;
3466
3467        tracing::info!("Successfully created Elements wallet: {}", wallet_name);
3468        Ok(())
3469    }
3470
3471    /// Get a new address from an Elements wallet
3472    ///
3473    /// This method requests a new address from the specified Elements wallet.
3474    /// The address will be generated by Elements and can be used for receiving funds.
3475    /// Defaults to native segwit (bech32) addresses for optimal compatibility.
3476    ///
3477    /// # Arguments
3478    /// * `wallet_name` - Name of the wallet to get address from
3479    /// * `address_type` - Optional address type ("bech32", "legacy", "p2sh-segwit"). Defaults to "bech32"
3480    ///
3481    /// # Errors
3482    /// Returns an error if the RPC call fails or the response format is unexpected
3483    ///
3484    /// # Examples
3485    /// ```no_run
3486    /// # use amp_rs::ElementsRpc;
3487    /// # #[tokio::main]
3488    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3489    /// let rpc = ElementsRpc::from_env()?;
3490    ///
3491    /// // Generate native segwit address (default)
3492    /// let address = rpc.get_new_address("test_wallet", None).await?;
3493    ///
3494    /// // Or explicitly request native segwit
3495    /// let bech32_address = rpc.get_new_address("test_wallet", Some("bech32")).await?;
3496    ///
3497    /// println!("Native segwit address: {}", address);
3498    /// # Ok(())
3499    /// # }
3500    /// ```
3501    pub async fn get_new_address(
3502        &self,
3503        wallet_name: &str,
3504        address_type: Option<&str>,
3505    ) -> Result<String, AmpError> {
3506        // First load the wallet to ensure it's available
3507        self.load_wallet(wallet_name).await?;
3508
3509        // Set default to native segwit (bech32) for Elements
3510        let addr_type = address_type.unwrap_or("bech32");
3511
3512        // For Elements, we need to use the correct parameters for getnewaddress
3513        // getnewaddress [label] [address_type]
3514        let params = serde_json::json!(["", addr_type]);
3515
3516        // Create RPC request for getnewaddress
3517        let request = RpcRequest {
3518            jsonrpc: "1.0".to_string(),
3519            id: "amp-client".to_string(),
3520            method: "getnewaddress".to_string(),
3521            params,
3522        };
3523
3524        // Use the wallet-specific RPC endpoint
3525        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
3526
3527        let response = self
3528            .client
3529            .post(&wallet_url)
3530            .basic_auth(&self.username, Some(&self.password))
3531            .json(&request)
3532            .send()
3533            .await
3534            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
3535
3536        if !response.status().is_success() {
3537            let status = response.status();
3538            let error_body = response
3539                .text()
3540                .await
3541                .unwrap_or_else(|_| "Unable to read error body".to_string());
3542            return Err(AmpError::rpc(format!(
3543                "RPC request failed with status: {status} - Body: {error_body}"
3544            )));
3545        }
3546
3547        let rpc_response: RpcResponse<serde_json::Value> = response
3548            .json()
3549            .await
3550            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
3551
3552        if let Some(error) = rpc_response.error {
3553            return Err(AmpError::rpc(format!(
3554                "RPC error getting new address: {} (code: {})",
3555                error.message, error.code
3556            )));
3557        }
3558
3559        if let Some(result) = rpc_response.result {
3560            if let Some(address) = result.as_str() {
3561                tracing::info!("Generated new {} address: {}", addr_type, address);
3562                return Ok(address.to_string());
3563            }
3564        }
3565
3566        Err(AmpError::rpc(format!(
3567            "Failed to get new address from wallet '{wallet_name}': unexpected response format"
3568        )))
3569    }
3570
3571    /// Get the confidential version of an address from Elements wallet
3572    ///
3573    /// This method takes a regular (unconfidential) address and returns its confidential
3574    /// counterpart, which includes blinding keys for confidential transactions.
3575    ///
3576    /// # Arguments
3577    ///
3578    /// * `wallet_name` - Name of the Elements wallet
3579    /// * `address` - The unconfidential address to get info for
3580    ///
3581    /// # Returns
3582    ///
3583    /// Returns the confidential address string
3584    ///
3585    /// # Example
3586    ///
3587    /// ```no_run
3588    /// # use amp_rs::ElementsRpc;
3589    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
3590    /// let rpc = ElementsRpc::from_env()?;
3591    /// let unconfidential_address = "tex1q...";
3592    /// // Note: This would need to be called in an async context
3593    /// // let confidential_address = rpc.get_confidential_address("test_wallet", unconfidential_address).await?;
3594    /// // println!("Confidential address: {}", confidential_address);
3595    /// # Ok(())
3596    /// # }
3597    /// ```
3598    /// Gets the confidential address for a given unconfidential address from a wallet
3599    ///
3600    /// # Errors
3601    /// Returns an error if the RPC call fails or the response format is unexpected
3602    pub async fn get_confidential_address(
3603        &self,
3604        wallet_name: &str,
3605        address: &str,
3606    ) -> Result<String, AmpError> {
3607        // First load the wallet to ensure it's available
3608        self.load_wallet(wallet_name).await?;
3609
3610        let params = serde_json::json!([address]);
3611
3612        // Create RPC request for getaddressinfo
3613        let request = RpcRequest {
3614            jsonrpc: "1.0".to_string(),
3615            id: "amp-client".to_string(),
3616            method: "getaddressinfo".to_string(),
3617            params,
3618        };
3619
3620        // Use the wallet-specific RPC endpoint
3621        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
3622
3623        let response = self
3624            .client
3625            .post(&wallet_url)
3626            .basic_auth(&self.username, Some(&self.password))
3627            .json(&request)
3628            .send()
3629            .await
3630            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
3631
3632        if !response.status().is_success() {
3633            let status = response.status();
3634            let error_body = response
3635                .text()
3636                .await
3637                .unwrap_or_else(|_| "Unable to read error body".to_string());
3638            return Err(AmpError::rpc(format!(
3639                "RPC request failed with status: {status} - Body: {error_body}"
3640            )));
3641        }
3642
3643        let rpc_response: RpcResponse<serde_json::Value> = response
3644            .json()
3645            .await
3646            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
3647
3648        if let Some(error) = rpc_response.error {
3649            return Err(AmpError::rpc(format!(
3650                "RPC error getting address info: {} (code: {})",
3651                error.message, error.code
3652            )));
3653        }
3654
3655        if let Some(result) = rpc_response.result {
3656            if let Some(confidential_address) = result.get("confidential").and_then(|v| v.as_str())
3657            {
3658                tracing::info!("Retrieved confidential address for: {}", address);
3659                return Ok(confidential_address.to_string());
3660            }
3661        }
3662
3663        Err(AmpError::rpc(format!(
3664            "Failed to get confidential address for '{address}': unexpected response format"
3665        )))
3666    }
3667
3668    /// Get the private key for an address from Elements wallet
3669    ///
3670    /// This method exports the private key for a specific address from the Elements wallet.
3671    /// The private key can then be imported into LWK for signing.
3672    ///
3673    /// Note: This is a simplified implementation that returns a placeholder private key.
3674    /// For production use, implement proper wallet-specific RPC calls.
3675    ///
3676    /// # Arguments
3677    /// * `wallet_name` - Name of the wallet containing the address
3678    /// * `address` - The address to get the private key for
3679    ///
3680    /// # Errors
3681    /// Returns an error if the RPC call fails
3682    ///
3683    /// # Examples
3684    /// ```no_run
3685    /// # use amp_rs::ElementsRpc;
3686    /// # #[tokio::main]
3687    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3688    /// let rpc = ElementsRpc::from_env()?;
3689    /// let address = rpc.get_new_address("test_wallet", None).await?;
3690    /// let private_key = rpc.dump_private_key("test_wallet", &address).await?;
3691    /// println!("Private key: {}", private_key);
3692    /// # Ok(())
3693    /// # }
3694    /// ```
3695    pub async fn dump_private_key(
3696        &self,
3697        wallet_name: &str,
3698        address: &str,
3699    ) -> Result<String, AmpError> {
3700        // First load the wallet to ensure it's available
3701        self.load_wallet(wallet_name).await?;
3702
3703        let params = serde_json::json!([address]);
3704
3705        // Create RPC request for dumpprivkey
3706        let request = RpcRequest {
3707            jsonrpc: "1.0".to_string(),
3708            id: "amp-client".to_string(),
3709            method: "dumpprivkey".to_string(),
3710            params,
3711        };
3712
3713        // Use the wallet-specific RPC endpoint
3714        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
3715
3716        let response = self
3717            .client
3718            .post(&wallet_url)
3719            .basic_auth(&self.username, Some(&self.password))
3720            .json(&request)
3721            .send()
3722            .await
3723            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
3724
3725        if !response.status().is_success() {
3726            return Err(AmpError::rpc(format!(
3727                "RPC request failed with status: {}",
3728                response.status()
3729            )));
3730        }
3731
3732        let rpc_response: RpcResponse<serde_json::Value> = response
3733            .json()
3734            .await
3735            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
3736
3737        if let Some(error) = rpc_response.error {
3738            return Err(AmpError::rpc(format!(
3739                "RPC error dumping private key: {} (code: {})",
3740                error.message, error.code
3741            )));
3742        }
3743
3744        if let Some(result) = rpc_response.result {
3745            if let Some(private_key) = result.as_str() {
3746                tracing::info!("Successfully exported private key for address: {}", address);
3747                return Ok(private_key.to_string());
3748            }
3749        }
3750
3751        Err(AmpError::rpc(format!(
3752            "Failed to dump private key for address '{address}': unexpected response format"
3753        )))
3754    }
3755
3756    /// Creates a descriptor wallet in Elements
3757    ///
3758    /// # Arguments
3759    /// * `wallet_name` - Name for the new wallet
3760    ///
3761    /// # Errors
3762    /// Returns an error if the RPC call fails
3763    ///
3764    /// # Examples
3765    /// ```no_run
3766    /// # use amp_rs::ElementsRpc;
3767    /// # #[tokio::main]
3768    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3769    /// let rpc = ElementsRpc::from_env()?;
3770    /// rpc.create_descriptor_wallet("test_wallet").await?;
3771    /// # Ok(())
3772    /// # }
3773    /// ```
3774    pub async fn create_descriptor_wallet(&self, wallet_name: &str) -> Result<(), AmpError> {
3775        let params = serde_json::json!([wallet_name, true]); // true enables descriptors
3776
3777        let _result: serde_json::Value = self.rpc_call("createwallet", params).await?;
3778
3779        tracing::info!("Successfully created descriptor wallet: {}", wallet_name);
3780        Ok(())
3781    }
3782
3783    /// Imports a single descriptor into an Elements wallet
3784    ///
3785    /// This method imports a descriptor that enables the wallet to scan and recognize
3786    /// addresses/UTXOs from a mnemonic. For LWK descriptors with `<0;1>/*` format,
3787    /// a single descriptor covers both receive and change addresses.
3788    ///
3789    /// # Arguments
3790    /// * `wallet_name` - Name of the wallet to import descriptor into
3791    /// * `descriptor` - The descriptor to import
3792    ///
3793    /// # Errors
3794    /// Returns an error if the RPC call fails
3795    ///
3796    /// # Examples
3797    /// ```no_run
3798    /// # use amp_rs::ElementsRpc;
3799    /// # #[tokio::main]
3800    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3801    /// let rpc = ElementsRpc::from_env()?;
3802    /// let descriptor = "ct(slip77(...),elwpkh([...]/84h/1h/0h]tpub.../<0;1>/*))#checksum";
3803    /// rpc.import_descriptor("test_wallet", descriptor).await?;
3804    /// # Ok(())
3805    /// # }
3806    /// ```
3807    pub async fn import_descriptor(
3808        &self,
3809        wallet_name: &str,
3810        descriptor: &str,
3811    ) -> Result<(), AmpError> {
3812        tracing::info!("Importing descriptor into wallet: {}", wallet_name);
3813        tracing::debug!("Descriptor: {}", descriptor);
3814
3815        let descriptors = serde_json::json!([
3816            {
3817                "desc": descriptor,
3818                "timestamp": "now",
3819                "active": true,
3820                "internal": false  // For LWK descriptors with <0;1>/*, this covers both chains
3821            }
3822        ]);
3823
3824        // Use -rpcwallet parameter to specify the wallet
3825        let request = RpcRequest {
3826            jsonrpc: "1.0".to_string(),
3827            id: "amp-client".to_string(),
3828            method: "importdescriptors".to_string(),
3829            params: descriptors,
3830        };
3831
3832        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
3833
3834        let response = self
3835            .client
3836            .post(&wallet_url)
3837            .basic_auth(&self.username, Some(&self.password))
3838            .json(&request)
3839            .send()
3840            .await
3841            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
3842
3843        if !response.status().is_success() {
3844            return Err(AmpError::rpc(format!(
3845                "RPC request failed with status: {}",
3846                response.status()
3847            )));
3848        }
3849
3850        let rpc_response: RpcResponse<serde_json::Value> = response
3851            .json()
3852            .await
3853            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
3854
3855        if let Some(error) = rpc_response.error {
3856            return Err(AmpError::rpc(format!(
3857                "RPC error {}: {}",
3858                error.code, error.message
3859            )));
3860        }
3861
3862        let result = rpc_response
3863            .result
3864            .ok_or_else(|| AmpError::rpc("RPC response missing result field".to_string()))?;
3865
3866        // Check if descriptor was imported successfully
3867        if let Some(results) = result.as_array() {
3868            if let Some(result) = results.first() {
3869                if let Some(success) = result.get("success").and_then(serde_json::Value::as_bool) {
3870                    if !success {
3871                        let error_msg = result
3872                            .get("error")
3873                            .and_then(|e| e.get("message"))
3874                            .and_then(|m| m.as_str())
3875                            .unwrap_or("Unknown error");
3876                        return Err(AmpError::rpc(format!(
3877                            "Failed to import descriptor: {error_msg}"
3878                        )));
3879                    }
3880                } else {
3881                    return Err(AmpError::rpc(format!(
3882                        "Invalid response format for descriptor import: {result:?}"
3883                    )));
3884                }
3885            }
3886        } else {
3887            return Err(AmpError::rpc(format!(
3888                "Invalid response format: expected array, got {result:?}"
3889            )));
3890        }
3891
3892        tracing::info!(
3893            "Successfully imported descriptor into wallet: {}",
3894            wallet_name
3895        );
3896        Ok(())
3897    }
3898
3899    /// Imports descriptors into an Elements wallet (legacy method for compatibility)
3900    ///
3901    /// This method imports descriptors that enable the wallet to scan and recognize
3902    /// addresses/UTXOs from a mnemonic. If both descriptors are the same (as with LWK
3903    /// descriptors using `<0;1>/*` format), only one descriptor is imported.
3904    ///
3905    /// # Arguments
3906    /// * `wallet_name` - Name of the wallet to import descriptors into
3907    /// * `receive_descriptor` - The receive descriptor
3908    /// * `change_descriptor` - The change descriptor
3909    ///
3910    /// # Errors
3911    /// Returns an error if the RPC call fails
3912    ///
3913    /// # Examples
3914    /// ```no_run
3915    /// # use amp_rs::ElementsRpc;
3916    /// # #[tokio::main]
3917    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3918    /// let rpc = ElementsRpc::from_env()?;
3919    /// let descriptor = "ct(slip77(...),elwpkh([...]/84h/1h/0h]tpub.../<0;1>/*))#checksum";
3920    /// rpc.import_descriptors("test_wallet", descriptor, descriptor).await?;
3921    /// # Ok(())
3922    /// # }
3923    /// ```
3924    #[allow(clippy::cognitive_complexity)]
3925    pub async fn import_descriptors(
3926        &self,
3927        wallet_name: &str,
3928        receive_descriptor: &str,
3929        change_descriptor: &str,
3930    ) -> Result<(), AmpError> {
3931        // If both descriptors are the same (LWK case), import only once
3932        if receive_descriptor == change_descriptor {
3933            return self
3934                .import_descriptor(wallet_name, receive_descriptor)
3935                .await;
3936        }
3937
3938        tracing::info!(
3939            "Importing separate receive and change descriptors into wallet: {}",
3940            wallet_name
3941        );
3942        tracing::debug!("Receive descriptor: {}", receive_descriptor);
3943        tracing::debug!("Change descriptor: {}", change_descriptor);
3944
3945        let descriptors = serde_json::json!([
3946            {
3947                "desc": receive_descriptor,
3948                "timestamp": "now",
3949                "active": true,
3950                "internal": false
3951            },
3952            {
3953                "desc": change_descriptor,
3954                "timestamp": "now",
3955                "active": true,
3956                "internal": true
3957            }
3958        ]);
3959
3960        // Use -rpcwallet parameter to specify the wallet
3961        let request = RpcRequest {
3962            jsonrpc: "1.0".to_string(),
3963            id: "amp-client".to_string(),
3964            method: "importdescriptors".to_string(),
3965            params: descriptors,
3966        };
3967
3968        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
3969
3970        let response = self
3971            .client
3972            .post(&wallet_url)
3973            .basic_auth(&self.username, Some(&self.password))
3974            .json(&request)
3975            .send()
3976            .await
3977            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
3978
3979        if !response.status().is_success() {
3980            return Err(AmpError::rpc(format!(
3981                "RPC request failed with status: {}",
3982                response.status()
3983            )));
3984        }
3985
3986        let rpc_response: RpcResponse<serde_json::Value> = response
3987            .json()
3988            .await
3989            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
3990
3991        if let Some(error) = rpc_response.error {
3992            return Err(AmpError::rpc(format!(
3993                "RPC error {}: {}",
3994                error.code, error.message
3995            )));
3996        }
3997
3998        let result = rpc_response
3999            .result
4000            .ok_or_else(|| AmpError::rpc("RPC response missing result field".to_string()))?;
4001
4002        // Check if both descriptors were imported successfully
4003        if let Some(results) = result.as_array() {
4004            for (i, result) in results.iter().enumerate() {
4005                if let Some(success) = result.get("success").and_then(serde_json::Value::as_bool) {
4006                    if !success {
4007                        let desc_type = if i == 0 { "receive" } else { "change" };
4008                        let error_msg = result
4009                            .get("error")
4010                            .and_then(|e| e.get("message"))
4011                            .and_then(|m| m.as_str())
4012                            .unwrap_or("Unknown error");
4013                        return Err(AmpError::rpc(format!(
4014                            "Failed to import {desc_type} descriptor: {error_msg}"
4015                        )));
4016                    }
4017                } else {
4018                    return Err(AmpError::rpc(format!(
4019                        "Invalid response format for descriptor import: {result:?}"
4020                    )));
4021                }
4022            }
4023        } else {
4024            return Err(AmpError::rpc(format!(
4025                "Invalid response format: expected array, got {result:?}"
4026            )));
4027        }
4028
4029        tracing::info!(
4030            "Successfully imported descriptors into wallet: {}",
4031            wallet_name
4032        );
4033        Ok(())
4034    }
4035
4036    /// Sets up a wallet with descriptors from a mnemonic
4037    ///
4038    /// This is a convenience method that combines wallet creation and descriptor import.
4039    /// It creates a descriptor wallet and imports the receive and change descriptors
4040    /// generated from the provided mnemonic.
4041    ///
4042    /// # Arguments
4043    /// * `wallet_name` - Name for the new wallet
4044    /// * `receive_descriptor` - The receive descriptor (external chain /0/*)
4045    /// * `change_descriptor` - The change descriptor (internal chain /1/*)
4046    ///
4047    /// # Errors
4048    /// Returns an error if wallet creation or descriptor import fails
4049    ///
4050    /// # Examples
4051    /// ```no_run
4052    /// # use amp_rs::ElementsRpc;
4053    /// # #[tokio::main]
4054    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4055    /// let rpc = ElementsRpc::from_env()?;
4056    /// let receive_desc = "wpkh([d34db33f/84h/1h/0h]xprv.../0/*)#checksum";
4057    /// let change_desc = "wpkh([d34db33f/84h/1h/0h]xprv.../1/*)#checksum";
4058    /// rpc.setup_wallet_with_descriptors("test_wallet", receive_desc, change_desc).await?;
4059    /// # Ok(())
4060    /// # }
4061    /// ```
4062    #[allow(clippy::cognitive_complexity)]
4063    pub async fn setup_wallet_with_descriptors(
4064        &self,
4065        wallet_name: &str,
4066        receive_descriptor: &str,
4067        change_descriptor: &str,
4068    ) -> Result<(), AmpError> {
4069        tracing::info!("Setting up wallet with descriptors: {}", wallet_name);
4070
4071        // Try to create the wallet (may fail if it already exists)
4072        match self.create_descriptor_wallet(wallet_name).await {
4073            Ok(()) => {
4074                tracing::info!("Created new descriptor wallet: {}", wallet_name);
4075            }
4076            Err(e) => {
4077                let error_msg = e.to_string();
4078                if error_msg.contains("already exists")
4079                    || error_msg.contains("Database already exists")
4080                {
4081                    tracing::info!(
4082                        "Wallet {} already exists, proceeding with descriptor import",
4083                        wallet_name
4084                    );
4085                } else {
4086                    return Err(e);
4087                }
4088            }
4089        }
4090
4091        // Import the descriptors
4092        self.import_descriptors(wallet_name, receive_descriptor, change_descriptor)
4093            .await?;
4094
4095        tracing::info!(
4096            "Successfully set up wallet with descriptors: {}",
4097            wallet_name
4098        );
4099        Ok(())
4100    }
4101}
4102
4103#[cfg(test)]
4104mod elements_rpc_tests {
4105    use super::*;
4106    use httpmock::prelude::*;
4107    use serial_test::serial;
4108    use std::collections::HashMap;
4109
4110    #[test]
4111    fn test_elements_rpc_new() {
4112        let rpc = ElementsRpc::new(
4113            "http://localhost:18884".to_string(),
4114            "user".to_string(),
4115            "pass".to_string(),
4116        );
4117
4118        assert_eq!(rpc.base_url, "http://localhost:18884");
4119        assert_eq!(rpc.username, "user");
4120        assert_eq!(rpc.password, "pass");
4121    }
4122
4123    #[test]
4124    #[serial]
4125    fn test_elements_rpc_from_env_missing_vars() {
4126        // Store original values to restore later
4127        let original_url = env::var("ELEMENTS_RPC_URL").ok();
4128        let original_user = env::var("ELEMENTS_RPC_USER").ok();
4129        let original_password = env::var("ELEMENTS_RPC_PASSWORD").ok();
4130
4131        // Clear environment variables to test error handling
4132        env::remove_var("ELEMENTS_RPC_URL");
4133        env::remove_var("ELEMENTS_RPC_USER");
4134        env::remove_var("ELEMENTS_RPC_PASSWORD");
4135
4136        let result = ElementsRpc::from_env();
4137        assert!(
4138            result.is_err(),
4139            "ElementsRpc::from_env() should fail when env vars are missing"
4140        );
4141
4142        match result.unwrap_err() {
4143            AmpError::Validation(msg) => {
4144                assert!(
4145                    msg.contains("ELEMENTS_RPC_URL"),
4146                    "Error message should mention missing ELEMENTS_RPC_URL"
4147                );
4148            }
4149            _ => panic!("Expected validation error"),
4150        }
4151
4152        // Restore original values or keep removed if they weren't set
4153        if let Some(val) = original_url {
4154            env::set_var("ELEMENTS_RPC_URL", val);
4155        }
4156        if let Some(val) = original_user {
4157            env::set_var("ELEMENTS_RPC_USER", val);
4158        }
4159        if let Some(val) = original_password {
4160            env::set_var("ELEMENTS_RPC_PASSWORD", val);
4161        }
4162    }
4163
4164    #[test]
4165    #[serial]
4166    fn test_elements_rpc_from_env_success() {
4167        // Store original values to restore later
4168        let original_url = env::var("ELEMENTS_RPC_URL").ok();
4169        let original_user = env::var("ELEMENTS_RPC_USER").ok();
4170        let original_password = env::var("ELEMENTS_RPC_PASSWORD").ok();
4171
4172        // Set test values
4173        env::set_var("ELEMENTS_RPC_URL", "http://localhost:18884");
4174        env::set_var("ELEMENTS_RPC_USER", "testuser");
4175        env::set_var("ELEMENTS_RPC_PASSWORD", "testpass");
4176
4177        let result = ElementsRpc::from_env();
4178        assert!(
4179            result.is_ok(),
4180            "ElementsRpc::from_env() should succeed when all env vars are set"
4181        );
4182
4183        let rpc = result.unwrap();
4184        assert_eq!(rpc.base_url, "http://localhost:18884");
4185        assert_eq!(rpc.username, "testuser");
4186        assert_eq!(rpc.password, "testpass");
4187
4188        // Restore original values or remove if they weren't set
4189        match original_url {
4190            Some(val) => env::set_var("ELEMENTS_RPC_URL", val),
4191            None => env::remove_var("ELEMENTS_RPC_URL"),
4192        }
4193        match original_user {
4194            Some(val) => env::set_var("ELEMENTS_RPC_USER", val),
4195            None => env::remove_var("ELEMENTS_RPC_USER"),
4196        }
4197        match original_password {
4198            Some(val) => env::set_var("ELEMENTS_RPC_PASSWORD", val),
4199            None => env::remove_var("ELEMENTS_RPC_PASSWORD"),
4200        }
4201    }
4202
4203    #[test]
4204    fn test_elements_rpc_method_signatures() {
4205        // Test that all new methods have correct signatures and can be called
4206        let rpc = ElementsRpc::new(
4207            "http://localhost:18884".to_string(),
4208            "user".to_string(),
4209            "pass".to_string(),
4210        );
4211
4212        // Test that methods exist and have correct signatures (compilation test)
4213        let _: std::pin::Pin<
4214            Box<dyn std::future::Future<Output = Result<Vec<Unspent>, AmpError>> + Send + '_>,
4215        > = Box::pin(rpc.list_unspent(Some("test_asset")));
4216
4217        let inputs = vec![TxInput {
4218            txid: "test_txid".to_string(),
4219            vout: 0,
4220            sequence: None,
4221        }];
4222        let outputs = std::collections::HashMap::new();
4223        let assets = std::collections::HashMap::new();
4224
4225        let _: std::pin::Pin<
4226            Box<dyn std::future::Future<Output = Result<String, AmpError>> + Send + '_>,
4227        > = Box::pin(rpc.create_raw_transaction(inputs, outputs, assets));
4228
4229        let _: std::pin::Pin<
4230            Box<dyn std::future::Future<Output = Result<String, AmpError>> + Send + '_>,
4231        > = Box::pin(rpc.send_raw_transaction("test_hex"));
4232
4233        let _: std::pin::Pin<
4234            Box<dyn std::future::Future<Output = Result<TransactionDetail, AmpError>> + Send + '_>,
4235        > = Box::pin(rpc.get_transaction("test_txid"));
4236    }
4237
4238    // Mock RPC response tests for UTXO and transaction operations
4239
4240    #[tokio::test]
4241    async fn test_get_network_info_success() {
4242        let server = MockServer::start();
4243
4244        let mock_response = serde_json::json!({
4245            "jsonrpc": "1.0",
4246            "id": "amp-client",
4247            "result": {
4248                "version": 220000,
4249                "subversion": "/Liquid:22.0.0/",
4250                "protocolversion": 70016,
4251                "localservices": "0000000000000409",
4252                "localrelay": true,
4253                "timeoffset": 0,
4254                "networkactive": true,
4255                "connections": 8,
4256                "networks": [],
4257                "relayfee": 0.00001000,
4258                "incrementalfee": 0.00001000,
4259                "localaddresses": [],
4260                "warnings": ""
4261            }
4262        });
4263
4264        let mock = server.mock(|when, then| {
4265            when.method(POST)
4266                .path("/")
4267                .header("authorization", "Basic dXNlcjpwYXNz") // base64 of "user:pass"
4268                .json_body(serde_json::json!({
4269                    "jsonrpc": "1.0",
4270                    "id": "amp-client",
4271                    "method": "getnetworkinfo",
4272                    "params": []
4273                }));
4274            then.status(200)
4275                .header("content-type", "application/json")
4276                .json_body(mock_response);
4277        });
4278
4279        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
4280        let result = rpc.get_network_info().await;
4281
4282        assert!(result.is_ok());
4283        let network_info = result.unwrap();
4284        assert_eq!(network_info.version, 220000);
4285        assert_eq!(network_info.subversion, "/Liquid:22.0.0/");
4286        assert_eq!(network_info.connections, 8);
4287
4288        mock.assert();
4289    }
4290
4291    #[tokio::test]
4292    async fn test_get_blockchain_info_success() {
4293        let server = MockServer::start();
4294
4295        let mock_response = serde_json::json!({
4296            "jsonrpc": "1.0",
4297            "id": "amp-client",
4298            "result": {
4299                "chain": "liquidregtest",
4300                "blocks": 12345,
4301                "headers": 12345,
4302                "bestblockhash": "abc123def456789",
4303                "difficulty": 4.656542373906925e-10,
4304                "mediantime": 1640995200,
4305                "verificationprogress": 1.0,
4306                "initialblockdownload": false,
4307                "chainwork": "0000000000000000000000000000000000000000000000000000000000003039",
4308                "size_on_disk": 1234567,
4309                "pruned": false,
4310                "softforks": {},
4311                "warnings": ""
4312            }
4313        });
4314
4315        let mock = server.mock(|when, then| {
4316            when.method(POST)
4317                .path("/")
4318                .header("authorization", "Basic dXNlcjpwYXNz")
4319                .json_body(serde_json::json!({
4320                    "jsonrpc": "1.0",
4321                    "id": "amp-client",
4322                    "method": "getblockchaininfo",
4323                    "params": []
4324                }));
4325            then.status(200)
4326                .header("content-type", "application/json")
4327                .json_body(mock_response);
4328        });
4329
4330        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
4331        let result = rpc.get_blockchain_info().await;
4332
4333        assert!(result.is_ok());
4334        let blockchain_info = result.unwrap();
4335        assert_eq!(blockchain_info.chain, "liquidregtest");
4336        assert_eq!(blockchain_info.blocks, 12345);
4337        assert_eq!(blockchain_info.bestblockhash, "abc123def456789");
4338
4339        mock.assert();
4340    }
4341
4342    #[tokio::test]
4343    async fn test_list_unspent_with_asset_filter() {
4344        let server = MockServer::start();
4345
4346        let mock_response = serde_json::json!({
4347            "jsonrpc": "1.0",
4348            "id": "amp-client",
4349            "result": [
4350                {
4351                    "txid": "abc123def456789",
4352                    "vout": 0,
4353                    "amount": 100.0,
4354                    "asset": "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d",
4355                    "address": "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq",
4356                    "spendable": true,
4357                    "confirmations": 6,
4358                    "scriptpubkey": "76a914abc123def456789abc123def456789abc123de88ac"
4359                },
4360                {
4361                    "txid": "def456abc123789",
4362                    "vout": 1,
4363                    "amount": 50.0,
4364                    "asset": "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d",
4365                    "address": "lq1qq3xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq",
4366                    "spendable": true,
4367                    "confirmations": 3
4368                }
4369            ]
4370        });
4371
4372        let asset_id = "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d";
4373
4374        let mock = server.mock(|when, then| {
4375            when.method(POST)
4376                .path("/")
4377                .header("authorization", "Basic dXNlcjpwYXNz")
4378                .json_body(serde_json::json!({
4379                    "jsonrpc": "1.0",
4380                    "id": "amp-client",
4381                    "method": "listunspent",
4382                    "params": [1, 9999999, [], true, {"asset": asset_id}]
4383                }));
4384            then.status(200)
4385                .header("content-type", "application/json")
4386                .json_body(mock_response);
4387        });
4388
4389        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
4390        let result = rpc.list_unspent(Some(asset_id)).await;
4391
4392        assert!(result.is_ok());
4393        let utxos = result.unwrap();
4394        assert_eq!(utxos.len(), 2);
4395        assert_eq!(utxos[0].txid, "abc123def456789");
4396        assert_eq!(utxos[0].amount, 100.0);
4397        assert_eq!(utxos[0].asset, asset_id);
4398        assert_eq!(utxos[1].txid, "def456abc123789");
4399        assert_eq!(utxos[1].amount, 50.0);
4400
4401        mock.assert();
4402    }
4403
4404    #[tokio::test]
4405    async fn test_list_unspent_without_filter() {
4406        let server = MockServer::start();
4407
4408        let mock_response = serde_json::json!({
4409            "jsonrpc": "1.0",
4410            "id": "amp-client",
4411            "result": [
4412                {
4413                    "txid": "ghi789jkl012345",
4414                    "vout": 0,
4415                    "amount": 25.0,
4416                    "asset": "different_asset_id",
4417                    "address": "lq1qq4xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq",
4418                    "spendable": true,
4419                    "confirmations": 10
4420                }
4421            ]
4422        });
4423
4424        let mock = server.mock(|when, then| {
4425            when.method(POST)
4426                .path("/")
4427                .header("authorization", "Basic dXNlcjpwYXNz")
4428                .json_body(serde_json::json!({
4429                    "jsonrpc": "1.0",
4430                    "id": "amp-client",
4431                    "method": "listunspent",
4432                    "params": [1, 9999999, [], true]
4433                }));
4434            then.status(200)
4435                .header("content-type", "application/json")
4436                .json_body(mock_response);
4437        });
4438
4439        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
4440        let result = rpc.list_unspent(None).await;
4441
4442        assert!(result.is_ok());
4443        let utxos = result.unwrap();
4444        assert_eq!(utxos.len(), 1);
4445        assert_eq!(utxos[0].txid, "ghi789jkl012345");
4446        assert_eq!(utxos[0].amount, 25.0);
4447
4448        mock.assert();
4449    }
4450
4451    #[tokio::test]
4452    async fn test_create_raw_transaction_success() {
4453        let server = MockServer::start();
4454
4455        let mock_response = serde_json::json!({
4456            "jsonrpc": "1.0",
4457            "id": "amp-client",
4458            "result": "0200000000010abc123def456789abc123def456789abc123def456789abc123def456789abc123def456789000000006b483045022100..."
4459        });
4460
4461        let mock = server.mock(|when, then| {
4462            when.method(POST)
4463                .path("/")
4464                .header("authorization", "Basic dXNlcjpwYXNz")
4465                .json_body(serde_json::json!({
4466                    "jsonrpc": "1.0",
4467                    "id": "amp-client",
4468                    "method": "createrawtransaction",
4469                    "params": [
4470                        [
4471                            {
4472                                "txid": "input_txid_123",
4473                                "vout": 0,
4474                                "sequence": 4294967295u32
4475                            }
4476                        ],
4477                        {
4478                            "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq": 100.0
4479                        },
4480                        0,
4481                        false,
4482                        {
4483                            "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq": "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d"
4484                        }
4485                    ]
4486                }));
4487            then.status(200)
4488                .header("content-type", "application/json")
4489                .json_body(mock_response);
4490        });
4491
4492        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
4493
4494        let inputs = vec![TxInput {
4495            txid: "input_txid_123".to_string(),
4496            vout: 0,
4497            sequence: Some(0xffffffff),
4498        }];
4499
4500        let mut outputs = HashMap::new();
4501        outputs.insert(
4502            "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
4503            100.0,
4504        );
4505
4506        let mut assets = HashMap::new();
4507        assets.insert(
4508            "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
4509            "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d".to_string(),
4510        );
4511
4512        let result = rpc.create_raw_transaction(inputs, outputs, assets).await;
4513
4514        assert!(result.is_ok());
4515        let raw_tx = result.unwrap();
4516        assert!(raw_tx.starts_with("0200000000010abc123def456789"));
4517
4518        mock.assert();
4519    }
4520
4521    #[tokio::test]
4522    async fn test_send_raw_transaction_success() {
4523        let server = MockServer::start();
4524
4525        let mock_response = serde_json::json!({
4526            "jsonrpc": "1.0",
4527            "id": "amp-client",
4528            "result": "abc123def456789abc123def456789abc123def456789abc123def456789abc123de"
4529        });
4530
4531        let signed_tx_hex = "0200000000010abc123def456789abc123def456789abc123def456789abc123def456789abc123def456789000000006b483045022100...";
4532
4533        let mock = server.mock(|when, then| {
4534            when.method(POST)
4535                .path("/")
4536                .header("authorization", "Basic dXNlcjpwYXNz")
4537                .json_body(serde_json::json!({
4538                    "jsonrpc": "1.0",
4539                    "id": "amp-client",
4540                    "method": "sendrawtransaction",
4541                    "params": [signed_tx_hex]
4542                }));
4543            then.status(200)
4544                .header("content-type", "application/json")
4545                .json_body(mock_response);
4546        });
4547
4548        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
4549        let result = rpc.send_raw_transaction(signed_tx_hex).await;
4550
4551        assert!(result.is_ok());
4552        let txid = result.unwrap();
4553        assert_eq!(
4554            txid,
4555            "abc123def456789abc123def456789abc123def456789abc123def456789abc123de"
4556        );
4557
4558        mock.assert();
4559    }
4560
4561    #[tokio::test]
4562    async fn test_get_transaction_success() {
4563        let server = MockServer::start();
4564
4565        let mock_response = serde_json::json!({
4566            "jsonrpc": "1.0",
4567            "id": "amp-client",
4568            "result": {
4569                "txid": "abc123def456789abc123def456789abc123def456789abc123def456789abc123de",
4570                "confirmations": 6,
4571                "blockheight": 12345,
4572                "hex": "0200000000010abc123def456789...",
4573                "blockhash": "def456abc123789def456abc123789def456abc123789def456abc123789def456ab",
4574                "blocktime": 1640995200,
4575                "time": 1640995200,
4576                "timereceived": 1640995180
4577            }
4578        });
4579
4580        let txid = "abc123def456789abc123def456789abc123def456789abc123def456789abc123de";
4581
4582        let mock = server.mock(|when, then| {
4583            when.method(POST)
4584                .path("/")
4585                .header("authorization", "Basic dXNlcjpwYXNz")
4586                .json_body(serde_json::json!({
4587                    "jsonrpc": "1.0",
4588                    "id": "amp-client",
4589                    "method": "gettransaction",
4590                    "params": [txid, true]
4591                }));
4592            then.status(200)
4593                .header("content-type", "application/json")
4594                .json_body(mock_response);
4595        });
4596
4597        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
4598        let result = rpc.get_transaction(txid).await;
4599
4600        assert!(result.is_ok());
4601        let tx_detail = result.unwrap();
4602        assert_eq!(tx_detail.txid, txid);
4603        assert_eq!(tx_detail.confirmations, 6);
4604        assert_eq!(tx_detail.blockheight, Some(12345));
4605        assert_eq!(tx_detail.blocktime, Some(1640995200));
4606
4607        mock.assert();
4608    }
4609
4610    // Error handling tests
4611
4612    #[tokio::test]
4613    async fn test_rpc_call_network_failure() {
4614        // Use an invalid URL to simulate network failure
4615        let rpc = ElementsRpc::new(
4616            "http://invalid-host:99999".to_string(),
4617            "user".to_string(),
4618            "pass".to_string(),
4619        );
4620
4621        let result = rpc.get_network_info().await;
4622        assert!(result.is_err());
4623
4624        match result.unwrap_err() {
4625            AmpError::Rpc(msg) => {
4626                assert!(msg.contains("Failed to send RPC request"));
4627            }
4628            _ => panic!("Expected RPC error for network failure"),
4629        }
4630    }
4631
4632    #[tokio::test]
4633    async fn test_rpc_call_http_error_status() {
4634        let server = MockServer::start();
4635
4636        let mock = server.mock(|when, then| {
4637            when.method(POST).path("/");
4638            then.status(500)
4639                .header("content-type", "application/json")
4640                .body("Internal Server Error");
4641        });
4642
4643        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
4644        let result = rpc.get_network_info().await;
4645
4646        assert!(result.is_err());
4647        match result.unwrap_err() {
4648            AmpError::Rpc(msg) => {
4649                assert!(msg.contains("RPC request failed with status: 500"));
4650            }
4651            _ => panic!("Expected RPC error for HTTP error status"),
4652        }
4653
4654        mock.assert();
4655    }
4656
4657    #[tokio::test]
4658    async fn test_rpc_call_invalid_json_response() {
4659        let server = MockServer::start();
4660
4661        let mock = server.mock(|when, then| {
4662            when.method(POST).path("/");
4663            then.status(200)
4664                .header("content-type", "application/json")
4665                .body("invalid json response");
4666        });
4667
4668        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
4669        let result = rpc.get_network_info().await;
4670
4671        assert!(result.is_err());
4672        match result.unwrap_err() {
4673            AmpError::Rpc(msg) => {
4674                assert!(msg.contains("Failed to parse RPC response"));
4675            }
4676            _ => panic!("Expected RPC error for invalid JSON"),
4677        }
4678
4679        mock.assert();
4680    }
4681
4682    #[tokio::test]
4683    async fn test_rpc_call_error_response() {
4684        let server = MockServer::start();
4685
4686        let mock_response = serde_json::json!({
4687            "jsonrpc": "1.0",
4688            "id": "amp-client",
4689            "result": null,
4690            "error": {
4691                "code": -32601,
4692                "message": "Method not found"
4693            }
4694        });
4695
4696        let mock = server.mock(|when, then| {
4697            when.method(POST).path("/");
4698            then.status(200)
4699                .header("content-type", "application/json")
4700                .json_body(mock_response);
4701        });
4702
4703        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
4704        let result = rpc.get_network_info().await;
4705
4706        assert!(result.is_err());
4707        match result.unwrap_err() {
4708            AmpError::Rpc(msg) => {
4709                assert!(msg.contains("RPC error -32601: Method not found"));
4710            }
4711            _ => panic!("Expected RPC error for error response"),
4712        }
4713
4714        mock.assert();
4715    }
4716
4717    #[tokio::test]
4718    async fn test_rpc_call_missing_result() {
4719        let server = MockServer::start();
4720
4721        let mock_response = serde_json::json!({
4722            "jsonrpc": "1.0",
4723            "id": "amp-client",
4724            "result": null,
4725            "error": null
4726        });
4727
4728        let mock = server.mock(|when, then| {
4729            when.method(POST).path("/");
4730            then.status(200)
4731                .header("content-type", "application/json")
4732                .json_body(mock_response);
4733        });
4734
4735        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
4736        let result = rpc.get_network_info().await;
4737
4738        assert!(result.is_err());
4739        match result.unwrap_err() {
4740            AmpError::Rpc(msg) => {
4741                assert!(msg.contains("RPC response missing result field"));
4742            }
4743            _ => panic!("Expected RPC error for missing result"),
4744        }
4745
4746        mock.assert();
4747    }
4748
4749    // Authentication tests
4750
4751    #[tokio::test]
4752    async fn test_rpc_authentication_headers() {
4753        let server = MockServer::start();
4754
4755        let mock_response = serde_json::json!({
4756            "jsonrpc": "1.0",
4757            "id": "amp-client",
4758            "result": {
4759                "version": 220000,
4760                "subversion": "/Liquid:22.0.0/",
4761                "protocolversion": 70016,
4762                "localservices": "0000000000000409",
4763                "localrelay": true,
4764                "timeoffset": 0,
4765                "networkactive": true,
4766                "connections": 8,
4767                "networks": [],
4768                "relayfee": 0.00001000,
4769                "incrementalfee": 0.00001000,
4770                "localaddresses": [],
4771                "warnings": ""
4772            }
4773        });
4774
4775        // Test with custom username and password
4776        let mock = server.mock(|when, then| {
4777            when.method(POST)
4778                .path("/")
4779                .header("authorization", "Basic dGVzdHVzZXI6dGVzdHBhc3M=") // base64 of "testuser:testpass"
4780                .json_body(serde_json::json!({
4781                    "jsonrpc": "1.0",
4782                    "id": "amp-client",
4783                    "method": "getnetworkinfo",
4784                    "params": []
4785                }));
4786            then.status(200)
4787                .header("content-type", "application/json")
4788                .json_body(mock_response);
4789        });
4790
4791        let rpc = ElementsRpc::new(
4792            server.url("/"),
4793            "testuser".to_string(),
4794            "testpass".to_string(),
4795        );
4796        let result = rpc.get_network_info().await;
4797
4798        assert!(result.is_ok());
4799        mock.assert();
4800    }
4801
4802    // Wallet passphrase tests
4803
4804    #[tokio::test]
4805    async fn test_wallet_passphrase_success() {
4806        let server = MockServer::start();
4807
4808        let mock_response = serde_json::json!({
4809            "jsonrpc": "1.0",
4810            "id": "amp-client",
4811            "result": null
4812        });
4813
4814        let mock = server.mock(|when, then| {
4815            when.method(POST)
4816                .path("/")
4817                .header("authorization", "Basic dXNlcjpwYXNz")
4818                .json_body(serde_json::json!({
4819                    "jsonrpc": "1.0",
4820                    "id": "amp-client",
4821                    "method": "walletpassphrase",
4822                    "params": ["my_passphrase", 300]
4823                }));
4824            then.status(200)
4825                .header("content-type", "application/json")
4826                .json_body(mock_response);
4827        });
4828
4829        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
4830        let result = rpc.wallet_passphrase("my_passphrase", 300).await;
4831
4832        assert!(result.is_ok());
4833        mock.assert();
4834    }
4835
4836    // Connection validation tests
4837
4838    #[tokio::test]
4839    async fn test_validate_connection_success() {
4840        let server = MockServer::start();
4841
4842        let mock_response = serde_json::json!({
4843            "jsonrpc": "1.0",
4844            "id": "amp-client",
4845            "result": {
4846                "version": 220000,
4847                "subversion": "/Liquid:22.0.0/",
4848                "protocolversion": 70016,
4849                "localservices": "0000000000000409",
4850                "localrelay": true,
4851                "timeoffset": 0,
4852                "networkactive": true,
4853                "connections": 8,
4854                "networks": [],
4855                "relayfee": 0.00001000,
4856                "incrementalfee": 0.00001000,
4857                "localaddresses": [],
4858                "warnings": ""
4859            }
4860        });
4861
4862        let mock = server.mock(|when, then| {
4863            when.method(POST).path("/");
4864            then.status(200)
4865                .header("content-type", "application/json")
4866                .json_body(mock_response);
4867        });
4868
4869        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
4870        let result = rpc.validate_connection().await;
4871
4872        assert!(result.is_ok());
4873        mock.assert();
4874    }
4875
4876    #[tokio::test]
4877    async fn test_get_node_status_success() {
4878        let server = MockServer::start();
4879
4880        let network_mock_response = serde_json::json!({
4881            "jsonrpc": "1.0",
4882            "id": "amp-client",
4883            "result": {
4884                "version": 220000,
4885                "subversion": "/Liquid:22.0.0/",
4886                "protocolversion": 70016,
4887                "localservices": "0000000000000409",
4888                "localrelay": true,
4889                "timeoffset": 0,
4890                "networkactive": true,
4891                "connections": 8,
4892                "networks": [],
4893                "relayfee": 0.00001000,
4894                "incrementalfee": 0.00001000,
4895                "localaddresses": [],
4896                "warnings": ""
4897            }
4898        });
4899
4900        let blockchain_mock_response = serde_json::json!({
4901            "jsonrpc": "1.0",
4902            "id": "amp-client",
4903            "result": {
4904                "chain": "liquidregtest",
4905                "blocks": 12345,
4906                "headers": 12345,
4907                "bestblockhash": "abc123def456789",
4908                "difficulty": 4.656542373906925e-10,
4909                "mediantime": 1640995200,
4910                "verificationprogress": 1.0,
4911                "initialblockdownload": false,
4912                "chainwork": "0000000000000000000000000000000000000000000000000000000000003039",
4913                "size_on_disk": 1234567,
4914                "pruned": false,
4915                "softforks": {},
4916                "warnings": ""
4917            }
4918        });
4919
4920        let network_mock = server.mock(|when, then| {
4921            when.method(POST).path("/").json_body(serde_json::json!({
4922                "jsonrpc": "1.0",
4923                "id": "amp-client",
4924                "method": "getnetworkinfo",
4925                "params": []
4926            }));
4927            then.status(200)
4928                .header("content-type", "application/json")
4929                .json_body(network_mock_response);
4930        });
4931
4932        let blockchain_mock = server.mock(|when, then| {
4933            when.method(POST).path("/").json_body(serde_json::json!({
4934                "jsonrpc": "1.0",
4935                "id": "amp-client",
4936                "method": "getblockchaininfo",
4937                "params": []
4938            }));
4939            then.status(200)
4940                .header("content-type", "application/json")
4941                .json_body(blockchain_mock_response);
4942        });
4943
4944        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
4945        let result = rpc.get_node_status().await;
4946
4947        assert!(result.is_ok());
4948        let (network_info, blockchain_info) = result.unwrap();
4949        assert_eq!(network_info.version, 220000);
4950        assert_eq!(blockchain_info.blocks, 12345);
4951
4952        network_mock.assert();
4953        blockchain_mock.assert();
4954    }
4955
4956    // Tests for UTXO selection and transaction building logic
4957
4958    #[tokio::test]
4959    async fn test_build_distribution_transaction_zero_amount() {
4960        let rpc = ElementsRpc::new(
4961            "http://localhost:18884".to_string(),
4962            "user".to_string(),
4963            "pass".to_string(),
4964        );
4965
4966        let address_amounts = HashMap::new(); // Empty distribution
4967
4968        let result = rpc
4969            .build_distribution_transaction(
4970                "test_wallet",
4971                "asset_id",
4972                address_amounts,
4973                "change_address",
4974                1.0,
4975            )
4976            .await;
4977
4978        assert!(result.is_err());
4979        match result.unwrap_err() {
4980            AmpError::Validation(msg) => {
4981                assert!(msg.contains("Total distribution amount must be greater than zero"));
4982            }
4983            _ => panic!("Expected validation error for zero distribution amount"),
4984        }
4985    }
4986
4987    #[tokio::test]
4988    async fn test_sign_transaction_validation() {
4989        let rpc = ElementsRpc::new(
4990            "http://localhost:18884".to_string(),
4991            "user".to_string(),
4992            "pass".to_string(),
4993        );
4994
4995        // Mock signer for testing
4996        struct MockSigner {
4997            should_fail: bool,
4998            return_value: String,
4999        }
5000
5001        #[async_trait::async_trait]
5002        impl crate::signer::Signer for MockSigner {
5003            async fn sign_transaction(
5004                &self,
5005                _unsigned_tx: &str,
5006            ) -> Result<String, crate::signer::SignerError> {
5007                if self.should_fail {
5008                    Err(crate::signer::SignerError::Lwk(
5009                        "Mock signing failure".to_string(),
5010                    ))
5011                } else {
5012                    // Return a longer hex string to simulate signed transaction (20+ bytes when decoded)
5013                    Ok(format!(
5014                        "{}deadbeefcafebabe1234567890abcdef",
5015                        self.return_value
5016                    ))
5017                }
5018            }
5019
5020            fn as_any(&self) -> &dyn std::any::Any {
5021                self
5022            }
5023        }
5024
5025        // Test empty transaction hex
5026        let mock_signer = MockSigner {
5027            should_fail: false,
5028            return_value: "".to_string(),
5029        };
5030        let result = rpc.sign_transaction("", &mock_signer).await;
5031        assert!(result.is_err());
5032        assert!(result.unwrap_err().to_string().contains("cannot be empty"));
5033
5034        // Test odd length hex
5035        let result = rpc.sign_transaction("abc", &mock_signer).await;
5036        assert!(result.is_err());
5037        assert!(result.unwrap_err().to_string().contains("even length"));
5038
5039        // Test invalid hex characters
5040        let result = rpc.sign_transaction("abcg", &mock_signer).await;
5041        assert!(result.is_err());
5042        assert!(result
5043            .unwrap_err()
5044            .to_string()
5045            .contains("invalid hex characters"));
5046
5047        // Test signer failure
5048        let mock_signer = MockSigner {
5049            should_fail: true,
5050            return_value: "".to_string(),
5051        };
5052        let result = rpc.sign_transaction("abcd", &mock_signer).await;
5053        assert!(result.is_err());
5054        assert!(result
5055            .unwrap_err()
5056            .to_string()
5057            .contains("Mock signing failure"));
5058
5059        // Test successful signing
5060        let mock_signer = MockSigner {
5061            should_fail: false,
5062            return_value: "abcd".to_string(),
5063        };
5064        let result = rpc.sign_transaction("abcd", &mock_signer).await;
5065        if result.is_err() {
5066            println!("Error: {}", result.as_ref().unwrap_err());
5067        }
5068        assert!(result.is_ok());
5069        assert_eq!(result.unwrap(), "abcddeadbeefcafebabe1234567890abcdef");
5070    }
5071
5072    #[tokio::test]
5073    async fn test_sign_transaction_validation_edge_cases() {
5074        let rpc = ElementsRpc::new(
5075            "http://localhost:18884".to_string(),
5076            "user".to_string(),
5077            "pass".to_string(),
5078        );
5079
5080        // Mock signer that returns invalid responses
5081        struct BadMockSigner {
5082            return_empty: bool,
5083            return_odd_length: bool,
5084            return_invalid_hex: bool,
5085            return_shorter: bool,
5086        }
5087
5088        #[async_trait::async_trait]
5089        impl crate::signer::Signer for BadMockSigner {
5090            async fn sign_transaction(
5091                &self,
5092                unsigned_tx: &str,
5093            ) -> Result<String, crate::signer::SignerError> {
5094                if self.return_empty {
5095                    Ok("".to_string())
5096                } else if self.return_odd_length {
5097                    Ok("abc".to_string())
5098                } else if self.return_invalid_hex {
5099                    Ok("abcg".to_string())
5100                } else if self.return_shorter {
5101                    Ok("ab".to_string()) // Shorter than input "abcd"
5102                } else {
5103                    Ok(format!("{}deadbeef", unsigned_tx))
5104                }
5105            }
5106
5107            fn as_any(&self) -> &dyn std::any::Any {
5108                self
5109            }
5110        }
5111
5112        // Test signer returning empty string
5113        let bad_signer = BadMockSigner {
5114            return_empty: true,
5115            return_odd_length: false,
5116            return_invalid_hex: false,
5117            return_shorter: false,
5118        };
5119        let result = rpc.sign_transaction("abcd", &bad_signer).await;
5120        assert!(result.is_err());
5121        assert!(result.unwrap_err().to_string().contains("cannot be empty"));
5122
5123        // Test signer returning odd length hex
5124        let bad_signer = BadMockSigner {
5125            return_empty: false,
5126            return_odd_length: true,
5127            return_invalid_hex: false,
5128            return_shorter: false,
5129        };
5130        let result = rpc.sign_transaction("abcd", &bad_signer).await;
5131        assert!(result.is_err());
5132        assert!(result.unwrap_err().to_string().contains("even length"));
5133
5134        // Test signer returning invalid hex
5135        let bad_signer = BadMockSigner {
5136            return_empty: false,
5137            return_odd_length: false,
5138            return_invalid_hex: true,
5139            return_shorter: false,
5140        };
5141        let result = rpc.sign_transaction("abcd", &bad_signer).await;
5142        assert!(result.is_err());
5143        assert!(result
5144            .unwrap_err()
5145            .to_string()
5146            .contains("invalid hex characters"));
5147
5148        // Test signer returning shorter transaction (invalid)
5149        let bad_signer = BadMockSigner {
5150            return_empty: false,
5151            return_odd_length: false,
5152            return_invalid_hex: false,
5153            return_shorter: true,
5154        };
5155        let result = rpc.sign_transaction("abcd", &bad_signer).await;
5156        assert!(result.is_err());
5157        assert!(result
5158            .unwrap_err()
5159            .to_string()
5160            .contains("shorter than unsigned transaction"));
5161    }
5162
5163    #[tokio::test]
5164    async fn test_sign_transaction_minimum_size_validation() {
5165        let rpc = ElementsRpc::new(
5166            "http://localhost:18884".to_string(),
5167            "user".to_string(),
5168            "pass".to_string(),
5169        );
5170
5171        // Mock signer that returns very small transactions
5172        struct TinyMockSigner;
5173
5174        #[async_trait::async_trait]
5175        impl crate::signer::Signer for TinyMockSigner {
5176            async fn sign_transaction(
5177                &self,
5178                _unsigned_tx: &str,
5179            ) -> Result<String, crate::signer::SignerError> {
5180                Ok("abcd".to_string()) // Only 2 bytes when decoded
5181            }
5182
5183            fn as_any(&self) -> &dyn std::any::Any {
5184                self
5185            }
5186        }
5187
5188        let tiny_signer = TinyMockSigner;
5189        let result = rpc.sign_transaction("abcd", &tiny_signer).await;
5190        assert!(result.is_err());
5191        let error_msg = result.unwrap_err().to_string();
5192        assert!(error_msg.contains("minimum size"));
5193        assert!(error_msg.contains("minimum is 10 bytes"));
5194    }
5195
5196    #[tokio::test]
5197    async fn test_sign_transaction_success_case() {
5198        let rpc = ElementsRpc::new(
5199            "http://localhost:18884".to_string(),
5200            "user".to_string(),
5201            "pass".to_string(),
5202        );
5203
5204        // Mock signer that returns a valid signed transaction
5205        struct GoodMockSigner;
5206
5207        #[async_trait::async_trait]
5208        impl crate::signer::Signer for GoodMockSigner {
5209            async fn sign_transaction(
5210                &self,
5211                unsigned_tx: &str,
5212            ) -> Result<String, crate::signer::SignerError> {
5213                // Return a longer valid hex string (20+ bytes when decoded)
5214                Ok(format!("{}deadbeefcafebabe1234567890abcdef", unsigned_tx))
5215            }
5216
5217            fn as_any(&self) -> &dyn std::any::Any {
5218                self
5219            }
5220        }
5221
5222        let good_signer = GoodMockSigner;
5223
5224        // Test with a reasonable sized unsigned transaction
5225        let unsigned_tx = "0200000000010123456789abcdef"; // 14 bytes when decoded
5226        let result = rpc.sign_transaction(unsigned_tx, &good_signer).await;
5227
5228        assert!(result.is_ok());
5229        let signed_tx = result.unwrap();
5230        assert!(signed_tx.starts_with(unsigned_tx));
5231        assert!(signed_tx.len() > unsigned_tx.len());
5232        assert!(signed_tx.contains("deadbeefcafebabe"));
5233    }
5234
5235    #[tokio::test]
5236    async fn test_sign_and_broadcast_transaction_mock() {
5237        // Create a mock server for testing the broadcast part
5238        let server = MockServer::start();
5239
5240        // Mock the RPC response for sendrawtransaction
5241        let mock = server.mock(|when, then| {
5242            when.method(POST).path("/").json_body(serde_json::json!({
5243                "jsonrpc": "1.0",
5244                "id": "amp-client",
5245                "method": "sendrawtransaction",
5246                "params": ["0200000000010123456789abcdefdeadbeefcafebabe1234567890abcdef"]
5247            }));
5248            then.status(200).json_body(serde_json::json!({
5249                "jsonrpc": "1.0",
5250                "id": "amp-client",
5251                "result": "abc123def456789",
5252                "error": null
5253            }));
5254        });
5255
5256        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5257
5258        // Mock signer for testing
5259        struct TestMockSigner;
5260
5261        #[async_trait::async_trait]
5262        impl crate::signer::Signer for TestMockSigner {
5263            async fn sign_transaction(
5264                &self,
5265                unsigned_tx: &str,
5266            ) -> Result<String, crate::signer::SignerError> {
5267                Ok(format!("{}deadbeefcafebabe1234567890abcdef", unsigned_tx))
5268            }
5269
5270            fn as_any(&self) -> &dyn std::any::Any {
5271                self
5272            }
5273        }
5274
5275        let signer = TestMockSigner;
5276        let unsigned_tx = "0200000000010123456789abcdef";
5277
5278        let result = rpc
5279            .sign_and_broadcast_transaction(unsigned_tx, &signer)
5280            .await;
5281
5282        assert!(result.is_ok());
5283        assert_eq!(result.unwrap(), "abc123def456789");
5284
5285        // Verify the mock was called
5286        mock.assert();
5287    }
5288
5289    #[tokio::test]
5290    async fn test_sign_and_broadcast_transaction_signing_failure() {
5291        let rpc = ElementsRpc::new(
5292            "http://localhost:18884".to_string(),
5293            "user".to_string(),
5294            "pass".to_string(),
5295        );
5296
5297        // Mock signer that fails
5298        struct FailingSigner;
5299
5300        #[async_trait::async_trait]
5301        impl crate::signer::Signer for FailingSigner {
5302            async fn sign_transaction(
5303                &self,
5304                _unsigned_tx: &str,
5305            ) -> Result<String, crate::signer::SignerError> {
5306                Err(crate::signer::SignerError::Lwk(
5307                    "Signing failed".to_string(),
5308                ))
5309            }
5310
5311            fn as_any(&self) -> &dyn std::any::Any {
5312                self
5313            }
5314        }
5315
5316        let failing_signer = FailingSigner;
5317        let result = rpc
5318            .sign_and_broadcast_transaction("abcd", &failing_signer)
5319            .await;
5320
5321        assert!(result.is_err());
5322        let error_msg = result.unwrap_err().to_string();
5323        // The error should be a Signer error containing the original failure message
5324        assert!(error_msg.contains("Signer error"));
5325        assert!(error_msg.contains("Signing failed"));
5326    }
5327
5328    #[tokio::test]
5329    async fn test_sign_and_broadcast_transaction_broadcast_failure() {
5330        // Create a mock server that returns an error for broadcast
5331        let server = MockServer::start();
5332
5333        let mock = server.mock(|when, then| {
5334            when.method(POST).path("/");
5335            then.status(200).json_body(serde_json::json!({
5336                "jsonrpc": "1.0",
5337                "id": "amp-client",
5338                "result": null,
5339                "error": {
5340                    "code": -26,
5341                    "message": "Transaction rejected"
5342                }
5343            }));
5344        });
5345
5346        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5347
5348        // Mock signer that succeeds
5349        struct WorkingSigner;
5350
5351        #[async_trait::async_trait]
5352        impl crate::signer::Signer for WorkingSigner {
5353            async fn sign_transaction(
5354                &self,
5355                unsigned_tx: &str,
5356            ) -> Result<String, crate::signer::SignerError> {
5357                Ok(format!("{}deadbeefcafebabe1234567890abcdef", unsigned_tx))
5358            }
5359
5360            fn as_any(&self) -> &dyn std::any::Any {
5361                self
5362            }
5363        }
5364
5365        let working_signer = WorkingSigner;
5366        let unsigned_tx = "0200000000010123456789abcdef";
5367
5368        let result = rpc
5369            .sign_and_broadcast_transaction(unsigned_tx, &working_signer)
5370            .await;
5371
5372        assert!(result.is_err());
5373        let error_msg = result.unwrap_err().to_string();
5374        assert!(error_msg.contains("Failed during transaction broadcast phase"));
5375        assert!(error_msg.contains("Transaction rejected"));
5376
5377        mock.assert();
5378    }
5379
5380    #[tokio::test]
5381    async fn test_wait_for_confirmations_success() {
5382        let server = MockServer::start();
5383
5384        let txid = "abc123def456789abc123def456789abc123def456789abc123def456789abc123de";
5385
5386        // First call returns 1 confirmation (not enough)
5387        let _mock_response_1 = serde_json::json!({
5388            "jsonrpc": "1.0",
5389            "id": "amp-client",
5390            "result": {
5391                "txid": txid,
5392                "confirmations": 1,
5393                "blockheight": 12345,
5394                "hex": "0200000000010abc123def456789...",
5395                "blockhash": "def456abc123789def456abc123789def456abc123789def456abc123789def456ab",
5396                "blocktime": 1640995200,
5397                "time": 1640995200,
5398                "timereceived": 1640995180
5399            }
5400        });
5401
5402        // Second call returns 2 confirmations (sufficient)
5403        let mock_response_2 = serde_json::json!({
5404            "jsonrpc": "1.0",
5405            "id": "amp-client",
5406            "result": {
5407                "txid": txid,
5408                "confirmations": 2,
5409                "blockheight": 12345,
5410                "hex": "0200000000010abc123def456789...",
5411                "blockhash": "def456abc123789def456abc123789def456abc123789def456abc123789def456ab",
5412                "blocktime": 1640995200,
5413                "time": 1640995200,
5414                "timereceived": 1640995180
5415            }
5416        });
5417
5418        // Create a mock that returns 2 confirmations immediately (simpler test)
5419        let mock = server.mock(|when, then| {
5420            when.method(POST)
5421                .path("/")
5422                .header("authorization", "Basic dXNlcjpwYXNz")
5423                .json_body(serde_json::json!({
5424                    "jsonrpc": "1.0",
5425                    "id": "amp-client",
5426                    "method": "gettransaction",
5427                    "params": [txid, true]
5428                }));
5429            then.status(200)
5430                .header("content-type", "application/json")
5431                .json_body(mock_response_2); // Return sufficient confirmations immediately
5432        });
5433
5434        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5435
5436        // Use fast polling (1 second) for testing
5437        let result = rpc
5438            .wait_for_confirmations_with_interval(txid, Some(2), Some(1), Some(1))
5439            .await;
5440
5441        assert!(result.is_ok());
5442        let tx_detail = result.unwrap();
5443        assert_eq!(tx_detail.confirmations, 2);
5444        assert_eq!(tx_detail.txid, txid);
5445
5446        // Mock should have been called once
5447        mock.assert();
5448    }
5449
5450    #[tokio::test]
5451    async fn test_wait_for_confirmations_timeout() {
5452        let server = MockServer::start();
5453
5454        let txid = "abc123def456789abc123def456789abc123def456789abc123def456789abc123de";
5455
5456        // Always return insufficient confirmations
5457        let mock_response = serde_json::json!({
5458            "jsonrpc": "1.0",
5459            "id": "amp-client",
5460            "result": {
5461                "txid": txid,
5462                "confirmations": 1,
5463                "blockheight": 12345,
5464                "hex": "0200000000010abc123def456789...",
5465                "blockhash": null,
5466                "blocktime": null,
5467                "time": null,
5468                "timereceived": null
5469            }
5470        });
5471
5472        let _mock = server.mock(|when, then| {
5473            when.method(POST)
5474                .path("/")
5475                .header("authorization", "Basic dXNlcjpwYXNz")
5476                .json_body(serde_json::json!({
5477                    "jsonrpc": "1.0",
5478                    "id": "amp-client",
5479                    "method": "gettransaction",
5480                    "params": [txid, true]
5481                }));
5482            then.status(200)
5483                .header("content-type", "application/json")
5484                .json_body(mock_response);
5485        });
5486
5487        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5488
5489        // Use a very short timeout for testing (0 = 3 seconds) and fast polling (1 second)
5490        let result = rpc
5491            .wait_for_confirmations_with_interval(txid, Some(2), Some(0), Some(1))
5492            .await;
5493
5494        assert!(result.is_err());
5495        match result.unwrap_err() {
5496            AmpError::Timeout(msg) => {
5497                assert!(msg.contains("Timeout waiting for confirmations"));
5498                assert!(msg.contains(txid));
5499                assert!(msg.contains("retry confirmation"));
5500            }
5501            _ => panic!("Expected timeout error"),
5502        }
5503
5504        // Mock will be called multiple times during the timeout period
5505        // We don't assert on the exact number since it depends on timing
5506    }
5507
5508    #[tokio::test]
5509    async fn test_wait_for_confirmations_immediate_success() {
5510        let server = MockServer::start();
5511
5512        let txid = "abc123def456789abc123def456789abc123def456789abc123def456789abc123de";
5513
5514        // Transaction already has sufficient confirmations
5515        let mock_response = serde_json::json!({
5516            "jsonrpc": "1.0",
5517            "id": "amp-client",
5518            "result": {
5519                "txid": txid,
5520                "confirmations": 5,
5521                "blockheight": 12345,
5522                "hex": "0200000000010abc123def456789...",
5523                "blockhash": "def456abc123789def456abc123789def456abc123789def456abc123789def456ab",
5524                "blocktime": 1640995200,
5525                "time": 1640995200,
5526                "timereceived": 1640995180
5527            }
5528        });
5529
5530        let mock = server.mock(|when, then| {
5531            when.method(POST)
5532                .path("/")
5533                .header("authorization", "Basic dXNlcjpwYXNz")
5534                .json_body(serde_json::json!({
5535                    "jsonrpc": "1.0",
5536                    "id": "amp-client",
5537                    "method": "gettransaction",
5538                    "params": [txid, true]
5539                }));
5540            then.status(200)
5541                .header("content-type", "application/json")
5542                .json_body(mock_response);
5543        });
5544
5545        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5546
5547        let result = rpc.wait_for_confirmations(txid, Some(2), Some(10)).await;
5548
5549        assert!(result.is_ok());
5550        let tx_detail = result.unwrap();
5551        assert_eq!(tx_detail.confirmations, 5);
5552        assert_eq!(tx_detail.txid, txid);
5553
5554        // Should only need one call since confirmations are already sufficient
5555        mock.assert();
5556    }
5557}
5558
5559/// Configuration for retry behavior in API requests
5560#[derive(Debug, Clone)]
5561pub struct RetryConfig {
5562    /// Maximum number of retry attempts
5563    pub max_attempts: u32,
5564    /// Base delay in milliseconds for exponential backoff
5565    pub base_delay_ms: u64,
5566    /// Maximum delay in milliseconds to cap exponential backoff
5567    pub max_delay_ms: u64,
5568    /// Request timeout in seconds
5569    pub timeout_seconds: u64,
5570}
5571
5572impl Default for RetryConfig {
5573    fn default() -> Self {
5574        Self {
5575            max_attempts: 3,
5576            base_delay_ms: 1000,
5577            max_delay_ms: 30_000,
5578            timeout_seconds: 10,
5579        }
5580    }
5581}
5582
5583impl RetryConfig {
5584    /// Creates a `RetryConfig` from environment variables with default fallbacks
5585    ///
5586    /// Environment variables:
5587    /// - `API_RETRY_MAX_ATTEMPTS`: Maximum retry attempts (default: 3)
5588    /// - `API_RETRY_BASE_DELAY_MS`: Base delay in milliseconds (default: 1000)
5589    /// - `API_RETRY_MAX_DELAY_MS`: Maximum delay in milliseconds (default: 30000)
5590    /// - `API_REQUEST_TIMEOUT_SECONDS`: Request timeout in seconds (default: 10)
5591    ///
5592    /// # Errors
5593    ///
5594    /// Returns an error if any environment variable contains an invalid value
5595    pub fn from_env() -> Result<Self, Error> {
5596        let max_attempts = match env::var("API_RETRY_MAX_ATTEMPTS") {
5597            Ok(val) => val.parse::<u32>().map_err(|e| {
5598                Error::InvalidRetryConfig(format!("Invalid API_RETRY_MAX_ATTEMPTS: {e}"))
5599            })?,
5600            Err(_) => 3,
5601        };
5602
5603        let base_delay_ms = match env::var("API_RETRY_BASE_DELAY_MS") {
5604            Ok(val) => val.parse::<u64>().map_err(|e| {
5605                Error::InvalidRetryConfig(format!("Invalid API_RETRY_BASE_DELAY_MS: {e}"))
5606            })?,
5607            Err(_) => 1000,
5608        };
5609
5610        let max_delay_ms = match env::var("API_RETRY_MAX_DELAY_MS") {
5611            Ok(val) => val.parse::<u64>().map_err(|e| {
5612                Error::InvalidRetryConfig(format!("Invalid API_RETRY_MAX_DELAY_MS: {e}"))
5613            })?,
5614            Err(_) => 30_000,
5615        };
5616
5617        let timeout_seconds = match env::var("API_REQUEST_TIMEOUT_SECONDS") {
5618            Ok(val) => val.parse::<u64>().map_err(|e| {
5619                Error::InvalidRetryConfig(format!("Invalid API_REQUEST_TIMEOUT_SECONDS: {e}"))
5620            })?,
5621            Err(_) => 10,
5622        };
5623
5624        // Validate configuration
5625        if max_attempts == 0 {
5626            return Err(Error::InvalidRetryConfig(
5627                "max_attempts must be greater than 0".to_string(),
5628            ));
5629        }
5630        if base_delay_ms == 0 {
5631            return Err(Error::InvalidRetryConfig(
5632                "base_delay_ms must be greater than 0".to_string(),
5633            ));
5634        }
5635        if max_delay_ms < base_delay_ms {
5636            return Err(Error::InvalidRetryConfig(
5637                "max_delay_ms must be greater than or equal to base_delay_ms".to_string(),
5638            ));
5639        }
5640        if timeout_seconds == 0 {
5641            return Err(Error::InvalidRetryConfig(
5642                "timeout_seconds must be greater than 0".to_string(),
5643            ));
5644        }
5645
5646        Ok(Self {
5647            max_attempts,
5648            base_delay_ms,
5649            max_delay_ms,
5650            timeout_seconds,
5651        })
5652    }
5653
5654    /// Creates a `RetryConfig` optimized for test environments
5655    ///
5656    /// Uses reduced values for faster test execution:
5657    /// - 2 retry attempts
5658    /// - 500ms base delay
5659    /// - 5000ms max delay
5660    /// - 5 second timeout
5661    #[must_use]
5662    pub const fn for_tests() -> Self {
5663        Self {
5664            max_attempts: 2,
5665            base_delay_ms: 500,
5666            max_delay_ms: 5000,
5667            timeout_seconds: 5,
5668        }
5669    }
5670
5671    /// Sets a custom timeout value
5672    #[must_use]
5673    pub const fn with_timeout(mut self, timeout_seconds: u64) -> Self {
5674        self.timeout_seconds = timeout_seconds;
5675        self
5676    }
5677
5678    /// Sets custom max attempts
5679    #[must_use]
5680    pub const fn with_max_attempts(mut self, max_attempts: u32) -> Self {
5681        self.max_attempts = max_attempts;
5682        self
5683    }
5684
5685    /// Sets custom base delay
5686    #[must_use]
5687    pub const fn with_base_delay_ms(mut self, base_delay_ms: u64) -> Self {
5688        self.base_delay_ms = base_delay_ms;
5689        self
5690    }
5691
5692    /// Sets custom max delay
5693    #[must_use]
5694    pub const fn with_max_delay_ms(mut self, max_delay_ms: u64) -> Self {
5695        self.max_delay_ms = max_delay_ms;
5696        self
5697    }
5698}
5699
5700/// HTTP client with sophisticated retry logic and exponential backoff
5701#[derive(Debug, Clone)]
5702pub struct RetryClient {
5703    client: Client,
5704    config: RetryConfig,
5705}
5706
5707impl RetryClient {
5708    /// Creates a new `RetryClient` with the given configuration
5709    #[must_use]
5710    pub fn new(config: RetryConfig) -> Self {
5711        Self {
5712            client: Client::new(),
5713            config,
5714        }
5715    }
5716
5717    /// Creates a new `RetryClient` with default configuration
5718    #[must_use]
5719    pub fn with_default_config() -> Self {
5720        Self::new(RetryConfig::default())
5721    }
5722
5723    /// Creates a new `RetryClient` with test-optimized configuration
5724    #[must_use]
5725    pub fn for_tests() -> Self {
5726        Self::new(RetryConfig::for_tests())
5727    }
5728
5729    /// Executes an HTTP request with retry logic and exponential backoff
5730    ///
5731    /// # Arguments
5732    /// * `request_builder` - A function that creates the request builder
5733    ///
5734    /// # Returns
5735    /// The response if successful, or an error after all retries are exhausted
5736    ///
5737    /// # Errors
5738    /// Returns `TokenError::Timeout` if the request times out
5739    /// Returns `TokenError::RateLimited` if rate limited and retries are exhausted
5740    /// Returns `TokenError::ObtainFailed` if all retry attempts fail
5741    #[allow(clippy::cognitive_complexity)]
5742    pub async fn execute_with_retry<F>(
5743        &self,
5744        request_builder: F,
5745    ) -> Result<reqwest::Response, TokenError>
5746    where
5747        F: Fn() -> reqwest::RequestBuilder + Send + Sync,
5748    {
5749        let mut last_error = String::new();
5750        let mut attempt = 0;
5751
5752        while attempt < self.config.max_attempts {
5753            attempt += 1;
5754
5755            // Create the request with timeout
5756            let request =
5757                request_builder().timeout(StdDuration::from_secs(self.config.timeout_seconds));
5758
5759            // Execute the request
5760            match request.send().await {
5761                Ok(response) => {
5762                    let status = response.status();
5763
5764                    // Handle rate limiting (429 Too Many Requests)
5765                    if status == reqwest::StatusCode::TOO_MANY_REQUESTS {
5766                        let retry_after = Self::extract_retry_after(&response).unwrap_or(60);
5767
5768                        tracing::warn!(
5769                            "Rate limited (429) on attempt {}/{}. Retry after {} seconds",
5770                            attempt,
5771                            self.config.max_attempts,
5772                            retry_after
5773                        );
5774
5775                        // If this is our last attempt, return the rate limit error
5776                        if attempt >= self.config.max_attempts {
5777                            return Err(TokenError::rate_limited(retry_after));
5778                        }
5779
5780                        // Wait for the rate limit period (or our max delay, whichever is smaller)
5781                        let delay_ms = std::cmp::min(retry_after * 1000, self.config.max_delay_ms);
5782                        sleep(StdDuration::from_millis(delay_ms)).await;
5783                        continue;
5784                    }
5785
5786                    // Handle other client errors (4xx) - these are generally not retryable
5787                    if status.is_client_error() && status != reqwest::StatusCode::TOO_MANY_REQUESTS
5788                    {
5789                        last_error = format!("Client error: {status}");
5790                        tracing::error!("Non-retryable client error: {}", status);
5791                        break;
5792                    }
5793
5794                    // Handle server errors (5xx) - these are retryable
5795                    if status.is_server_error() {
5796                        last_error = format!("Server error: {status}");
5797                        tracing::warn!(
5798                            "Server error {} on attempt {}/{}",
5799                            status,
5800                            attempt,
5801                            self.config.max_attempts
5802                        );
5803
5804                        if attempt < self.config.max_attempts {
5805                            let delay = self.calculate_backoff_delay(attempt);
5806                            sleep(delay).await;
5807                            continue;
5808                        }
5809                        break;
5810                    }
5811
5812                    // Success case
5813                    return Ok(response);
5814                }
5815                Err(e) => {
5816                    last_error = e.to_string();
5817
5818                    // Check if this is a timeout error
5819                    if e.is_timeout() {
5820                        tracing::warn!(
5821                            "Request timeout on attempt {}/{}",
5822                            attempt,
5823                            self.config.max_attempts
5824                        );
5825
5826                        if attempt >= self.config.max_attempts {
5827                            return Err(TokenError::timeout(self.config.timeout_seconds));
5828                        }
5829                    } else {
5830                        tracing::warn!(
5831                            "Request failed on attempt {}/{}: {}",
5832                            attempt,
5833                            self.config.max_attempts,
5834                            e
5835                        );
5836                    }
5837
5838                    // If we have more attempts, wait and retry
5839                    if attempt < self.config.max_attempts {
5840                        let delay = self.calculate_backoff_delay(attempt);
5841                        sleep(delay).await;
5842                    }
5843                }
5844            }
5845        }
5846
5847        // All retries exhausted
5848        Err(TokenError::obtain_failed(attempt, last_error))
5849    }
5850
5851    /// Calculates the delay for exponential backoff with jitter
5852    ///
5853    /// Uses the formula: `min(base_delay * 2^(attempt-1) + jitter, max_delay)`
5854    /// where jitter is a random value between 0 and `base_delay/2`
5855    pub fn calculate_backoff_delay(&self, attempt: u32) -> StdDuration {
5856        use rand::Rng;
5857
5858        let base_delay = self.config.base_delay_ms;
5859        let max_delay = self.config.max_delay_ms;
5860
5861        // Calculate exponential backoff: base_delay * 2^(attempt-1)
5862        let exponential_delay = base_delay * 2_u64.pow(attempt.saturating_sub(1));
5863
5864        // Add jitter (random value between 0 and base_delay/2)
5865        let jitter = rand::thread_rng().gen_range(0..=base_delay / 2);
5866        let total_delay = exponential_delay + jitter;
5867
5868        // Cap at max_delay
5869        let final_delay = std::cmp::min(total_delay, max_delay);
5870
5871        tracing::debug!(
5872            "Calculated backoff delay for attempt {}: {}ms (exponential: {}ms, jitter: {}ms, capped at: {}ms)",
5873            attempt,
5874            final_delay,
5875            exponential_delay,
5876            jitter,
5877            max_delay
5878        );
5879
5880        StdDuration::from_millis(final_delay)
5881    }
5882
5883    /// Extracts the Retry-After header value from a 429 response
5884    ///
5885    /// Returns the number of seconds to wait, or None if the header is not present
5886    /// or cannot be parsed
5887    fn extract_retry_after(response: &reqwest::Response) -> Option<u64> {
5888        response
5889            .headers()
5890            .get("retry-after")
5891            .and_then(|value| value.to_str().ok())
5892            .and_then(|s| s.parse::<u64>().ok())
5893    }
5894
5895    /// Gets the underlying reqwest client
5896    #[must_use]
5897    pub const fn client(&self) -> &Client {
5898        &self.client
5899    }
5900
5901    /// Gets the retry configuration
5902    #[must_use]
5903    pub const fn config(&self) -> &RetryConfig {
5904        &self.config
5905    }
5906}
5907
5908/// Singleton instance of the `TokenManager` for shared token storage across all `ApiClient` instances
5909static GLOBAL_TOKEN_MANAGER: OnceCell<Arc<TokenManager>> = OnceCell::const_new();
5910
5911/// Core token manager with proactive refresh and secure storage
5912#[derive(Debug)]
5913pub struct TokenManager {
5914    pub token_data: Arc<Mutex<Option<TokenData>>>,
5915    pub retry_client: RetryClient,
5916    base_url: Url,
5917    /// Semaphore to ensure only one token operation (obtain/refresh) happens at a time
5918    /// This prevents race conditions where multiple threads try to refresh/obtain simultaneously
5919    token_operation_semaphore: Arc<Semaphore>,
5920}
5921
5922impl TokenManager {
5923    /// Gets the global singleton instance of `TokenManager`
5924    ///
5925    /// This ensures all `ApiClient` instances share the same token storage,
5926    /// preventing multiple token acquisition attempts in concurrent tests.
5927    ///
5928    /// # Errors
5929    /// Returns an error if the `TokenManager` cannot be initialized
5930    pub async fn get_global_instance() -> Result<Arc<Self>, Error> {
5931        let manager = GLOBAL_TOKEN_MANAGER
5932            .get_or_try_init(|| async {
5933                let config = RetryConfig::from_env()?;
5934                let base_url = get_amp_api_base_url()?;
5935                let manager = Self::with_config_and_base_url(config, base_url).await?;
5936                Ok::<Arc<Self>, Error>(Arc::new(manager))
5937            })
5938            .await?;
5939
5940        Ok(manager.clone())
5941    }
5942
5943    /// Creates a new `TokenManager` with default configuration
5944    ///
5945    /// # Errors
5946    /// Returns an error if the base URL cannot be obtained from environment variables
5947    pub async fn new() -> Result<Self, Error> {
5948        let config = RetryConfig::from_env()?;
5949        Self::with_config(config).await
5950    }
5951
5952    /// Creates a new `TokenManager` with the specified retry configuration
5953    ///
5954    /// # Errors
5955    /// Returns an error if the base URL cannot be obtained from environment variables
5956    pub async fn with_config(config: RetryConfig) -> Result<Self, Error> {
5957        let base_url = get_amp_api_base_url()?;
5958        Self::with_config_and_base_url(config, base_url).await
5959    }
5960
5961    /// Creates a new `TokenManager` with the specified configuration and base URL (for testing)
5962    ///
5963    /// # Errors
5964    /// This method is infallible but returns Result for API consistency
5965    pub async fn with_config_and_base_url(
5966        config: RetryConfig,
5967        base_url: Url,
5968    ) -> Result<Self, Error> {
5969        let manager = Self {
5970            token_data: Arc::new(Mutex::new(None)),
5971            retry_client: RetryClient::new(config),
5972            base_url,
5973            token_operation_semaphore: Arc::new(Semaphore::new(1)),
5974        };
5975
5976        // Load token from disk if persistence is enabled
5977        if Self::should_persist_tokens() {
5978            if let Ok(Some(token_data)) = manager.load_token_from_disk().await {
5979                *manager.token_data.lock().await = Some(token_data);
5980                tracing::info!("Token loaded from disk during initialization");
5981            }
5982        }
5983
5984        Ok(manager)
5985    }
5986
5987    /// Creates a new `TokenManager` with a pre-set mock token (for testing)
5988    ///
5989    /// # Errors
5990    /// This method is infallible but returns Result for API consistency
5991    pub fn with_mock_token(
5992        config: RetryConfig,
5993        base_url: Url,
5994        mock_token: String,
5995    ) -> Result<Self, Error> {
5996        let expires_at = Utc::now() + Duration::hours(24); // Mock token valid for 24 hours
5997        let token_data = TokenData::new(mock_token, expires_at);
5998
5999        let manager = Self {
6000            token_data: Arc::new(Mutex::new(Some(token_data))),
6001            retry_client: RetryClient::new(config),
6002            base_url,
6003            token_operation_semaphore: Arc::new(Semaphore::new(1)),
6004        };
6005
6006        Ok(manager)
6007    }
6008
6009    /// Gets a valid authentication token with proactive refresh logic
6010    ///
6011    /// This method implements thread-safe token management logic:
6012    /// 1. Check if a valid token exists and is not expiring soon (within 5 minutes)
6013    /// 2. If token needs refresh/obtain, acquire semaphore to prevent concurrent operations
6014    /// 3. Double-check token state after acquiring semaphore (another thread may have updated it)
6015    /// 4. Perform atomic token update operations
6016    /// 5. Return the valid token
6017    ///
6018    /// # Thread Safety
6019    /// This method is fully thread-safe and prevents race conditions by:
6020    /// - Using a semaphore to ensure only one token operation at a time
6021    /// - Double-checking token state after acquiring the semaphore
6022    /// - Performing atomic token updates within the critical section
6023    ///
6024    /// # Errors
6025    /// Returns a `TokenError` if token acquisition or refresh fails after all retries
6026    pub async fn get_token(&self) -> Result<String, Error> {
6027        // Fast path: check if we have a valid token without acquiring semaphore
6028        if let Some(token) = self.check_existing_token().await? {
6029            return Ok(token);
6030        }
6031
6032        // Slow path: token needs refresh/obtain, acquire semaphore for thread safety
6033        let _permit = self.acquire_token_semaphore().await?;
6034
6035        // Double-check token state after acquiring semaphore - another thread may have updated it
6036        if let Some(token) = self.check_existing_token().await? {
6037            tracing::debug!("Token was updated by another thread, using existing valid token");
6038            return Ok(token);
6039        }
6040
6041        // At this point, we need to refresh or obtain a new token
6042        self.handle_token_refresh_or_obtain().await
6043    }
6044
6045    /// Checks if we have a valid existing token that doesn't expire soon
6046    async fn check_existing_token(&self) -> Result<Option<String>, Error> {
6047        let token_guard = self.token_data.lock().await;
6048        if let Some(ref token_data) = *token_guard {
6049            if !token_data.expires_soon(Duration::minutes(5)) {
6050                tracing::debug!("Using existing valid token");
6051                let token = token_data.token.expose_secret().clone();
6052                drop(token_guard);
6053                return Ok(Some(token));
6054            }
6055        }
6056        drop(token_guard);
6057        Ok(None)
6058    }
6059
6060    /// Acquires the token operation semaphore for thread-safe operations
6061    async fn acquire_token_semaphore(&self) -> Result<tokio::sync::SemaphorePermit<'_>, Error> {
6062        let permit = self
6063            .token_operation_semaphore
6064            .acquire()
6065            .await
6066            .map_err(|e| {
6067                Error::Token(TokenError::storage(format!(
6068                    "Failed to acquire token operation semaphore: {e}"
6069                )))
6070            })?;
6071
6072        tracing::debug!("Acquired token operation semaphore for thread-safe token management");
6073        Ok(permit)
6074    }
6075
6076    /// Handles the token refresh or obtain logic
6077    async fn handle_token_refresh_or_obtain(&self) -> Result<String, Error> {
6078        let needs_refresh = self.determine_token_operation().await;
6079
6080        if needs_refresh {
6081            match self.refresh_token_internal().await {
6082                Ok(token) => {
6083                    tracing::info!("Token refreshed successfully");
6084                    return Ok(token);
6085                }
6086                Err(e) => {
6087                    tracing::warn!("Token refresh failed, falling back to obtain: {e}");
6088                    // Fall through to obtain new token
6089                }
6090            }
6091        }
6092
6093        // Either we needed to obtain from the start, or refresh failed
6094        self.obtain_token_internal().await
6095    }
6096
6097    /// Determines whether we need to refresh or obtain a new token
6098    async fn determine_token_operation(&self) -> bool {
6099        let token_guard = self.token_data.lock().await;
6100        token_guard.as_ref().map_or_else(
6101            || {
6102                tracing::info!("No token exists, will obtain new token");
6103                false
6104            },
6105            |token_data| {
6106                if token_data.is_expired() {
6107                    tracing::info!("Token is expired, will obtain new token");
6108                    false
6109                } else {
6110                    tracing::info!("Token expires soon, will attempt refresh");
6111                    true
6112                }
6113            },
6114        )
6115    }
6116
6117    /// Obtains a new authentication token using environment credentials with retry logic
6118    ///
6119    /// This method:
6120    /// 1. Reads credentials from environment variables
6121    /// 2. Makes a token request with retry logic
6122    /// 3. Stores the new token with 24-hour expiry
6123    /// 4. Returns the token string
6124    ///
6125    /// # Thread Safety
6126    /// This method acquires the token operation semaphore to ensure thread-safe operation.
6127    /// For internal use within already-synchronized contexts, use `obtain_token_internal()`.
6128    ///
6129    /// # Errors
6130    /// Returns an error if:
6131    /// - Environment variables are missing
6132    /// - All retry attempts fail
6133    /// - Response parsing fails
6134    pub async fn obtain_token(&self) -> Result<String, Error> {
6135        let _permit = self
6136            .token_operation_semaphore
6137            .acquire()
6138            .await
6139            .map_err(|e| {
6140                Error::Token(TokenError::storage(format!(
6141                    "Failed to acquire token operation semaphore: {e}"
6142                )))
6143            })?;
6144
6145        self.obtain_token_internal().await
6146    }
6147
6148    /// Internal method to obtain a new authentication token without acquiring semaphore
6149    ///
6150    /// This method should only be called from contexts where the token operation semaphore
6151    /// has already been acquired (e.g., from within `get_token()`).
6152    ///
6153    /// # Errors
6154    /// Returns an error if:
6155    /// - Environment variables are missing
6156    /// - All retry attempts fail
6157    /// - Response parsing fails
6158    async fn obtain_token_internal(&self) -> Result<String, Error> {
6159        tracing::debug!("Obtaining new authentication token");
6160
6161        let request_payload = Self::get_credentials_from_env()?;
6162        let url = self.build_obtain_token_url();
6163        let response = self.execute_token_request(&url, &request_payload).await?;
6164        let token_response = self.parse_token_response(response).await?;
6165
6166        self.store_token_data(&token_response.token).await;
6167
6168        tracing::info!("New authentication token obtained successfully");
6169        Ok(token_response.token)
6170    }
6171
6172    /// Gets credentials from environment variables
6173    fn get_credentials_from_env() -> Result<TokenRequest, Error> {
6174        let username = env::var("AMP_USERNAME")
6175            .map_err(|_| Error::MissingEnvVar("AMP_USERNAME".to_string()))?;
6176        let password = env::var("AMP_PASSWORD")
6177            .map_err(|_| Error::MissingEnvVar("AMP_PASSWORD".to_string()))?;
6178
6179        Ok(TokenRequest { username, password })
6180    }
6181
6182    /// Builds the URL for token obtain endpoint
6183    fn build_obtain_token_url(&self) -> Url {
6184        let mut url = self.base_url.clone();
6185        url.path_segments_mut()
6186            .unwrap()
6187            .push("user")
6188            .push("obtain_token");
6189        url
6190    }
6191
6192    /// Executes the token request with retry logic
6193    async fn execute_token_request(
6194        &self,
6195        url: &Url,
6196        request_payload: &TokenRequest,
6197    ) -> Result<reqwest::Response, Error> {
6198        let response = self
6199            .retry_client
6200            .execute_with_retry(|| {
6201                self.retry_client
6202                    .client()
6203                    .post(url.clone())
6204                    .json(request_payload)
6205            })
6206            .await
6207            .map_err(Error::Token)?;
6208
6209        if !response.status().is_success() {
6210            let status = response.status();
6211            let error_text = response
6212                .text()
6213                .await
6214                .unwrap_or_else(|_| "Unknown error".to_string());
6215            return Err(Error::TokenRequestFailed { status, error_text });
6216        }
6217
6218        Ok(response)
6219    }
6220
6221    /// Parses the token response from the API
6222    async fn parse_token_response(
6223        &self,
6224        response: reqwest::Response,
6225    ) -> Result<TokenResponse, Error> {
6226        response
6227            .json()
6228            .await
6229            .map_err(|e| Error::ResponseParsingFailed(e.to_string()))
6230    }
6231
6232    /// Stores the token data with 24-hour expiry and optional disk persistence
6233    async fn store_token_data(&self, token: &str) {
6234        let expires_at = Utc::now() + Duration::days(1);
6235        let token_data = TokenData::new(token.to_string(), expires_at);
6236
6237        // Atomic token update - hold the lock for the minimal time needed
6238        *self.token_data.lock().await = Some(token_data.clone());
6239        tracing::debug!("Token data updated atomically in storage");
6240
6241        // Save to disk if persistence is enabled
6242        if Self::should_persist_tokens() {
6243            if let Err(e) = self.save_token_to_disk(&token_data).await {
6244                tracing::warn!("Failed to save token to disk: {e}");
6245            }
6246        }
6247    }
6248
6249    /// Refreshes the current authentication token with fallback to obtain on failure
6250    ///
6251    /// This method:
6252    /// 1. Uses the existing token to request a refresh
6253    /// 2. Updates the stored token data on success
6254    /// 3. Falls back to obtaining a new token if refresh fails
6255    ///
6256    /// # Thread Safety
6257    /// This method acquires the token operation semaphore to ensure thread-safe operation.
6258    /// For internal use within already-synchronized contexts, use `refresh_token_internal()`.
6259    ///
6260    /// # Errors
6261    /// Returns an error if both refresh and obtain operations fail
6262    pub async fn refresh_token(&self) -> Result<String, Error> {
6263        let _permit = self
6264            .token_operation_semaphore
6265            .acquire()
6266            .await
6267            .map_err(|e| {
6268                Error::Token(TokenError::storage(format!(
6269                    "Failed to acquire token operation semaphore: {e}"
6270                )))
6271            })?;
6272
6273        self.refresh_token_internal().await
6274    }
6275
6276    /// Internal method to refresh the current authentication token without acquiring semaphore
6277    ///
6278    /// This method should only be called from contexts where the token operation semaphore
6279    /// has already been acquired (e.g., from within `get_token()`).
6280    ///
6281    /// # Errors
6282    /// Returns an error if both refresh and obtain operations fail
6283    #[allow(clippy::cognitive_complexity)]
6284    async fn refresh_token_internal(&self) -> Result<String, Error> {
6285        tracing::debug!("Refreshing authentication token");
6286
6287        let Some(current_token) = self.get_current_token_for_refresh().await else {
6288            tracing::warn!("No token available for refresh, obtaining new token");
6289            return self.obtain_token_internal().await;
6290        };
6291
6292        let url = self.build_refresh_token_url();
6293        let response = self.execute_refresh_request(&url, &current_token).await;
6294
6295        match response {
6296            Ok(resp) => self.handle_refresh_response(resp).await,
6297            Err(e) => {
6298                tracing::warn!("Token refresh request failed: {e}, falling back to obtain");
6299                self.obtain_token_internal().await
6300            }
6301        }
6302    }
6303
6304    /// Gets the current token for refresh operations
6305    async fn get_current_token_for_refresh(&self) -> Option<String> {
6306        let token_guard = self.token_data.lock().await;
6307        token_guard
6308            .as_ref()
6309            .map(|token_data| token_data.token.expose_secret().clone())
6310    }
6311
6312    /// Builds the URL for token refresh endpoint
6313    fn build_refresh_token_url(&self) -> Url {
6314        let mut url = self.base_url.clone();
6315        url.path_segments_mut()
6316            .unwrap()
6317            .push("user")
6318            .push("refresh_token");
6319        url
6320    }
6321
6322    /// Executes the refresh request with retry logic
6323    async fn execute_refresh_request(
6324        &self,
6325        url: &Url,
6326        current_token: &str,
6327    ) -> Result<reqwest::Response, TokenError> {
6328        self.retry_client
6329            .execute_with_retry(|| {
6330                self.retry_client
6331                    .client()
6332                    .post(url.clone())
6333                    .header(AUTHORIZATION, format!("token {current_token}"))
6334            })
6335            .await
6336    }
6337
6338    /// Handles the refresh response, either storing the new token or falling back to obtain
6339    async fn handle_refresh_response(&self, resp: reqwest::Response) -> Result<String, Error> {
6340        if !resp.status().is_success() {
6341            let status = resp.status();
6342            let error_text = resp
6343                .text()
6344                .await
6345                .unwrap_or_else(|_| "Unknown error".to_string());
6346
6347            tracing::warn!("Token refresh failed with status {status}: {error_text}");
6348            return self.obtain_token_internal().await;
6349        }
6350
6351        let token_response: TokenResponse = resp
6352            .json()
6353            .await
6354            .map_err(|e| Error::ResponseParsingFailed(e.to_string()))?;
6355
6356        self.store_token_data(&token_response.token).await;
6357        tracing::info!("Authentication token refreshed successfully");
6358        Ok(token_response.token)
6359    }
6360
6361    /// Gets current token information for debugging and monitoring
6362    ///
6363    /// Returns detailed information about the current token including:
6364    /// - Expiry time and remaining duration
6365    /// - Token age since acquisition
6366    /// - Expiry status flags
6367    ///
6368    /// # Returns
6369    /// `Some(TokenInfo)` if a token exists, `None` if no token is stored
6370    ///
6371    /// # Errors
6372    /// Returns an error if token information retrieval fails
6373    pub async fn get_token_info(&self) -> Result<Option<TokenInfo>, Error> {
6374        tracing::debug!("Retrieving token information for debugging");
6375
6376        let token_info = self.token_data.lock().await.as_ref().map(TokenInfo::from);
6377
6378        match &token_info {
6379            Some(info) => {
6380                tracing::debug!(
6381                    "Token info retrieved - expires_at: {}, age: {:?}, expires_in: {:?}, is_expired: {}, expires_soon: {}",
6382                    info.expires_at,
6383                    info.age,
6384                    info.expires_in,
6385                    info.is_expired,
6386                    info.expires_soon
6387                );
6388            }
6389            None => {
6390                tracing::debug!("No token information available - no token stored");
6391            }
6392        }
6393
6394        Ok(token_info)
6395    }
6396
6397    /// Clears the stored token (useful for testing scenarios)
6398    ///
6399    /// This method removes the current token from storage, forcing the next
6400    /// `get_token()` call to obtain a fresh token.
6401    ///
6402    /// # Errors
6403    /// Returns an error if token clearing fails
6404    pub async fn clear_token(&self) -> Result<(), Error> {
6405        tracing::debug!("Clearing stored token from memory and disk");
6406
6407        let had_token = self.clear_token_from_memory().await;
6408        self.clear_token_from_disk_if_enabled().await;
6409        Self::log_token_clear_result(had_token);
6410
6411        Ok(())
6412    }
6413
6414    /// Clears the token from memory and returns whether a token was present
6415    async fn clear_token_from_memory(&self) -> bool {
6416        let mut token_guard = self.token_data.lock().await;
6417        let had_token = token_guard.is_some();
6418        *token_guard = None;
6419        drop(token_guard);
6420        had_token
6421    }
6422
6423    /// Clears the token from disk if persistence is enabled
6424    async fn clear_token_from_disk_if_enabled(&self) {
6425        if Self::should_persist_tokens() {
6426            if let Err(e) = self.remove_token_from_disk().await {
6427                tracing::warn!("Failed to remove token from disk: {e}");
6428            }
6429        }
6430    }
6431
6432    /// Logs the result of the token clearing operation
6433    fn log_token_clear_result(had_token: bool) {
6434        if had_token {
6435            tracing::info!("Token successfully cleared from memory and disk - next get_token() will obtain fresh token");
6436        } else {
6437            tracing::debug!("No token was stored to clear");
6438        }
6439    }
6440
6441    /// Forces a token refresh regardless of current token status
6442    ///
6443    /// This method bypasses the normal proactive refresh logic and immediately
6444    /// attempts to refresh the current token. If no token exists or refresh fails,
6445    /// it falls back to obtaining a new token.
6446    ///
6447    /// # Thread Safety
6448    /// This method is fully thread-safe and uses the same semaphore-based synchronization
6449    /// as other token operations to prevent race conditions.
6450    ///
6451    /// # Errors
6452    /// Returns an error if both refresh and obtain operations fail
6453    pub async fn force_refresh(&self) -> Result<String, Error> {
6454        tracing::info!("Forcing token refresh - bypassing normal proactive refresh logic");
6455
6456        let _permit = self.acquire_token_semaphore().await?;
6457        self.log_token_status_for_refresh().await;
6458        self.execute_forced_refresh().await
6459    }
6460
6461    /// Logs the current token status for forced refresh operation
6462    async fn log_token_status_for_refresh(&self) {
6463        let has_token = {
6464            let token_guard = self.token_data.lock().await;
6465            token_guard.is_some()
6466        };
6467
6468        if has_token {
6469            tracing::debug!("Existing token found, attempting forced refresh");
6470        } else {
6471            tracing::debug!("No existing token found, will obtain new token");
6472        }
6473    }
6474
6475    /// Executes the forced refresh operation
6476    async fn execute_forced_refresh(&self) -> Result<String, Error> {
6477        match self.refresh_token_internal().await {
6478            Ok(token) => {
6479                tracing::info!("Forced token refresh completed successfully");
6480                Ok(token)
6481            }
6482            Err(e) => {
6483                tracing::error!("Forced token refresh failed: {e}");
6484                Err(e)
6485            }
6486        }
6487    }
6488
6489    /// Determines if token persistence is enabled based on environment variables
6490    ///
6491    /// Token persistence is enabled when:
6492    /// - `AMP_TESTS=live` (for live API testing)
6493    /// - `AMP_TOKEN_PERSISTENCE=true` is set
6494    /// - NOT in mock test environments (to prevent test pollution)
6495    fn should_persist_tokens() -> bool {
6496        // Use the new environment detection logic
6497        let environment = TokenEnvironment::detect();
6498
6499        // Never persist tokens in mock environments to prevent test pollution
6500        if environment.is_mock() {
6501            tracing::debug!("Token persistence disabled - mock environment detected");
6502            return false;
6503        }
6504
6505        // Check if explicitly enabled
6506        if env::var("AMP_TOKEN_PERSISTENCE").unwrap_or_default() == "true" {
6507            tracing::debug!("Token persistence enabled - AMP_TOKEN_PERSISTENCE=true");
6508            return true;
6509        }
6510
6511        // Use environment-based persistence setting
6512        let should_persist = environment.should_persist_tokens();
6513        tracing::debug!(
6514            "Token persistence setting from environment: {}",
6515            should_persist
6516        );
6517        should_persist
6518    }
6519
6520    /// Loads token data from disk if it exists and is valid
6521    async fn load_token_from_disk(&self) -> Result<Option<TokenData>, Error> {
6522        let token_file = "token.json";
6523
6524        if !self.token_file_exists(token_file).await {
6525            return Ok(None);
6526        }
6527
6528        let content = self.read_token_file(token_file).await?;
6529        self.parse_and_validate_token(token_file, &content).await
6530    }
6531
6532    /// Checks if the token file exists on disk
6533    async fn token_file_exists(&self, token_file: &str) -> bool {
6534        tokio::fs::try_exists(token_file).await.map_or_else(
6535            |_| {
6536                tracing::debug!("Error checking token file existence: {}", token_file);
6537                false
6538            },
6539            |exists| {
6540                if !exists {
6541                    tracing::debug!("Token file does not exist: {}", token_file);
6542                }
6543                exists
6544            },
6545        )
6546    }
6547
6548    /// Reads the token file content from disk
6549    async fn read_token_file(&self, token_file: &str) -> Result<String, Error> {
6550        use tokio::fs;
6551
6552        match fs::read_to_string(token_file).await {
6553            Ok(content) => Ok(content),
6554            Err(e) => {
6555                tracing::warn!("Failed to read token file: {e}");
6556                Err(Error::Token(TokenError::storage(format!(
6557                    "Failed to read token file: {e}"
6558                ))))
6559            }
6560        }
6561    }
6562
6563    /// Parses token content and validates expiration
6564    async fn parse_and_validate_token(
6565        &self,
6566        token_file: &str,
6567        content: &str,
6568    ) -> Result<Option<TokenData>, Error> {
6569        match serde_json::from_str::<TokenData>(content) {
6570            Ok(token_data) => self.handle_parsed_token(token_file, token_data).await,
6571            Err(e) => self.handle_parse_error(token_file, e).await,
6572        }
6573    }
6574
6575    /// Handles successfully parsed token data, checking expiration
6576    async fn handle_parsed_token(
6577        &self,
6578        token_file: &str,
6579        token_data: TokenData,
6580    ) -> Result<Option<TokenData>, Error> {
6581        if token_data.is_expired() {
6582            tracing::info!("Token loaded from disk is expired, removing file");
6583            let _ = tokio::fs::remove_file(token_file).await;
6584            Ok(None)
6585        } else {
6586            tracing::info!("Valid token loaded from disk");
6587            Ok(Some(token_data))
6588        }
6589    }
6590
6591    /// Handles token parsing errors by cleaning up the invalid file
6592    async fn handle_parse_error(
6593        &self,
6594        token_file: &str,
6595        e: serde_json::Error,
6596    ) -> Result<Option<TokenData>, Error> {
6597        tracing::warn!("Failed to parse token file, removing: {e}");
6598        let _ = tokio::fs::remove_file(token_file).await;
6599        Err(Error::Token(TokenError::serialization(format!(
6600            "Failed to parse token file: {e}"
6601        ))))
6602    }
6603
6604    /// Saves token data to disk
6605    async fn save_token_to_disk(&self, token_data: &TokenData) -> Result<(), Error> {
6606        use tokio::fs;
6607
6608        let token_file = "token.json";
6609
6610        match serde_json::to_string_pretty(token_data) {
6611            Ok(json) => match fs::write(token_file, json).await {
6612                Ok(()) => {
6613                    tracing::debug!("Token saved to disk: {}", token_file);
6614                    Ok(())
6615                }
6616                Err(e) => {
6617                    tracing::error!("Failed to write token file: {e}");
6618                    Err(Error::Token(TokenError::storage(format!(
6619                        "Failed to write token file: {e}"
6620                    ))))
6621                }
6622            },
6623            Err(e) => {
6624                tracing::error!("Failed to serialize token data: {e}");
6625                Err(Error::Token(TokenError::serialization(format!(
6626                    "Failed to serialize token data: {e}"
6627                ))))
6628            }
6629        }
6630    }
6631
6632    /// Removes the token file from disk
6633    async fn remove_token_from_disk(&self) -> Result<(), Error> {
6634        use tokio::fs;
6635
6636        let token_file = "token.json";
6637
6638        match fs::remove_file(token_file).await {
6639            Ok(()) => {
6640                tracing::debug!("Token file removed from disk: {}", token_file);
6641                Ok(())
6642            }
6643            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
6644                tracing::debug!("Token file does not exist, nothing to remove");
6645                Ok(())
6646            }
6647            Err(e) => {
6648                tracing::warn!("Failed to remove token file: {e}");
6649                Err(Error::Token(TokenError::storage(format!(
6650                    "Failed to remove token file: {e}"
6651                ))))
6652            }
6653        }
6654    }
6655
6656    /// Forces cleanup of token persistence files (useful for testing)
6657    /// This method removes token files regardless of persistence settings
6658    ///
6659    /// # Errors
6660    /// Returns an error if:
6661    /// - File system permissions prevent deletion of the token file
6662    /// - I/O errors occur during file deletion operations
6663    /// - The token file is locked by another process
6664    pub async fn force_cleanup_token_files() -> Result<(), Error> {
6665        use tokio::fs;
6666
6667        let token_file = "token.json";
6668
6669        match fs::remove_file(token_file).await {
6670            Ok(()) => {
6671                tracing::debug!("Token file forcefully removed: {}", token_file);
6672                Ok(())
6673            }
6674            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
6675                tracing::debug!("No token file to clean up");
6676                Ok(())
6677            }
6678            Err(e) => {
6679                tracing::warn!("Failed to force cleanup token file: {e}");
6680                Err(Error::Token(TokenError::storage(format!(
6681                    "Failed to force cleanup token file: {e}"
6682                ))))
6683            }
6684        }
6685    }
6686
6687    /// Resets the global `TokenManager` singleton (useful for testing)
6688    ///
6689    /// This method clears the global singleton instance, forcing the next
6690    /// call to `get_global_instance()` to create a fresh `TokenManager`.
6691    /// Primarily intended for test scenarios where a clean state is needed.
6692    ///
6693    /// # Errors
6694    /// Returns an error if:
6695    /// - Token clearing operations fail during the reset process
6696    /// - File system errors occur when clearing persistent token data
6697    /// - The global instance is in an invalid state that prevents cleanup
6698    pub async fn reset_global_instance() -> Result<(), Error> {
6699        // Clear any existing token from the current global instance
6700        if let Some(manager) = GLOBAL_TOKEN_MANAGER.get() {
6701            let _ = manager.clear_token().await;
6702        }
6703
6704        // Reset the OnceCell to allow a new instance to be created
6705        // Note: OnceCell doesn't have a reset method, so we can't actually reset it
6706        // The best we can do is clear the token from the existing instance
6707        tracing::debug!("Global TokenManager instance token cleared for testing");
6708        Ok(())
6709    }
6710}
6711
6712#[derive(Debug)]
6713pub struct ApiClient {
6714    client: Client,
6715    base_url: Url,
6716    token_strategy: Box<dyn TokenStrategy>,
6717}
6718
6719#[allow(clippy::missing_errors_doc, clippy::missing_panics_doc)]
6720impl ApiClient {
6721    /// Creates a new API client with the base URL from environment variables.
6722    ///
6723    /// Automatically selects the appropriate token strategy based on environment detection:
6724    /// - Mock strategy for mock environments (no persistence, isolated tokens)
6725    /// - Live strategy for live environments (full token management with persistence)
6726    ///
6727    /// # Errors
6728    ///
6729    /// Returns an error if:
6730    /// - The `AMP_API_BASE_URL` environment variable contains an invalid URL
6731    /// - Token strategy initialization fails
6732    ///
6733    /// # Examples
6734    /// ```no_run
6735    /// # use amp_rs::ApiClient;
6736    /// # #[tokio::main]
6737    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
6738    /// // Create a new client - automatically detects environment
6739    /// let client = ApiClient::new().await?;
6740    ///
6741    /// // Client is ready to use
6742    /// let assets = client.get_assets().await?;
6743    /// println!("Found {} assets", assets.len());
6744    /// # Ok(())
6745    /// # }
6746    /// ```
6747    pub async fn new() -> Result<Self, Error> {
6748        let base_url = get_amp_api_base_url()?;
6749        let client = Client::new();
6750
6751        // Automatic strategy selection based on environment
6752        let token_strategy = TokenEnvironment::create_auto_strategy(None).await?;
6753
6754        tracing::info!(
6755            "Created ApiClient with {} strategy for base URL: {}",
6756            token_strategy.strategy_type(),
6757            base_url
6758        );
6759
6760        Ok(Self {
6761            client,
6762            base_url,
6763            token_strategy,
6764        })
6765    }
6766
6767    /// Creates a new API client with the specified base URL.
6768    ///
6769    /// Automatically selects the appropriate token strategy based on environment detection.
6770    ///
6771    /// # Errors
6772    ///
6773    /// Returns an error if token strategy initialization fails.
6774    ///
6775    /// # Examples
6776    /// ```no_run
6777    /// # use amp_rs::ApiClient;
6778    /// # use reqwest::Url;
6779    /// # #[tokio::main]
6780    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
6781    /// let base_url = Url::parse("https://amp-test.blockstream.com/api")?;
6782    /// let client = ApiClient::with_base_url(base_url).await?;
6783    ///
6784    /// // Client is ready to use with the specified URL
6785    /// let assets = client.get_assets().await?;
6786    /// # Ok(())
6787    /// # }
6788    /// ```
6789    pub async fn with_base_url(base_url: Url) -> Result<Self, Error> {
6790        let client = Client::new();
6791
6792        // Automatic strategy selection based on environment
6793        let token_strategy = TokenEnvironment::create_auto_strategy(None).await?;
6794
6795        tracing::info!(
6796            "Created ApiClient with {} strategy for base URL: {}",
6797            token_strategy.strategy_type(),
6798            base_url
6799        );
6800
6801        Ok(Self {
6802            client,
6803            base_url,
6804            token_strategy,
6805        })
6806    }
6807
6808    /// Creates a new API client with a custom token strategy (useful for testing).
6809    ///
6810    /// # Errors
6811    ///
6812    /// Returns an error if the base URL cannot be obtained from environment variables.
6813    pub fn with_token_strategy(token_strategy: Box<dyn TokenStrategy>) -> Result<Self, Error> {
6814        let base_url = get_amp_api_base_url()?;
6815
6816        tracing::info!(
6817            "Created ApiClient with explicit {} strategy for base URL: {}",
6818            token_strategy.strategy_type(),
6819            base_url
6820        );
6821
6822        Ok(Self {
6823            client: Client::new(),
6824            base_url,
6825            token_strategy,
6826        })
6827    }
6828
6829    /// Creates a new API client with a custom token manager (useful for testing).
6830    ///
6831    /// # Errors
6832    ///
6833    /// Returns an error if the base URL cannot be obtained from environment variables.
6834    pub fn with_token_manager(token_manager: Arc<TokenManager>) -> Result<Self, Error> {
6835        let base_url = get_amp_api_base_url()?;
6836        let token_strategy: Box<dyn TokenStrategy> =
6837            Box::new(LiveTokenStrategy::with_token_manager(token_manager));
6838
6839        tracing::info!(
6840            "Created ApiClient with custom token manager for base URL: {}",
6841            base_url
6842        );
6843
6844        Ok(Self {
6845            client: Client::new(),
6846            base_url,
6847            token_strategy,
6848        })
6849    }
6850
6851    /// Creates a new API client for testing with a mock token strategy that always returns a fixed token.
6852    /// This bypasses all token acquisition and management logic and uses complete isolation.
6853    ///
6854    /// # Errors
6855    ///
6856    /// This method is infallible but returns Result for API consistency.
6857    ///
6858    /// # Examples
6859    /// ```
6860    /// # use amp_rs::ApiClient;
6861    /// # use reqwest::Url;
6862    /// # #[tokio::main]
6863    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
6864    /// let base_url = Url::parse("http://localhost:8080/api")?;
6865    /// let client = ApiClient::with_mock_token(base_url, "test_token".to_string())?;
6866    ///
6867    /// // Client will always use "test_token" for authentication
6868    /// let token = client.get_token().await?;
6869    /// assert_eq!(token, "test_token");
6870    /// # Ok(())
6871    /// # }
6872    /// ```
6873    pub fn with_mock_token(base_url: Url, mock_token: String) -> Result<Self, Error> {
6874        let client = Client::new();
6875        let token_strategy: Box<dyn TokenStrategy> = Box::new(MockTokenStrategy::new(mock_token));
6876
6877        tracing::info!(
6878            "Created ApiClient with explicit mock token strategy for base URL: {}",
6879            base_url
6880        );
6881
6882        Ok(Self {
6883            client,
6884            base_url,
6885            token_strategy,
6886        })
6887    }
6888
6889    /// Obtains a new authentication token from the AMP API.
6890    ///
6891    /// **Note**: This method is deprecated in favor of the automatic token management
6892    /// provided by `get_token()`. The `TokenManager` handles token acquisition internally
6893    /// with enhanced retry logic and error handling.
6894    ///
6895    /// # Errors
6896    ///
6897    /// Returns an error if:
6898    /// - The `AMP_USERNAME` or `AMP_PASSWORD` environment variables are not set
6899    /// - The HTTP request fails
6900    /// - The token request is rejected by the server
6901    /// - The response cannot be parsed
6902    #[deprecated(note = "Use get_token() instead - it provides automatic token management")]
6903    pub async fn obtain_amp_token(&self) -> Result<String, Error> {
6904        // Delegate to get_token for backward compatibility
6905        self.get_token().await
6906    }
6907
6908    /// Gets current token information for debugging and monitoring.
6909    ///
6910    /// Returns detailed information about the current token including:
6911    /// - Expiry time and remaining duration
6912    /// - Token age since acquisition
6913    /// - Expiry status flags
6914    ///
6915    /// Note: Mock strategies may return limited or no token information.
6916    ///
6917    /// # Returns
6918    /// `Some(TokenInfo)` if a token exists, `None` if no token is stored or strategy doesn't support info
6919    ///
6920    /// # Errors
6921    /// Returns an error if token information retrieval fails
6922    ///
6923    /// # Examples
6924    /// ```no_run
6925    /// # use amp_rs::ApiClient;
6926    /// # #[tokio::main]
6927    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
6928    /// let client = ApiClient::new().await?;
6929    ///
6930    /// if let Some(token_info) = client.get_token_info().await? {
6931    ///     println!("Token expires at: {}", token_info.expires_at);
6932    ///     println!("Token is expired: {}", token_info.is_expired);
6933    /// } else {
6934    ///     println!("No token stored or mock strategy in use");
6935    /// }
6936    /// # Ok(())
6937    /// # }
6938    /// ```
6939    pub async fn get_token_info(&self) -> Result<Option<TokenInfo>, Error> {
6940        // Only live strategies support detailed token information
6941        if let Some(live_strategy) = self
6942            .token_strategy
6943            .as_any()
6944            .downcast_ref::<LiveTokenStrategy>()
6945        {
6946            live_strategy.get_token_info().await
6947        } else {
6948            // Mock strategies don't provide detailed token information
6949            tracing::debug!(
6950                "Token info not available for {} strategy",
6951                self.token_strategy.strategy_type()
6952            );
6953            Ok(None)
6954        }
6955    }
6956
6957    /// Clears the stored token (useful for testing scenarios).
6958    ///
6959    /// This method removes the current token from storage, forcing the next
6960    /// `get_token()` call to obtain a fresh token.
6961    ///
6962    /// # Errors
6963    /// Returns an error if token clearing fails
6964    ///
6965    /// # Examples
6966    /// ```no_run
6967    /// # use amp_rs::ApiClient;
6968    /// # #[tokio::main]
6969    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
6970    /// let client = ApiClient::new().await?;
6971    ///
6972    /// // Clear any existing token
6973    /// client.clear_token().await?;
6974    ///
6975    /// // Next get_token() call will obtain a fresh token
6976    /// let token = client.get_token().await?;
6977    /// # Ok(())
6978    /// # }
6979    /// ```
6980    pub async fn clear_token(&self) -> Result<(), Error> {
6981        self.token_strategy.clear_token().await
6982    }
6983
6984    /// Forces a token refresh regardless of current token status.
6985    ///
6986    /// This method bypasses the normal proactive refresh logic and immediately
6987    /// attempts to refresh the current token. If no token exists or refresh fails,
6988    /// it falls back to obtaining a new token.
6989    ///
6990    /// # Errors
6991    /// Returns an error if both refresh and obtain operations fail
6992    pub async fn force_refresh(&self) -> Result<String, Error> {
6993        // Clear current token and get a fresh one
6994        self.token_strategy.clear_token().await?;
6995        self.token_strategy.get_token().await
6996    }
6997
6998    /// Resets the global `TokenManager` singleton (useful for testing).
6999    ///
7000    /// This method clears the token from the global `TokenManager` instance.
7001    /// Primarily intended for test scenarios where a clean token state is needed.
7002    ///
7003    /// # Errors
7004    /// Returns an error if the reset operation fails
7005    pub async fn reset_global_token_manager() -> Result<(), Error> {
7006        TokenManager::reset_global_instance().await
7007    }
7008
7009    /// Gets a valid authentication token with automatic token management.
7010    ///
7011    /// This method uses the integrated `TokenManager` to handle:
7012    /// - Proactive token refresh (5 minutes before expiry)
7013    /// - Automatic fallback from refresh to obtain on failure
7014    /// - Retry logic with exponential backoff
7015    /// - Thread-safe token storage
7016    ///
7017    /// # Errors
7018    ///
7019    /// Returns an error if token acquisition or refresh fails after all retries.
7020    ///
7021    /// # Examples
7022    /// ```no_run
7023    /// # use amp_rs::ApiClient;
7024    /// # #[tokio::main]
7025    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
7026    /// let client = ApiClient::new().await?;
7027    ///
7028    /// // Get a valid token - automatically handles refresh if needed
7029    /// let token = client.get_token().await?;
7030    /// println!("Got token: {}", &token[..10]); // Print first 10 chars
7031    /// # Ok(())
7032    /// # }
7033    /// ```
7034    pub async fn get_token(&self) -> Result<String, Error> {
7035        self.token_strategy.get_token().await
7036    }
7037
7038    /// Returns the type of token strategy currently in use
7039    ///
7040    /// This is useful for debugging and testing to verify the correct strategy is selected.
7041    ///
7042    /// # Returns
7043    /// A string indicating the strategy type: "mock" or "live"
7044    #[must_use]
7045    pub fn get_strategy_type(&self) -> &'static str {
7046        self.token_strategy.strategy_type()
7047    }
7048
7049    /// Returns whether the current strategy persists tokens
7050    ///
7051    /// This is useful for understanding the token management behavior.
7052    ///
7053    /// # Returns
7054    /// `true` if tokens are persisted to disk, `false` for in-memory only
7055    #[must_use]
7056    pub fn should_persist_tokens(&self) -> bool {
7057        self.token_strategy.should_persist()
7058    }
7059
7060    /// Force cleanup of token files (for test cleanup)
7061    ///
7062    /// This is a static method that can be used to cleanup token files
7063    /// without needing an `ApiClient` instance. Useful for test teardown.
7064    ///
7065    /// # Errors
7066    /// Returns an error if token file cleanup fails
7067    pub async fn force_cleanup_token_files() -> Result<(), Error> {
7068        // Only cleanup if we're not in a live test environment
7069        let environment = TokenEnvironment::detect();
7070        if !environment.is_live() || environment.is_mock() {
7071            TokenManager::force_cleanup_token_files().await?;
7072            tracing::debug!("Token files cleaned up for non-live environment");
7073        } else {
7074            tracing::debug!("Skipping token file cleanup in live environment");
7075        }
7076        Ok(())
7077    }
7078
7079    async fn request_raw(
7080        &self,
7081        method: Method,
7082        path: &[&str],
7083        body: Option<impl serde::Serialize>,
7084    ) -> Result<reqwest::Response, Error> {
7085        let debug_logging = std::env::var("AMP_DEBUG").is_ok();
7086
7087        if debug_logging {
7088            eprintln!("🌐 HTTP Request: {} /{}", method, path.join("/"));
7089        }
7090
7091        let token = self.get_token().await?;
7092        let mut url = self.base_url.clone();
7093        url.path_segments_mut().unwrap().extend(path);
7094
7095        if debug_logging {
7096            eprintln!("🔗 Full URL: {url}");
7097        }
7098
7099        // Retry logic for network issues
7100        let max_retries = 3;
7101        let mut last_error = None;
7102
7103        for attempt in 1..=max_retries {
7104            if debug_logging && attempt > 1 {
7105                eprintln!("🔄 Retry attempt {attempt} of {max_retries}");
7106            }
7107
7108            let mut request_builder = self
7109                .client
7110                .request(method.clone(), url.clone())
7111                .header(AUTHORIZATION, format!("token {token}"))
7112                .timeout(std::time::Duration::from_secs(60)); // Increase timeout to 60 seconds
7113
7114            if let Some(ref body) = body {
7115                if debug_logging && attempt == 1 {
7116                    if let Ok(json_body) = serde_json::to_string_pretty(&body) {
7117                        eprintln!(
7118                            "📤 Request body ({} bytes):\n{}",
7119                            json_body.len(),
7120                            json_body
7121                        );
7122                    } else {
7123                        eprintln!("📤 Request body: [serialization failed]");
7124                    }
7125                }
7126                request_builder = request_builder.json(&body);
7127            } else if debug_logging && attempt == 1 {
7128                eprintln!("📤 Request body: [empty]");
7129            }
7130
7131            if debug_logging {
7132                eprintln!("🚀 Sending HTTP request (attempt {attempt})...");
7133            }
7134
7135            match request_builder.send().await {
7136                Ok(response) => {
7137                    let status = response.status();
7138
7139                    if debug_logging {
7140                        eprintln!("📥 Response status: {status}");
7141                    }
7142
7143                    if !status.is_success() {
7144                        let error_text = response
7145                            .text()
7146                            .await
7147                            .unwrap_or_else(|_| "Unknown error".to_string());
7148
7149                        if debug_logging {
7150                            eprintln!("❌ Error response body: {error_text}");
7151                        }
7152
7153                        return Err(Error::RequestFailed(format!(
7154                            "Request to {path:?} failed with status {status}: {error_text}"
7155                        )));
7156                    }
7157
7158                    if debug_logging {
7159                        eprintln!("✅ HTTP request successful");
7160                    }
7161
7162                    return Ok(response);
7163                }
7164                Err(e) => {
7165                    if debug_logging {
7166                        eprintln!("❌ HTTP request failed (attempt {attempt}): {e:?}");
7167                        eprintln!("   Error kind: {:?}", e.is_timeout());
7168                        eprintln!("   Is connect error: {}", e.is_connect());
7169                        eprintln!("   Is request error: {}", e.is_request());
7170                    }
7171
7172                    last_error = Some(e);
7173
7174                    // Only retry on network/connection errors, not on client errors
7175                    if attempt < max_retries {
7176                        #[allow(clippy::cast_sign_loss)] // attempt is always positive (1-3)
7177                        let delay = std::time::Duration::from_millis((attempt as u64) * 1000);
7178                        if debug_logging {
7179                            eprintln!("⏳ Waiting {}ms before retry...", delay.as_millis());
7180                        }
7181                        tokio::time::sleep(delay).await;
7182                    }
7183                }
7184            }
7185        }
7186
7187        // If we get here, all retries failed
7188        if debug_logging {
7189            eprintln!("❌ All {max_retries} retry attempts failed");
7190        }
7191
7192        Err(Error::Reqwest(last_error.unwrap()))
7193    }
7194
7195    async fn request_json<T: DeserializeOwned>(
7196        &self,
7197        method: Method,
7198        path: &[&str],
7199        body: Option<impl serde::Serialize>,
7200    ) -> Result<T, Error> {
7201        let response = self.request_raw(method, path, body).await?;
7202        response
7203            .json()
7204            .await
7205            .map_err(|e| Error::ResponseParsingFailed(e.to_string()))
7206    }
7207
7208    async fn request_empty(
7209        &self,
7210        method: Method,
7211        path: &[&str],
7212        body: Option<impl serde::Serialize>,
7213    ) -> Result<(), Error> {
7214        self.request_raw(method, path, body).await?;
7215        Ok(())
7216    }
7217
7218    /// Gets the API changelog.
7219    ///
7220    /// # Errors
7221    ///
7222    /// Returns an error if:
7223    /// - Authentication fails
7224    /// - The HTTP request fails
7225    /// - The server returns an error status
7226    /// - The response cannot be parsed as JSON
7227    pub async fn get_changelog(&self) -> Result<serde_json::Value, Error> {
7228        self.request_json(Method::GET, &["changelog"], None::<&()>)
7229            .await
7230    }
7231
7232    /// Changes the user's password.
7233    ///
7234    /// # Errors
7235    ///
7236    /// Returns an error if:
7237    /// - Authentication fails
7238    /// - The HTTP request fails
7239    /// - The server rejects the password change
7240    /// - The response cannot be parsed
7241    pub async fn user_change_password(
7242        &self,
7243        password: Secret<String>,
7244    ) -> Result<ChangePasswordResponse, Error> {
7245        let request = ChangePasswordRequest {
7246            password: Secret::new(Password(password.expose_secret().clone())),
7247        };
7248        self.request_json(Method::POST, &["user", "change_password"], Some(request))
7249            .await
7250    }
7251
7252    /// Gets a list of all assets.
7253    ///
7254    /// # Errors
7255    ///
7256    /// Returns an error if:
7257    /// - Authentication fails
7258    /// - The HTTP request fails
7259    /// - The server returns an error status
7260    /// - The response cannot be parsed
7261    ///
7262    /// # Examples
7263    /// ```no_run
7264    /// # use amp_rs::ApiClient;
7265    /// # #[tokio::main]
7266    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
7267    /// let client = ApiClient::new().await?;
7268    ///
7269    /// let assets = client.get_assets().await?;
7270    /// for asset in assets {
7271    ///     println!("Asset: {} ({})", asset.name, asset.ticker.unwrap_or_default());
7272    /// }
7273    /// # Ok(())
7274    /// # }
7275    /// ```
7276    pub async fn get_assets(&self) -> Result<Vec<Asset>, Error> {
7277        self.request_json(Method::GET, &["assets"], None::<&()>)
7278            .await
7279    }
7280
7281    /// Gets a specific asset by UUID.
7282    ///
7283    /// # Errors
7284    ///
7285    /// Returns an error if:
7286    /// - Authentication fails
7287    /// - The HTTP request fails
7288    /// - The asset does not exist
7289    /// - The response cannot be parsed
7290    ///
7291    /// # Examples
7292    /// ```no_run
7293    /// # use amp_rs::ApiClient;
7294    /// # #[tokio::main]
7295    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
7296    /// let client = ApiClient::new().await?;
7297    ///
7298    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
7299    /// let asset = client.get_asset(asset_uuid).await?;
7300    ///
7301    /// println!("Asset: {} ({})", asset.name, asset.ticker.unwrap_or_default());
7302    /// println!("Registered: {}, Locked: {}", asset.is_registered, asset.is_locked);
7303    /// # Ok(())
7304    /// # }
7305    /// ```
7306    pub async fn get_asset(&self, asset_uuid: &str) -> Result<Asset, Error> {
7307        self.request_json(Method::GET, &["assets", asset_uuid], None::<&()>)
7308            .await
7309    }
7310
7311    /// Issues a new asset.
7312    ///
7313    /// # Errors
7314    ///
7315    /// Returns an error if:
7316    /// - Authentication fails
7317    /// - The HTTP request fails
7318    /// - The issuance request is invalid
7319    /// - The response cannot be parsed
7320    ///
7321    /// # Examples
7322    /// ```no_run
7323    /// # use amp_rs::{ApiClient, model::IssuanceRequest};
7324    /// # #[tokio::main]
7325    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
7326    /// let client = ApiClient::new().await?;
7327    ///
7328    /// let issuance_request = IssuanceRequest {
7329    ///     name: "My Token".to_string(),
7330    ///     amount: 1000000,
7331    ///     destination_address: "vjU2i2EM2viGEzSywpStMPkTX9U9QSDsLSN63kJJYVpxKJZuxaph8v5r5Jf11aqnfBVdjSbrvcJ2pw26".to_string(),
7332    ///     domain: "example.com".to_string(),
7333    ///     ticker: "MYTKN".to_string(),
7334    ///     pubkey: "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798".to_string(),
7335    ///     precision: Some(8),
7336    ///     is_confidential: Some(true),
7337    ///     is_reissuable: Some(false),
7338    ///     reissuance_amount: None,
7339    ///     reissuance_address: None,
7340    ///     transfer_restricted: Some(false),
7341    /// };
7342    ///
7343    /// let response = client.issue_asset(&issuance_request).await?;
7344    /// println!("Issued asset with UUID: {}", response.asset_uuid);
7345    /// # Ok(())
7346    /// # }
7347    /// ```
7348    pub async fn issue_asset(
7349        &self,
7350        issuance_request: &IssuanceRequest,
7351    ) -> Result<IssuanceResponse, Error> {
7352        self.request_json(Method::POST, &["assets", "issue"], Some(issuance_request))
7353            .await
7354    }
7355
7356    /// Edits an existing asset.
7357    ///
7358    /// # Errors
7359    ///
7360    /// Returns an error if:
7361    /// - Authentication fails
7362    /// - The HTTP request fails
7363    /// - The asset does not exist
7364    /// - The edit request is invalid
7365    /// - The response cannot be parsed
7366    pub async fn edit_asset(
7367        &self,
7368        asset_uuid: &str,
7369        edit_asset_request: &EditAssetRequest,
7370    ) -> Result<Asset, Error> {
7371        self.request_json(
7372            Method::PUT,
7373            &["assets", asset_uuid, "edit"],
7374            Some(edit_asset_request),
7375        )
7376        .await
7377    }
7378
7379    /// # Errors
7380    /// Returns an error if:
7381    /// - The asset does not exist or cannot be found
7382    /// - Authentication fails or token is invalid
7383    /// - Network connectivity issues occur
7384    /// - The server returns an error status
7385    pub async fn delete_asset(&self, asset_uuid: &str) -> Result<(), Error> {
7386        self.request_empty(
7387            Method::DELETE,
7388            &["assets", asset_uuid, "delete"],
7389            None::<&()>,
7390        )
7391        .await
7392    }
7393
7394    /// # Errors
7395    /// Returns an error if:
7396    /// - The transaction ID is invalid or not found
7397    /// - Authentication fails or token is invalid
7398    /// - Network connectivity issues occur
7399    /// - The server returns an error status
7400    /// - The response cannot be parsed
7401    pub async fn get_broadcast_status(&self, txid: &str) -> Result<BroadcastResponse, Error> {
7402        self.request_json(Method::GET, &["tx", "broadcast", txid], None::<&()>)
7403            .await
7404    }
7405
7406    /// # Errors
7407    /// Returns an error if:
7408    /// - The transaction hex is invalid or malformed
7409    /// - The transaction is rejected by the network
7410    /// - Authentication fails or token is invalid
7411    /// - Network connectivity issues occur
7412    /// - The server returns an error status
7413    /// - The response cannot be parsed
7414    pub async fn broadcast_transaction(&self, tx_hex: &str) -> Result<BroadcastResponse, Error> {
7415        self.request_json(Method::POST, &["tx", "broadcast"], Some(tx_hex))
7416            .await
7417    }
7418
7419    /// # Errors
7420    /// Returns an error if:
7421    /// - The asset UUID is invalid or not found
7422    /// - The asset is already registered
7423    /// - Authentication fails or token is invalid
7424    /// - Network connectivity issues occur
7425    /// - The server returns an error status
7426    /// - The response cannot be parsed
7427    pub async fn register_asset(&self, asset_uuid: &str) -> Result<Asset, Error> {
7428        self.request_json(
7429            Method::GET,
7430            &["assets", asset_uuid, "register"],
7431            None::<&()>,
7432        )
7433        .await
7434    }
7435
7436    /// # Errors
7437    /// Returns an error if:
7438    /// - The asset UUID is invalid or not found
7439    /// - The user lacks authorization to register the asset
7440    /// - The asset is already registered
7441    /// - Authentication fails or token is invalid
7442    /// - Network connectivity issues occur
7443    /// - The server returns an error status
7444    /// - The response cannot be parsed
7445    pub async fn register_asset_authorized(&self, asset_uuid: &str) -> Result<Asset, Error> {
7446        self.request_json(
7447            Method::GET,
7448            &["assets", asset_uuid, "register-authorized"],
7449            None::<&()>,
7450        )
7451        .await
7452    }
7453
7454    /// # Errors
7455    /// Returns an error if:
7456    /// - The asset UUID is invalid or not found
7457    /// - The asset is already locked
7458    /// - The user lacks permission to lock the asset
7459    /// - Authentication fails or token is invalid
7460    /// - Network connectivity issues occur
7461    /// - The server returns an error status
7462    /// - The response cannot be parsed
7463    pub async fn lock_asset(&self, asset_uuid: &str) -> Result<Asset, Error> {
7464        self.request_json(Method::PUT, &["assets", asset_uuid, "lock"], None::<&()>)
7465            .await
7466    }
7467
7468    /// # Errors
7469    /// Returns an error if:
7470    /// - The asset UUID is invalid or not found
7471    /// - The asset is not currently locked
7472    /// - The user lacks permission to unlock the asset
7473    /// - Authentication fails or token is invalid
7474    /// - Network connectivity issues occur
7475    /// - The server returns an error status
7476    /// - The response cannot be parsed
7477    pub async fn unlock_asset(&self, asset_uuid: &str) -> Result<Asset, Error> {
7478        self.request_json(Method::PUT, &["assets", asset_uuid, "unlock"], None::<&()>)
7479            .await
7480    }
7481
7482    /// # Errors
7483    /// Returns an error if:
7484    /// - The asset UUID is invalid or not found
7485    /// - The activity parameters are invalid
7486    /// - Authentication fails or token is invalid
7487    /// - Network connectivity issues occur
7488    /// - The server returns an error status
7489    /// - The response cannot be parsed
7490    pub async fn get_asset_activities(
7491        &self,
7492        asset_uuid: &str,
7493        params: &AssetActivityParams,
7494    ) -> Result<Vec<Activity>, Error> {
7495        self.request_json(
7496            Method::GET,
7497            &["assets", asset_uuid, "activities"],
7498            Some(params),
7499        )
7500        .await
7501    }
7502
7503    /// # Errors
7504    /// Returns an error if:
7505    /// - The asset UUID is invalid or not found
7506    /// - The specified height is invalid or out of range
7507    /// - Authentication fails or token is invalid
7508    /// - Network connectivity issues occur
7509    /// - The server returns an error status
7510    /// - The response cannot be parsed
7511    pub async fn get_asset_ownerships(
7512        &self,
7513        asset_uuid: &str,
7514        height: Option<i64>,
7515    ) -> Result<Vec<Ownership>, Error> {
7516        let mut path = vec!["assets", asset_uuid, "ownerships"];
7517        let height_str;
7518        if let Some(h) = height {
7519            height_str = h.to_string();
7520            path.push(&height_str);
7521        }
7522        self.request_json(Method::GET, &path, None::<&()>).await
7523    }
7524
7525    /// # Errors
7526    /// Returns an error if:
7527    /// - The asset UUID is invalid or not found
7528    /// - Authentication fails or token is invalid
7529    /// - Network connectivity issues occur
7530    /// - The server returns an error status
7531    /// - The response cannot be parsed
7532    pub async fn get_asset_balance(&self, asset_uuid: &str) -> Result<Balance, Error> {
7533        self.request_json(Method::GET, &["assets", asset_uuid, "balance"], None::<&()>)
7534            .await
7535    }
7536
7537    /// # Errors
7538    /// Returns an error if:
7539    /// - The asset UUID is invalid or not found
7540    /// - Authentication fails or token is invalid
7541    /// - Network connectivity issues occur
7542    /// - The server returns an error status
7543    /// - The response cannot be parsed
7544    pub async fn get_asset_summary(&self, asset_uuid: &str) -> Result<AssetSummary, Error> {
7545        self.request_json(Method::GET, &["assets", asset_uuid, "summary"], None::<&()>)
7546            .await
7547    }
7548
7549    /// # Errors
7550    /// Returns an error if:
7551    /// - The asset UUID is invalid or not found
7552    /// - Authentication fails or token is invalid
7553    /// - Network connectivity issues occur
7554    /// - The server returns an error status
7555    /// - The response cannot be parsed
7556    pub async fn get_asset_utxos(&self, asset_uuid: &str) -> Result<Vec<Utxo>, Error> {
7557        self.request_json(Method::GET, &["assets", asset_uuid, "utxos"], None::<&()>)
7558            .await
7559    }
7560
7561    /// Gets the memo for a specific asset.
7562    ///
7563    /// # Arguments
7564    /// * `asset_uuid` - The UUID of the asset to retrieve the memo for
7565    ///
7566    /// # Returns
7567    /// The memo string associated with the asset
7568    ///
7569    /// # Errors
7570    /// Returns an error if:
7571    /// - Authentication fails
7572    /// - The HTTP request fails
7573    /// - The server returns an error status
7574    /// - The asset does not exist
7575    /// - The response cannot be parsed
7576    pub async fn get_asset_memo(&self, asset_uuid: &str) -> Result<String, Error> {
7577        self.request_json(Method::GET, &["assets", asset_uuid, "memo"], None::<&()>)
7578            .await
7579    }
7580
7581    /// Sets a memo for the specified asset.
7582    ///
7583    /// # Arguments
7584    /// * `asset_uuid` - The UUID of the asset to set the memo for
7585    /// * `memo` - The memo string to associate with the asset
7586    ///
7587    /// # Returns
7588    /// Returns `Ok(())` on success.
7589    ///
7590    /// # Errors
7591    /// Returns an error if:
7592    /// - Authentication fails
7593    /// - The HTTP request fails
7594    /// - The server returns an error status
7595    /// - The asset does not exist
7596    /// - The memo cannot be set due to validation errors
7597    ///
7598    /// # Example
7599    /// ```rust
7600    /// # use amp_rs::ApiClient;
7601    /// # async fn example(client: &ApiClient) -> Result<(), Box<dyn std::error::Error>> {
7602    /// client.set_asset_memo("asset-uuid-123", "This is a memo for the asset").await?;
7603    /// # Ok(())
7604    /// # }
7605    /// ```
7606    pub async fn set_asset_memo(&self, asset_uuid: &str, memo: &str) -> Result<(), Error> {
7607        let token = self.get_token().await?;
7608        let mut url = self.base_url.clone();
7609        url.path_segments_mut()
7610            .unwrap()
7611            .extend(&["assets", asset_uuid, "memo", "set"]);
7612
7613        let response = self
7614            .client
7615            .request(Method::POST, url)
7616            .header(AUTHORIZATION, format!("token {token}"))
7617            .header("content-type", "application/json")
7618            .body(format!("\"{}\"", memo.replace('"', "\\\"")))
7619            .send()
7620            .await?;
7621
7622        if !response.status().is_success() {
7623            let status = response.status();
7624            let error_text = response
7625                .text()
7626                .await
7627                .unwrap_or_else(|_| "Unknown error".to_string());
7628            return Err(Error::RequestFailed(format!(
7629                "Request to [\"assets\", \"{asset_uuid}\", \"memo\", \"set\"] failed with status {status}: {error_text}"
7630            )));
7631        }
7632
7633        Ok(())
7634    }
7635
7636    /// Blacklists specific UTXOs for an asset to prevent them from being used in transactions.
7637    ///
7638    /// This method adds the specified UTXOs to the asset's blacklist, preventing them from being
7639    /// used in future transactions. This is typically used for security purposes when UTXOs are
7640    /// suspected to be compromised or need to be temporarily disabled.
7641    ///
7642    /// # Arguments
7643    /// * `asset_uuid` - The UUID of the asset to blacklist UTXOs for
7644    /// * `utxos` - A slice of `Outpoint` structs representing the UTXOs to blacklist
7645    ///
7646    /// # Returns
7647    /// Returns a vector of `Utxo` structs representing the blacklisted UTXOs with their updated status.
7648    ///
7649    /// # Errors
7650    /// Returns an error if:
7651    /// - Authentication fails or insufficient permissions
7652    /// - The asset UUID is invalid or does not exist
7653    /// - One or more UTXOs are invalid or already blacklisted
7654    /// - The HTTP request fails
7655    /// - The server returns an error status
7656    /// - The response cannot be parsed
7657    ///
7658    /// # Examples
7659    /// ```no_run
7660    /// # use amp_rs::{ApiClient, model::Outpoint};
7661    /// # #[tokio::main]
7662    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
7663    /// let client = ApiClient::new().await?;
7664    ///
7665    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
7666    /// let utxos = vec![
7667    ///     Outpoint {
7668    ///         txid: "abc123...".to_string(),
7669    ///         vout: 0,
7670    ///     },
7671    ///     Outpoint {
7672    ///         txid: "def456...".to_string(),
7673    ///         vout: 1,
7674    ///     },
7675    /// ];
7676    ///
7677    /// let blacklisted_utxos = client.blacklist_asset_utxos(asset_uuid, &utxos).await?;
7678    /// println!("Blacklisted {} UTXOs", blacklisted_utxos.len());
7679    /// # Ok(())
7680    /// # }
7681    /// ```
7682    ///
7683    /// # Related Methods
7684    /// - [`whitelist_asset_utxos`](Self::whitelist_asset_utxos) - Remove UTXOs from blacklist
7685    /// - [`get_asset`](Self::get_asset) - Get asset information including UTXO status
7686    pub async fn blacklist_asset_utxos(
7687        &self,
7688        asset_uuid: &str,
7689        utxos: &[Outpoint],
7690    ) -> Result<Vec<Utxo>, Error> {
7691        self.request_json(
7692            Method::POST,
7693            &["assets", asset_uuid, "utxos", "blacklist"],
7694            Some(utxos),
7695        )
7696        .await
7697    }
7698
7699    /// Removes UTXOs from the asset's blacklist, allowing them to be used in transactions again.
7700    ///
7701    /// This method removes the specified UTXOs from the asset's blacklist, restoring their ability
7702    /// to be used in transactions. This is the reverse operation of blacklisting UTXOs.
7703    ///
7704    /// # Arguments
7705    /// * `asset_uuid` - The UUID of the asset to whitelist UTXOs for
7706    /// * `utxos` - A slice of `Outpoint` structs representing the UTXOs to remove from blacklist
7707    ///
7708    /// # Returns
7709    /// Returns a vector of `Utxo` structs representing the whitelisted UTXOs with their updated status.
7710    ///
7711    /// # Errors
7712    /// Returns an error if:
7713    /// - Authentication fails or insufficient permissions
7714    /// - The asset UUID is invalid or does not exist
7715    /// - One or more UTXOs are invalid or not currently blacklisted
7716    /// - The HTTP request fails
7717    /// - The server returns an error status
7718    /// - The response cannot be parsed
7719    ///
7720    /// # Examples
7721    /// ```no_run
7722    /// # use amp_rs::{ApiClient, model::Outpoint};
7723    /// # #[tokio::main]
7724    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
7725    /// let client = ApiClient::new().await?;
7726    ///
7727    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
7728    /// let utxos = vec![
7729    ///     Outpoint {
7730    ///         txid: "abc123...".to_string(),
7731    ///         vout: 0,
7732    ///     },
7733    /// ];
7734    ///
7735    /// let whitelisted_utxos = client.whitelist_asset_utxos(asset_uuid, &utxos).await?;
7736    /// println!("Whitelisted {} UTXOs", whitelisted_utxos.len());
7737    /// # Ok(())
7738    /// # }
7739    /// ```
7740    ///
7741    /// # Related Methods
7742    /// - [`blacklist_asset_utxos`](Self::blacklist_asset_utxos) - Add UTXOs to blacklist
7743    /// - [`get_asset`](Self::get_asset) - Get asset information including UTXO status
7744    pub async fn whitelist_asset_utxos(
7745        &self,
7746        asset_uuid: &str,
7747        utxos: &[Outpoint],
7748    ) -> Result<Vec<Utxo>, Error> {
7749        self.request_json(
7750            Method::POST,
7751            &["assets", asset_uuid, "utxos", "whitelist"],
7752            Some(utxos),
7753        )
7754        .await
7755    }
7756
7757    /// Gets the treasury addresses for a specific asset
7758    ///
7759    /// # Arguments
7760    /// * `asset_uuid` - The UUID of the asset to get treasury addresses for
7761    ///
7762    /// # Returns
7763    /// A vector of treasury addresses as strings
7764    ///
7765    /// # Errors
7766    /// Returns an error if:
7767    /// - The asset does not exist
7768    /// - The request fails
7769    /// - The response cannot be parsed
7770    pub async fn get_asset_treasury_addresses(
7771        &self,
7772        asset_uuid: &str,
7773    ) -> Result<Vec<String>, Error> {
7774        self.request_json(
7775            Method::GET,
7776            &["assets", asset_uuid, "treasury-addresses"],
7777            None::<&()>,
7778        )
7779        .await
7780    }
7781
7782    /// Adds treasury addresses to a specific asset
7783    ///
7784    /// # Arguments
7785    /// * `asset_uuid` - The UUID of the asset to add treasury addresses to
7786    /// * `addresses` - A slice of address strings to add as treasury addresses
7787    ///
7788    /// # Returns
7789    /// Returns `Ok(())` on success
7790    ///
7791    /// # Errors
7792    /// Returns an error if:
7793    /// - The asset does not exist
7794    /// - The addresses are invalid
7795    /// - The request fails
7796    /// - Insufficient permissions
7797    pub async fn add_asset_treasury_addresses(
7798        &self,
7799        asset_uuid: &str,
7800        addresses: &[String],
7801    ) -> Result<(), Error> {
7802        self.request_empty(
7803            Method::POST,
7804            &["assets", asset_uuid, "treasury-addresses", "add"],
7805            Some(addresses),
7806        )
7807        .await
7808    }
7809
7810    /// Removes treasury addresses from a specific asset.
7811    ///
7812    /// This method removes the specified addresses from the asset's treasury address list.
7813    /// Treasury addresses are special addresses that can be used for asset management operations
7814    /// such as reissuance and burning.
7815    ///
7816    /// # Arguments
7817    /// * `asset_uuid` - The UUID of the asset to remove treasury addresses from
7818    /// * `addresses` - A slice of address strings to remove from the treasury addresses
7819    ///
7820    /// # Returns
7821    /// Returns `Ok(())` on successful removal.
7822    ///
7823    /// # Errors
7824    /// Returns an error if:
7825    /// - Authentication fails or insufficient permissions
7826    /// - The asset UUID is invalid or does not exist
7827    /// - One or more addresses are invalid or not currently treasury addresses
7828    /// - The HTTP request fails
7829    /// - The server returns an error status
7830    /// - Attempting to remove the last treasury address (if not allowed)
7831    ///
7832    /// # Examples
7833    /// ```no_run
7834    /// # use amp_rs::ApiClient;
7835    /// # #[tokio::main]
7836    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
7837    /// let client = ApiClient::new().await?;
7838    ///
7839    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
7840    /// let addresses = vec![
7841    ///     "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh".to_string(),
7842    ///     "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4".to_string(),
7843    /// ];
7844    ///
7845    /// client.delete_asset_treasury_addresses(asset_uuid, &addresses).await?;
7846    /// println!("Removed {} treasury addresses", addresses.len());
7847    /// # Ok(())
7848    /// # }
7849    /// ```
7850    ///
7851    /// # Related Methods
7852    /// - [`add_asset_treasury_addresses`](Self::add_asset_treasury_addresses) - Add treasury addresses
7853    /// - [`get_asset_treasury_addresses`](Self::get_asset_treasury_addresses) - Get current treasury addresses
7854    /// - [`reissue_asset`](Self::reissue_asset) - Reissue assets using treasury addresses
7855    pub async fn delete_asset_treasury_addresses(
7856        &self,
7857        asset_uuid: &str,
7858        addresses: &[String],
7859    ) -> Result<(), Error> {
7860        self.request_empty(
7861            Method::DELETE,
7862            &["assets", asset_uuid, "treasury-addresses", "delete"],
7863            Some(addresses),
7864        )
7865        .await
7866    }
7867
7868    /// Gets a list of all registered users.
7869    ///
7870    /// # Errors
7871    /// Returns an error if:
7872    /// - Authentication fails
7873    /// - The HTTP request fails
7874    /// - The server returns an error status
7875    /// - The response cannot be parsed
7876    ///
7877    /// # Examples
7878    /// ```no_run
7879    /// # use amp_rs::ApiClient;
7880    /// # #[tokio::main]
7881    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
7882    /// let client = ApiClient::new().await?;
7883    ///
7884    /// let users = client.get_registered_users().await?;
7885    /// for user in users {
7886    ///     println!("User: {} (ID: {})", user.name, user.id);
7887    /// }
7888    /// # Ok(())
7889    /// # }
7890    /// ```
7891    pub async fn get_registered_users(
7892        &self,
7893    ) -> Result<Vec<crate::model::RegisteredUserResponse>, Error> {
7894        self.request_json(Method::GET, &["registered_users"], None::<&()>)
7895            .await
7896    }
7897
7898    /// Gets a specific registered user by ID.
7899    ///
7900    /// # Arguments
7901    /// * `user_id` - The ID of the registered user to retrieve
7902    ///
7903    /// # Errors
7904    /// Returns an error if:
7905    /// - Authentication fails
7906    /// - The HTTP request fails
7907    /// - The user ID does not exist
7908    /// - The response cannot be parsed
7909    ///
7910    /// # Examples
7911    /// ```no_run
7912    /// # use amp_rs::ApiClient;
7913    /// # #[tokio::main]
7914    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
7915    /// let client = ApiClient::new().await?;
7916    ///
7917    /// let user = client.get_registered_user(1).await?;
7918    /// println!("User: {} (ID: {})", user.name, user.id);
7919    /// # Ok(())
7920    /// # }
7921    /// ```
7922    pub async fn get_registered_user(
7923        &self,
7924        user_id: i64,
7925    ) -> Result<crate::model::RegisteredUserResponse, Error> {
7926        self.request_json(
7927            Method::GET,
7928            &["registered_users", &user_id.to_string()],
7929            None::<&()>,
7930        )
7931        .await
7932    }
7933
7934    /// Creates a new registered user in the AMP system.
7935    ///
7936    /// This method creates a new registered user with the provided information. Registered users
7937    /// can be associated with GAIDs, assigned to categories, and receive asset assignments.
7938    ///
7939    /// # Arguments
7940    /// * `new_user` - A `RegisteredUserAdd` struct containing the user information to create
7941    ///
7942    /// # Returns
7943    /// Returns a `RegisteredUserResponse` containing the created user's information including
7944    /// the assigned user ID.
7945    ///
7946    /// # Errors
7947    /// Returns an error if:
7948    /// - Authentication fails or insufficient permissions
7949    /// - The user data is invalid (e.g., missing required fields, invalid email format)
7950    /// - A user with the same identifier already exists
7951    /// - The HTTP request fails
7952    /// - The server returns an error status
7953    /// - The response cannot be parsed
7954    ///
7955    /// # Examples
7956    /// ```no_run
7957    /// # use amp_rs::{ApiClient, model::RegisteredUserAdd};
7958    /// # #[tokio::main]
7959    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
7960    /// let client = ApiClient::new().await?;
7961    ///
7962    /// let new_user = RegisteredUserAdd {
7963    ///     name: "John Doe".to_string(),
7964    ///     gaid: Some("GAbYScu6jkWUND2jo3L4KJxyvo55d".to_string()),
7965    ///     is_company: false,
7966    /// };
7967    ///
7968    /// let created_user = client.add_registered_user(&new_user).await?;
7969    /// println!("Created user: {} with ID {}", created_user.name, created_user.id);
7970    /// # Ok(())
7971    /// # }
7972    /// ```
7973    ///
7974    /// # Related Methods
7975    /// - [`get_registered_users`](Self::get_registered_users) - List all registered users
7976    /// - [`edit_registered_user`](Self::edit_registered_user) - Update user information
7977    /// - [`delete_registered_user`](Self::delete_registered_user) - Remove a user
7978    pub async fn add_registered_user(
7979        &self,
7980        new_user: &crate::model::RegisteredUserAdd,
7981    ) -> Result<crate::model::RegisteredUserResponse, Error> {
7982        self.request_json(Method::POST, &["registered_users", "add"], Some(new_user))
7983            .await
7984    }
7985
7986    /// Removes a registered user from the AMP system.
7987    ///
7988    /// This method permanently deletes a registered user and all associated data. This operation
7989    /// cannot be undone. Any GAIDs associated with the user will be disassociated, and any
7990    /// pending assignments may be affected.
7991    ///
7992    /// # Arguments
7993    /// * `user_id` - The ID of the registered user to delete
7994    ///
7995    /// # Returns
7996    /// Returns `Ok(())` on successful deletion.
7997    ///
7998    /// # Errors
7999    /// Returns an error if:
8000    /// - Authentication fails or insufficient permissions
8001    /// - The user ID is invalid or does not exist
8002    /// - The user has active assignments that prevent deletion
8003    /// - The HTTP request fails
8004    /// - The server returns an error status
8005    ///
8006    /// # Examples
8007    /// ```no_run
8008    /// # use amp_rs::ApiClient;
8009    /// # #[tokio::main]
8010    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
8011    /// let client = ApiClient::new().await?;
8012    ///
8013    /// let user_id = 123;
8014    /// client.delete_registered_user(user_id).await?;
8015    /// println!("Successfully deleted user with ID {}", user_id);
8016    /// # Ok(())
8017    /// # }
8018    /// ```
8019    ///
8020    /// # Related Methods
8021    /// - [`get_registered_user`](Self::get_registered_user) - Get user information before deletion
8022    /// - [`add_registered_user`](Self::add_registered_user) - Create a new user
8023    /// - [`get_registered_user_summary`](Self::get_registered_user_summary) - Check user's assignments
8024    pub async fn delete_registered_user(&self, user_id: i64) -> Result<(), Error> {
8025        self.request_empty(
8026            Method::DELETE,
8027            &["registered_users", &user_id.to_string(), "delete"],
8028            None::<&()>,
8029        )
8030        .await
8031    }
8032
8033    /// Updates registered user information.
8034    ///
8035    /// This method allows you to modify the information of an existing registered user.
8036    /// Only the fields provided in the edit data will be updated; other fields remain unchanged.
8037    ///
8038    /// # Arguments
8039    /// * `registered_user_id` - The ID of the registered user to update
8040    /// * `edit_data` - A `RegisteredUserEdit` struct containing the fields to update
8041    ///
8042    /// # Returns
8043    /// Returns a `RegisteredUserResponse` containing the updated user information.
8044    ///
8045    /// # Errors
8046    /// Returns an error if:
8047    /// - Authentication fails or insufficient permissions
8048    /// - The user ID is invalid or does not exist
8049    /// - The edit data contains invalid values (e.g., invalid email format)
8050    /// - The HTTP request fails
8051    /// - The server returns an error status
8052    /// - The response cannot be parsed
8053    ///
8054    /// # Examples
8055    /// ```no_run
8056    /// # use amp_rs::{ApiClient, model::RegisteredUserEdit};
8057    /// # #[tokio::main]
8058    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
8059    /// let client = ApiClient::new().await?;
8060    ///
8061    /// let user_id = 123;
8062    /// let edit_data = RegisteredUserEdit {
8063    ///     name: Some("Jane Doe".to_string()),
8064    /// };
8065    ///
8066    /// let updated_user = client.edit_registered_user(user_id, &edit_data).await?;
8067    /// println!("Updated user: {}", updated_user.name);
8068    /// # Ok(())
8069    /// # }
8070    /// ```
8071    ///
8072    /// # Related Methods
8073    /// - [`get_registered_user`](Self::get_registered_user) - Get current user information
8074    /// - [`add_registered_user`](Self::add_registered_user) - Create a new user
8075    /// - [`delete_registered_user`](Self::delete_registered_user) - Remove a user
8076    pub async fn edit_registered_user(
8077        &self,
8078        registered_user_id: i64,
8079        edit_data: &crate::model::RegisteredUserEdit,
8080    ) -> Result<crate::model::RegisteredUserResponse, Error> {
8081        self.request_json(
8082            Method::PUT,
8083            &["registered_users", &registered_user_id.to_string(), "edit"],
8084            Some(edit_data),
8085        )
8086        .await
8087    }
8088
8089    /// Gets comprehensive summary information for a registered user including assets and distributions.
8090    ///
8091    /// This method retrieves detailed summary information about a registered user, including
8092    /// their basic information, associated assets, assignment history, and distribution records.
8093    /// This provides a complete overview of the user's activity and holdings in the system.
8094    ///
8095    /// # Arguments
8096    /// * `registered_user_id` - The ID of the registered user to get summary for
8097    ///
8098    /// # Returns
8099    /// Returns a `RegisteredUserSummary` containing:
8100    /// - Basic user information (name, email, etc.)
8101    /// - List of associated GAIDs
8102    /// - Asset assignments and their status
8103    /// - Distribution history
8104    /// - Balance information
8105    /// - Activity timestamps
8106    ///
8107    /// # Errors
8108    /// Returns an error if:
8109    /// - Authentication fails or insufficient permissions
8110    /// - The user ID is invalid or does not exist
8111    /// - The HTTP request fails
8112    /// - The server returns an error status
8113    /// - The response cannot be parsed
8114    ///
8115    /// # Examples
8116    /// ```no_run
8117    /// # use amp_rs::ApiClient;
8118    /// # #[tokio::main]
8119    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
8120    /// let client = ApiClient::new().await?;
8121    ///
8122    /// let user_id = 123;
8123    /// let summary = client.get_registered_user_summary(user_id).await?;
8124    ///
8125    /// println!("Asset UUID: {}", summary.asset_uuid);
8126    /// println!("Asset ID: {}", summary.asset_id);
8127    /// println!("Asset assignments: {}", summary.assignments.len());
8128    /// println!("Distributions received: {}", summary.distributions.len());
8129    /// # Ok(())
8130    /// # }
8131    /// ```
8132    ///
8133    /// # Related Methods
8134    /// - [`get_registered_user`](Self::get_registered_user) - Get basic user information
8135    /// - [`get_registered_user_gaids`](Self::get_registered_user_gaids) - Get only GAIDs
8136    /// - [`get_asset_assignments`](Self::get_asset_assignments) - Get assignments for specific asset
8137    pub async fn get_registered_user_summary(
8138        &self,
8139        registered_user_id: i64,
8140    ) -> Result<crate::model::RegisteredUserSummary, Error> {
8141        self.request_json(
8142            Method::GET,
8143            &[
8144                "registered_users",
8145                &registered_user_id.to_string(),
8146                "summary",
8147            ],
8148            None::<&()>,
8149        )
8150        .await
8151    }
8152
8153    /// Gets all GAIDs (Green Address IDs) associated with a registered user.
8154    ///
8155    /// This method retrieves a list of all GAIDs that are currently associated with the specified
8156    /// registered user. GAIDs are unique identifiers that can be used to receive assets and
8157    /// track ownership.
8158    ///
8159    /// # Arguments
8160    /// * `registered_user_id` - The ID of the registered user to get GAIDs for
8161    ///
8162    /// # Returns
8163    /// Returns a vector of GAID strings associated with the user.
8164    ///
8165    /// # Errors
8166    /// Returns an error if:
8167    /// - Authentication fails or insufficient permissions
8168    /// - The user ID is invalid or does not exist
8169    /// - The HTTP request fails
8170    /// - The server returns an error status
8171    /// - The response cannot be parsed
8172    ///
8173    /// # Examples
8174    /// ```no_run
8175    /// # use amp_rs::ApiClient;
8176    /// # #[tokio::main]
8177    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
8178    /// let client = ApiClient::new().await?;
8179    ///
8180    /// let user_id = 123;
8181    /// let gaids = client.get_registered_user_gaids(user_id).await?;
8182    ///
8183    /// println!("User {} has {} associated GAIDs:", user_id, gaids.len());
8184    /// for gaid in gaids {
8185    ///     println!("  - {}", gaid);
8186    /// }
8187    /// # Ok(())
8188    /// # }
8189    /// ```
8190    ///
8191    /// # Related Methods
8192    /// - [`add_gaid_to_registered_user`](Self::add_gaid_to_registered_user) - Associate a GAID with user
8193    /// - [`set_default_gaid_for_registered_user`](Self::set_default_gaid_for_registered_user) - Set default GAID
8194    /// - [`get_gaid_registered_user`](Self::get_gaid_registered_user) - Find user by GAID
8195    /// - [`validate_gaid`](Self::validate_gaid) - Validate GAID format
8196    pub async fn get_registered_user_gaids(
8197        &self,
8198        registered_user_id: i64,
8199    ) -> Result<Vec<String>, Error> {
8200        self.request_json(
8201            Method::GET,
8202            &["registered_users", &registered_user_id.to_string(), "gaids"],
8203            None::<&()>,
8204        )
8205        .await
8206    }
8207
8208    /// Associates a GAID with a registered user.
8209    ///
8210    /// # Arguments
8211    /// * `registered_user_id` - The ID of the registered user
8212    /// * `gaid` - The GAID to associate with the user
8213    ///
8214    /// # Errors
8215    ///
8216    /// Returns an error if:
8217    /// - Authentication fails
8218    /// - The HTTP request fails
8219    /// - The server returns an error status
8220    /// - The registered user ID is invalid
8221    /// - The GAID is invalid or already associated
8222    pub async fn add_gaid_to_registered_user(
8223        &self,
8224        registered_user_id: i64,
8225        gaid: &str,
8226    ) -> Result<(), Error> {
8227        let request = GaidRequest {
8228            gaid: gaid.to_string(),
8229        };
8230
8231        self.request_empty(
8232            Method::POST,
8233            &[
8234                "registered_users",
8235                &registered_user_id.to_string(),
8236                "gaids",
8237                "add",
8238            ],
8239            Some(request),
8240        )
8241        .await
8242    }
8243
8244    /// Sets an existing GAID as the default for a registered user.
8245    ///
8246    /// This method allows you to designate a specific GAID as the primary/default
8247    /// GAID for a registered user. The GAID must already be associated with the user.
8248    ///
8249    /// # Arguments
8250    /// * `registered_user_id` - The ID of the registered user
8251    /// * `gaid` - The GAID to set as default
8252    ///
8253    /// # Returns
8254    /// Returns `Ok(())` if the operation is successful.
8255    ///
8256    /// # Errors
8257    /// Returns an error if:
8258    /// - Authentication fails
8259    /// - The HTTP request fails
8260    /// - The server returns an error status
8261    /// - The registered user ID is invalid
8262    /// - The GAID is not associated with the user
8263    pub async fn set_default_gaid_for_registered_user(
8264        &self,
8265        registered_user_id: i64,
8266        gaid: &str,
8267    ) -> Result<(), Error> {
8268        let request = GaidRequest {
8269            gaid: gaid.to_string(),
8270        };
8271
8272        self.request_empty(
8273            Method::POST,
8274            &[
8275                "registered_users",
8276                &registered_user_id.to_string(),
8277                "gaids",
8278                "set-default",
8279            ],
8280            Some(request),
8281        )
8282        .await
8283    }
8284
8285    /// Retrieves the registered user associated with a GAID
8286    ///
8287    /// # Arguments
8288    /// * `gaid` - The GAID to look up
8289    ///
8290    /// # Returns
8291    /// Returns the registered user data if the GAID is associated with a user
8292    ///
8293    /// # Errors
8294    /// This function will return an error if:
8295    /// - The GAID has no associated user
8296    /// - The GAID is invalid
8297    /// - Network or authentication errors occur
8298    pub async fn get_gaid_registered_user(
8299        &self,
8300        gaid: &str,
8301    ) -> Result<crate::model::RegisteredUserResponse, Error> {
8302        self.request_json(
8303            Method::GET,
8304            &["gaids", gaid, "registered_user"],
8305            None::<&()>,
8306        )
8307        .await
8308    }
8309
8310    /// Gets the balance information for a specific GAID.
8311    ///
8312    /// This method retrieves all asset balances associated with the given GAID,
8313    /// including confirmed balances and any lost outputs.
8314    ///
8315    /// # Arguments
8316    /// * `gaid` - The GAID to query balance for
8317    ///
8318    /// # Returns
8319    /// Returns a `Balance` struct containing confirmed balances and lost outputs
8320    ///
8321    /// # Errors
8322    /// Returns an error if:
8323    /// - The GAID is invalid
8324    /// - Network or authentication errors occur
8325    /// - The response cannot be parsed
8326    ///
8327    /// # Examples
8328    /// ```no_run
8329    /// # use amp_rs::ApiClient;
8330    /// # #[tokio::main]
8331    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
8332    /// let client = ApiClient::new().await?;
8333    ///
8334    /// let gaid = "GAbYScu6jkWUND2jo3L4KJxyvo55d";
8335    /// let balance = client.get_gaid_balance(gaid).await?;
8336    ///
8337    /// println!("GAID {} has {} balance entries", gaid, balance.len());
8338    /// for entry in balance {
8339    ///     println!("Asset {}: {} units", entry.asset_id, entry.balance);
8340    /// }
8341    /// # Ok(())
8342    /// # }
8343    /// ```
8344    pub async fn get_gaid_balance(&self, gaid: &str) -> Result<Balance, Error> {
8345        self.request_json(Method::GET, &["gaids", gaid, "balance"], None::<&()>)
8346            .await
8347    }
8348
8349    /// Retrieves the specific asset balance for a GAID
8350    ///
8351    /// # Arguments
8352    /// * `gaid` - The GAID to query
8353    /// * `asset_uuid` - The UUID of the asset to query
8354    ///
8355    /// # Returns
8356    /// Returns the specific asset balance information
8357    ///
8358    /// # Errors
8359    /// Returns an error if:
8360    /// - The GAID is invalid
8361    /// - The asset UUID is invalid
8362    /// - Network or authentication errors occur
8363    /// - The response cannot be parsed
8364    pub async fn get_gaid_asset_balance(
8365        &self,
8366        gaid: &str,
8367        asset_uuid: &str,
8368    ) -> Result<Ownership, Error> {
8369        // Try to get the response as a GaidBalanceEntry first, then convert to Ownership
8370        let balance_entry: GaidBalanceEntry = self
8371            .request_json(
8372                Method::GET,
8373                &["gaids", gaid, "balance", asset_uuid],
8374                None::<&()>,
8375            )
8376            .await?;
8377
8378        // Convert GaidBalanceEntry to Ownership format
8379        Ok(Ownership {
8380            owner: gaid.to_string(),
8381            amount: balance_entry.balance,
8382            gaid: Some(gaid.to_string()),
8383        })
8384    }
8385
8386    /// Gets a list of all categories.
8387    ///
8388    /// # Returns
8389    /// Returns a vector of `CategoryResponse` objects
8390    ///
8391    /// # Errors
8392    /// Returns an error if:
8393    /// - Authentication fails
8394    /// - The HTTP request fails
8395    /// - The response cannot be parsed
8396    ///
8397    /// # Examples
8398    /// ```no_run
8399    /// # use amp_rs::ApiClient;
8400    /// # #[tokio::main]
8401    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
8402    /// let client = ApiClient::new().await?;
8403    ///
8404    /// let categories = client.get_categories().await?;
8405    /// for category in categories {
8406    ///     println!("Category: {} (ID: {})", category.name, category.id);
8407    ///     if let Some(desc) = category.description {
8408    ///         println!("  Description: {}", desc);
8409    ///     }
8410    /// }
8411    /// # Ok(())
8412    /// # }
8413    /// ```
8414    pub async fn get_categories(&self) -> Result<Vec<CategoryResponse>, Error> {
8415        self.request_json(Method::GET, &["categories"], None::<&()>)
8416            .await
8417    }
8418
8419    /// Creates a new category for organizing users and assets.
8420    ///
8421    /// This method creates a new category that can be used to group registered users and assets
8422    /// for organizational purposes. Categories help manage permissions and provide logical
8423    /// groupings for assets and users.
8424    ///
8425    /// # Arguments
8426    /// * `new_category` - A `CategoryAdd` struct containing the category information to create
8427    ///
8428    /// # Returns
8429    /// Returns a `CategoryResponse` containing the created category information including
8430    /// the assigned category ID.
8431    ///
8432    /// # Errors
8433    /// Returns an error if:
8434    /// - Authentication fails or insufficient permissions
8435    /// - The category data is invalid (e.g., missing name, invalid characters)
8436    /// - A category with the same name already exists
8437    /// - The HTTP request fails
8438    /// - The server returns an error status
8439    /// - The response cannot be parsed
8440    ///
8441    /// # Examples
8442    /// ```no_run
8443    /// # use amp_rs::{ApiClient, model::CategoryAdd};
8444    /// # #[tokio::main]
8445    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
8446    /// let client = ApiClient::new().await?;
8447    ///
8448    /// let new_category = CategoryAdd {
8449    ///     name: "Premium Users".to_string(),
8450    ///     description: Some("High-value users with special privileges".to_string()),
8451    /// };
8452    ///
8453    /// let created_category = client.add_category(&new_category).await?;
8454    /// println!("Created category: {} with ID {}", created_category.name, created_category.id);
8455    /// # Ok(())
8456    /// # }
8457    /// ```
8458    ///
8459    /// # Related Methods
8460    /// - [`get_categories`](Self::get_categories) - List all categories
8461    /// - [`edit_category`](Self::edit_category) - Update category information
8462    /// - [`delete_category`](Self::delete_category) - Remove a category
8463    /// - [`add_registered_user_to_category`](Self::add_registered_user_to_category) - Add users to category
8464    pub async fn add_category(
8465        &self,
8466        new_category: &CategoryAdd,
8467    ) -> Result<CategoryResponse, Error> {
8468        self.request_json(Method::POST, &["categories", "add"], Some(new_category))
8469            .await
8470    }
8471
8472    /// Gets a specific category by ID.
8473    ///
8474    /// This method retrieves detailed information about a specific category, including
8475    /// its name, description, and associated users and assets.
8476    ///
8477    /// # Arguments
8478    /// * `category_id` - The ID of the category to retrieve
8479    ///
8480    /// # Returns
8481    /// Returns a `CategoryResponse` containing the category information including:
8482    /// - Category ID, name, and description
8483    /// - List of associated registered users
8484    /// - List of associated assets
8485    /// - Creation and modification timestamps
8486    ///
8487    /// # Errors
8488    /// Returns an error if:
8489    /// - Authentication fails or insufficient permissions
8490    /// - The category ID is invalid or does not exist
8491    /// - The HTTP request fails
8492    /// - The server returns an error status
8493    /// - The response cannot be parsed
8494    ///
8495    /// # Examples
8496    /// ```no_run
8497    /// # use amp_rs::ApiClient;
8498    /// # #[tokio::main]
8499    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
8500    /// let client = ApiClient::new().await?;
8501    ///
8502    /// let category_id = 1;
8503    /// let category = client.get_category(category_id).await?;
8504    ///
8505    /// println!("Category: {} (ID: {})", category.name, category.id);
8506    /// if let Some(desc) = category.description {
8507    ///     println!("Description: {}", desc);
8508    /// }
8509    /// println!("Users: {}, Assets: {}", category.registered_users.len(), category.assets.len());
8510    /// # Ok(())
8511    /// # }
8512    /// ```
8513    ///
8514    /// # Related Methods
8515    /// - [`get_categories`](Self::get_categories) - List all categories
8516    /// - [`add_category`](Self::add_category) - Create a new category
8517    /// - [`edit_category`](Self::edit_category) - Update category information
8518    /// - [`delete_category`](Self::delete_category) - Remove a category
8519    pub async fn get_category(&self, category_id: i64) -> Result<CategoryResponse, Error> {
8520        self.request_json(
8521            Method::GET,
8522            &["categories", &category_id.to_string()],
8523            None::<&()>,
8524        )
8525        .await
8526    }
8527
8528    /// Updates category information.
8529    ///
8530    /// This method allows you to modify the information of an existing category.
8531    /// Only the fields provided in the edit data will be updated; other fields remain unchanged.
8532    ///
8533    /// # Arguments
8534    /// * `category_id` - The ID of the category to update
8535    /// * `edit_category` - A `CategoryEdit` struct containing the fields to update
8536    ///
8537    /// # Returns
8538    /// Returns a `CategoryResponse` containing the updated category information.
8539    ///
8540    /// # Errors
8541    /// Returns an error if:
8542    /// - Authentication fails or insufficient permissions
8543    /// - The category ID is invalid or does not exist
8544    /// - The edit data contains invalid values (e.g., empty name, invalid characters)
8545    /// - A category with the new name already exists (if name is being changed)
8546    /// - The HTTP request fails
8547    /// - The server returns an error status
8548    /// - The response cannot be parsed
8549    ///
8550    /// # Examples
8551    /// ```no_run
8552    /// # use amp_rs::{ApiClient, model::CategoryEdit};
8553    /// # #[tokio::main]
8554    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
8555    /// let client = ApiClient::new().await?;
8556    ///
8557    /// let category_id = 1;
8558    /// let edit_data = CategoryEdit {
8559    ///     name: Some("VIP Users".to_string()),
8560    ///     description: Some("Very important users with premium access".to_string()),
8561    /// };
8562    ///
8563    /// let updated_category = client.edit_category(category_id, &edit_data).await?;
8564    /// println!("Updated category: {}", updated_category.name);
8565    /// # Ok(())
8566    /// # }
8567    /// ```
8568    ///
8569    /// # Related Methods
8570    /// - [`get_category`](Self::get_category) - Get current category information
8571    /// - [`add_category`](Self::add_category) - Create a new category
8572    /// - [`delete_category`](Self::delete_category) - Remove a category
8573    pub async fn edit_category(
8574        &self,
8575        category_id: i64,
8576        edit_category: &CategoryEdit,
8577    ) -> Result<CategoryResponse, Error> {
8578        self.request_json(
8579            Method::PUT,
8580            &["categories", &category_id.to_string(), "edit"],
8581            Some(edit_category),
8582        )
8583        .await
8584    }
8585
8586    /// Removes a category from the system.
8587    ///
8588    /// This method permanently deletes a category. All users and assets associated with the
8589    /// category will be disassociated, but the users and assets themselves are not deleted.
8590    /// This operation cannot be undone.
8591    ///
8592    /// # Arguments
8593    /// * `category_id` - The ID of the category to delete
8594    ///
8595    /// # Returns
8596    /// Returns `Ok(())` on successful deletion.
8597    ///
8598    /// # Errors
8599    /// Returns an error if:
8600    /// - Authentication fails or insufficient permissions
8601    /// - The category ID is invalid or does not exist
8602    /// - The category is still in use and cannot be deleted (depending on system configuration)
8603    /// - The HTTP request fails
8604    /// - The server returns an error status
8605    ///
8606    /// # Examples
8607    /// ```no_run
8608    /// # use amp_rs::ApiClient;
8609    /// # #[tokio::main]
8610    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
8611    /// let client = ApiClient::new().await?;
8612    ///
8613    /// let category_id = 1;
8614    /// client.delete_category(category_id).await?;
8615    /// println!("Successfully deleted category with ID {}", category_id);
8616    /// # Ok(())
8617    /// # }
8618    /// ```
8619    ///
8620    /// # Related Methods
8621    /// - [`get_category`](Self::get_category) - Get category information before deletion
8622    /// - [`add_category`](Self::add_category) - Create a new category
8623    /// - [`remove_registered_user_from_category`](Self::remove_registered_user_from_category) - Remove users first
8624    /// - [`remove_asset_from_category`](Self::remove_asset_from_category) - Remove assets first
8625    pub async fn delete_category(&self, category_id: i64) -> Result<(), Error> {
8626        self.request_empty(
8627            Method::DELETE,
8628            &["categories", &category_id.to_string(), "delete"],
8629            None::<&()>,
8630        )
8631        .await
8632    }
8633
8634    /// Associates a registered user with a category.
8635    ///
8636    /// This method adds a registered user to a category, allowing for organized grouping
8637    /// of users. Users can belong to multiple categories, and categories can contain
8638    /// multiple users.
8639    ///
8640    /// # Arguments
8641    /// * `category_id` - The ID of the category to add the user to
8642    /// * `user_id` - The ID of the registered user to add to the category
8643    ///
8644    /// # Returns
8645    /// Returns a `CategoryResponse` containing the updated category information including
8646    /// the newly added user.
8647    ///
8648    /// # Errors
8649    /// Returns an error if:
8650    /// - Authentication fails or insufficient permissions
8651    /// - The category ID is invalid or does not exist
8652    /// - The user ID is invalid or does not exist
8653    /// - The user is already associated with the category
8654    /// - The HTTP request fails
8655    /// - The server returns an error status
8656    /// - The response cannot be parsed
8657    ///
8658    /// # Examples
8659    /// ```no_run
8660    /// # use amp_rs::ApiClient;
8661    /// # #[tokio::main]
8662    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
8663    /// let client = ApiClient::new().await?;
8664    ///
8665    /// let category_id = 1;
8666    /// let user_id = 123;
8667    ///
8668    /// let updated_category = client.add_registered_user_to_category(category_id, user_id).await?;
8669    /// println!("Added user {} to category '{}'", user_id, updated_category.name);
8670    /// println!("Category now has {} users", updated_category.registered_users.len());
8671    /// # Ok(())
8672    /// # }
8673    /// ```
8674    ///
8675    /// # Related Methods
8676    /// - [`remove_registered_user_from_category`](Self::remove_registered_user_from_category) - Remove user from category
8677    /// - [`get_category`](Self::get_category) - Get category information including users
8678    /// - [`get_registered_user`](Self::get_registered_user) - Get user information
8679    pub async fn add_registered_user_to_category(
8680        &self,
8681        category_id: i64,
8682        user_id: i64,
8683    ) -> Result<CategoryResponse, Error> {
8684        self.request_json(
8685            Method::PUT,
8686            &[
8687                "categories",
8688                &category_id.to_string(),
8689                "registered_users",
8690                &user_id.to_string(),
8691                "add",
8692            ],
8693            None::<&()>,
8694        )
8695        .await
8696    }
8697
8698    /// Removes a registered user from a category.
8699    ///
8700    /// This method disassociates a registered user from a category. The user remains in the
8701    /// system but is no longer part of the specified category. This does not affect the user's
8702    /// association with other categories.
8703    ///
8704    /// # Arguments
8705    /// * `category_id` - The ID of the category to remove the user from
8706    /// * `user_id` - The ID of the registered user to remove from the category
8707    ///
8708    /// # Returns
8709    /// Returns a `CategoryResponse` containing the updated category information without
8710    /// the removed user.
8711    ///
8712    /// # Errors
8713    /// Returns an error if:
8714    /// - Authentication fails or insufficient permissions
8715    /// - The category ID is invalid or does not exist
8716    /// - The user ID is invalid or does not exist
8717    /// - The user is not currently associated with the category
8718    /// - The HTTP request fails
8719    /// - The server returns an error status
8720    /// - The response cannot be parsed
8721    ///
8722    /// # Examples
8723    /// ```no_run
8724    /// # use amp_rs::ApiClient;
8725    /// # #[tokio::main]
8726    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
8727    /// let client = ApiClient::new().await?;
8728    ///
8729    /// let category_id = 1;
8730    /// let user_id = 123;
8731    ///
8732    /// let updated_category = client.remove_registered_user_from_category(category_id, user_id).await?;
8733    /// println!("Removed user {} from category '{}'", user_id, updated_category.name);
8734    /// println!("Category now has {} users", updated_category.registered_users.len());
8735    /// # Ok(())
8736    /// # }
8737    /// ```
8738    ///
8739    /// # Related Methods
8740    /// - [`add_registered_user_to_category`](Self::add_registered_user_to_category) - Add user to category
8741    /// - [`get_category`](Self::get_category) - Get category information including users
8742    /// - [`get_registered_user`](Self::get_registered_user) - Get user information
8743    pub async fn remove_registered_user_from_category(
8744        &self,
8745        category_id: i64,
8746        user_id: i64,
8747    ) -> Result<CategoryResponse, Error> {
8748        self.request_json(
8749            Method::PUT,
8750            &[
8751                "categories",
8752                &category_id.to_string(),
8753                "registered_users",
8754                &user_id.to_string(),
8755                "remove",
8756            ],
8757            None::<&()>,
8758        )
8759        .await
8760    }
8761
8762    /// Associates an asset with a category.
8763    ///
8764    /// This method adds an asset to a category, allowing for organized grouping of assets.
8765    /// Assets can belong to multiple categories, and categories can contain multiple assets.
8766    /// This helps with asset management and permission organization.
8767    ///
8768    /// # Arguments
8769    /// * `category_id` - The ID of the category to add the asset to
8770    /// * `asset_uuid` - The UUID of the asset to add to the category
8771    ///
8772    /// # Returns
8773    /// Returns a `CategoryResponse` containing the updated category information including
8774    /// the newly added asset.
8775    ///
8776    /// # Errors
8777    /// Returns an error if:
8778    /// - Authentication fails or insufficient permissions
8779    /// - The category ID is invalid or does not exist
8780    /// - The asset UUID is invalid or does not exist
8781    /// - The asset is already associated with the category
8782    /// - The HTTP request fails
8783    /// - The server returns an error status
8784    /// - The response cannot be parsed
8785    ///
8786    /// # Examples
8787    /// ```no_run
8788    /// # use amp_rs::ApiClient;
8789    /// # #[tokio::main]
8790    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
8791    /// let client = ApiClient::new().await?;
8792    ///
8793    /// let category_id = 1;
8794    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
8795    ///
8796    /// let updated_category = client.add_asset_to_category(category_id, asset_uuid).await?;
8797    /// println!("Added asset {} to category '{}'", asset_uuid, updated_category.name);
8798    /// println!("Category now has {} assets", updated_category.assets.len());
8799    /// # Ok(())
8800    /// # }
8801    /// ```
8802    ///
8803    /// # Related Methods
8804    /// - [`remove_asset_from_category`](Self::remove_asset_from_category) - Remove asset from category
8805    /// - [`get_category`](Self::get_category) - Get category information including assets
8806    /// - [`get_asset`](Self::get_asset) - Get asset information
8807    pub async fn add_asset_to_category(
8808        &self,
8809        category_id: i64,
8810        asset_uuid: &str,
8811    ) -> Result<CategoryResponse, Error> {
8812        self.request_json(
8813            Method::PUT,
8814            &[
8815                "categories",
8816                &category_id.to_string(),
8817                "assets",
8818                asset_uuid,
8819                "add",
8820            ],
8821            None::<&()>,
8822        )
8823        .await
8824    }
8825
8826    /// Removes an asset from a category.
8827    ///
8828    /// This method disassociates an asset from a category. The asset remains in the system
8829    /// but is no longer part of the specified category. This does not affect the asset's
8830    /// association with other categories.
8831    ///
8832    /// # Arguments
8833    /// * `category_id` - The ID of the category to remove the asset from
8834    /// * `asset_uuid` - The UUID of the asset to remove from the category
8835    ///
8836    /// # Returns
8837    /// Returns a `CategoryResponse` containing the updated category information without
8838    /// the removed asset.
8839    ///
8840    /// # Errors
8841    /// Returns an error if:
8842    /// - Authentication fails or insufficient permissions
8843    /// - The category ID is invalid or does not exist
8844    /// - The asset UUID is invalid or does not exist
8845    /// - The asset is not currently associated with the category
8846    /// - The HTTP request fails
8847    /// - The server returns an error status
8848    /// - The response cannot be parsed
8849    ///
8850    /// # Examples
8851    /// ```no_run
8852    /// # use amp_rs::ApiClient;
8853    /// # #[tokio::main]
8854    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
8855    /// let client = ApiClient::new().await?;
8856    ///
8857    /// let category_id = 1;
8858    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
8859    ///
8860    /// let updated_category = client.remove_asset_from_category(category_id, asset_uuid).await?;
8861    /// println!("Removed asset {} from category '{}'", asset_uuid, updated_category.name);
8862    /// println!("Category now has {} assets", updated_category.assets.len());
8863    /// # Ok(())
8864    /// # }
8865    /// ```
8866    ///
8867    /// # Related Methods
8868    /// - [`add_asset_to_category`](Self::add_asset_to_category) - Add asset to category
8869    /// - [`get_category`](Self::get_category) - Get category information including assets
8870    /// - [`get_asset`](Self::get_asset) - Get asset information
8871    pub async fn remove_asset_from_category(
8872        &self,
8873        category_id: i64,
8874        asset_uuid: &str,
8875    ) -> Result<CategoryResponse, Error> {
8876        self.request_json(
8877            Method::PUT,
8878            &[
8879                "categories",
8880                &category_id.to_string(),
8881                "assets",
8882                asset_uuid,
8883                "remove",
8884            ],
8885            None::<&()>,
8886        )
8887        .await
8888    }
8889
8890    /// Validates a GAID (Green Address ID).
8891    ///
8892    /// # Arguments
8893    /// * `gaid` - The GAID string to validate
8894    ///
8895    /// # Returns
8896    /// Returns a `ValidateGaidResponse` indicating whether the GAID is valid
8897    ///
8898    /// # Errors
8899    /// Returns an error if:
8900    /// - Authentication fails
8901    /// - The HTTP request fails
8902    /// - The response cannot be parsed
8903    ///
8904    /// # Examples
8905    /// ```no_run
8906    /// # use amp_rs::ApiClient;
8907    /// # #[tokio::main]
8908    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
8909    /// let client = ApiClient::new().await?;
8910    ///
8911    /// let gaid = "GAbYScu6jkWUND2jo3L4KJxyvo55d";
8912    /// let validation = client.validate_gaid(gaid).await?;
8913    ///
8914    /// if validation.is_valid {
8915    ///     println!("GAID {} is valid", gaid);
8916    /// } else {
8917    ///     println!("GAID {} is invalid: {:?}", gaid, validation.error);
8918    /// }
8919    /// # Ok(())
8920    /// # }
8921    /// ```
8922    pub async fn validate_gaid(
8923        &self,
8924        gaid: &str,
8925    ) -> Result<crate::model::ValidateGaidResponse, Error> {
8926        self.request_json(Method::GET, &["gaids", gaid, "validate"], None::<&()>)
8927            .await
8928    }
8929
8930    /// Gets the address associated with a GAID.
8931    ///
8932    /// # Arguments
8933    /// * `gaid` - The GAID to get the address for
8934    ///
8935    /// # Returns
8936    /// Returns an `AddressGaidResponse` containing the address
8937    ///
8938    /// # Errors
8939    /// Returns an error if:
8940    /// - The GAID is invalid
8941    /// - Authentication fails
8942    /// - The HTTP request fails
8943    /// - The response cannot be parsed
8944    ///
8945    /// # Examples
8946    /// ```no_run
8947    /// # use amp_rs::ApiClient;
8948    /// # #[tokio::main]
8949    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
8950    /// let client = ApiClient::new().await?;
8951    ///
8952    /// let gaid = "GAbYScu6jkWUND2jo3L4KJxyvo55d";
8953    /// let address_response = client.get_gaid_address(gaid).await?;
8954    ///
8955    /// println!("Address for GAID {}: {}", gaid, address_response.address);
8956    /// # Ok(())
8957    /// # }
8958    /// ```
8959    pub async fn get_gaid_address(
8960        &self,
8961        gaid: &str,
8962    ) -> Result<crate::model::AddressGaidResponse, Error> {
8963        self.request_json(Method::GET, &["gaids", gaid, "address"], None::<&()>)
8964            .await
8965    }
8966
8967    /// Gets a list of all managers.
8968    ///
8969    /// # Returns
8970    /// Returns a vector of `Manager` objects
8971    ///
8972    /// # Errors
8973    /// Returns an error if:
8974    /// - Authentication fails
8975    /// - The HTTP request fails
8976    /// - The response cannot be parsed
8977    ///
8978    /// # Examples
8979    /// ```no_run
8980    /// # use amp_rs::ApiClient;
8981    /// # #[tokio::main]
8982    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
8983    /// let client = ApiClient::new().await?;
8984    ///
8985    /// let managers = client.get_managers().await?;
8986    /// for manager in managers {
8987    ///     println!("Manager: {} (ID: {})", manager.username, manager.id);
8988    /// }
8989    /// # Ok(())
8990    /// # }
8991    /// ```
8992    pub async fn get_managers(&self) -> Result<Vec<crate::model::Manager>, Error> {
8993        self.request_json(Method::GET, &["managers"], None::<&()>)
8994            .await
8995    }
8996
8997    /// Creates a new manager.
8998    ///
8999    /// # Arguments
9000    /// * `new_manager` - The manager creation request containing username and password
9001    ///
9002    /// # Returns
9003    /// Returns the created `Manager` object
9004    ///
9005    /// # Errors
9006    /// Returns an error if:
9007    /// - Authentication fails
9008    /// - The HTTP request fails
9009    /// - The manager creation request is invalid
9010    /// - The response cannot be parsed
9011    ///
9012    /// # Examples
9013    /// ```no_run
9014    /// # use amp_rs::{ApiClient, model::ManagerCreate};
9015    /// # #[tokio::main]
9016    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9017    /// let client = ApiClient::new().await?;
9018    ///
9019    /// let new_manager = ManagerCreate {
9020    ///     username: "new_manager".to_string(),
9021    ///     password: "secure_password".to_string(),
9022    /// };
9023    ///
9024    /// let manager = client.create_manager(&new_manager).await?;
9025    /// println!("Created manager: {} (ID: {})", manager.username, manager.id);
9026    /// # Ok(())
9027    /// # }
9028    /// ```
9029    pub async fn create_manager(
9030        &self,
9031        new_manager: &crate::model::ManagerCreate,
9032    ) -> Result<crate::model::Manager, Error> {
9033        self.request_json(Method::POST, &["managers", "create"], Some(new_manager))
9034            .await
9035    }
9036
9037    /// Gets all assignments for a specific asset.
9038    ///
9039    /// # Arguments
9040    /// * `asset_uuid` - The UUID of the asset to get assignments for
9041    ///
9042    /// # Returns
9043    /// Returns a vector of `Assignment` objects
9044    ///
9045    /// # Errors
9046    /// Returns an error if:
9047    /// - Authentication fails
9048    /// - The HTTP request fails
9049    /// - The asset UUID is invalid
9050    /// - The response cannot be parsed
9051    ///
9052    /// # Examples
9053    /// ```no_run
9054    /// # use amp_rs::ApiClient;
9055    /// # #[tokio::main]
9056    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9057    /// let client = ApiClient::new().await?;
9058    ///
9059    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
9060    /// let assignments = client.get_asset_assignments(asset_uuid).await?;
9061    ///
9062    /// for assignment in assignments {
9063    ///     println!("Assignment ID: {}, Amount: {}", assignment.id, assignment.amount);
9064    /// }
9065    /// # Ok(())
9066    /// # }
9067    /// ```
9068    pub async fn get_asset_assignments(&self, asset_uuid: &str) -> Result<Vec<Assignment>, Error> {
9069        self.request_json(
9070            Method::GET,
9071            &["assets", asset_uuid, "assignments"],
9072            None::<&()>,
9073        )
9074        .await
9075    }
9076
9077    /// Creates multiple asset assignments in batch.
9078    ///
9079    /// This method creates multiple asset assignments for the specified asset. Each assignment
9080    /// allocates a specific amount of the asset to a registered user. The assignments are
9081    /// created individually due to API limitations, but this method handles the batch processing
9082    /// automatically.
9083    ///
9084    /// # Arguments
9085    /// * `asset_uuid` - The UUID of the asset to create assignments for
9086    /// * `requests` - A slice of `CreateAssetAssignmentRequest` structs containing assignment details
9087    ///
9088    /// # Returns
9089    /// Returns a vector of `Assignment` structs representing the created assignments with their
9090    /// assigned IDs and status information.
9091    ///
9092    /// # Errors
9093    /// Returns an error if:
9094    /// - Authentication fails or insufficient permissions
9095    /// - The asset UUID is invalid or does not exist
9096    /// - Any assignment request contains invalid data (e.g., invalid user ID, negative amount)
9097    /// - Insufficient asset balance for the total requested assignments
9098    /// - Any individual assignment creation fails
9099    /// - The HTTP request fails
9100    /// - The server returns an error status
9101    /// - The response cannot be parsed
9102    ///
9103    /// # Examples
9104    /// ```no_run
9105    /// # use amp_rs::{ApiClient, model::CreateAssetAssignmentRequest};
9106    /// # #[tokio::main]
9107    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9108    /// let client = ApiClient::new().await?;
9109    ///
9110    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
9111    /// let requests = vec![
9112    ///     CreateAssetAssignmentRequest {
9113    ///         registered_user: 123,
9114    ///         amount: 1000,
9115    ///         vesting_timestamp: None,
9116    ///         ready_for_distribution: false,
9117    ///     },
9118    ///     CreateAssetAssignmentRequest {
9119    ///         registered_user: 456,
9120    ///         amount: 500,
9121    ///         vesting_timestamp: None,
9122    ///         ready_for_distribution: true,
9123    ///     },
9124    /// ];
9125    ///
9126    /// let assignments = client.create_asset_assignments(asset_uuid, &requests).await?;
9127    /// println!("Created {} assignments", assignments.len());
9128    /// for assignment in assignments {
9129    ///     println!("Assignment {}: {} units to user {}",
9130    ///              assignment.id, assignment.amount, assignment.registered_user);
9131    /// }
9132    /// # Ok(())
9133    /// # }
9134    /// ```
9135    ///
9136    /// # Related Methods
9137    /// - [`get_asset_assignments`](Self::get_asset_assignments) - List all assignments for an asset
9138    /// - [`delete_asset_assignment`](Self::delete_asset_assignment) - Remove an assignment
9139    /// - [`edit_asset_assignment`](Self::edit_asset_assignment) - Update assignment details
9140    /// - [`set_assignment_ready_for_distribution`](Self::set_assignment_ready_for_distribution) - Mark for distribution
9141    pub async fn create_asset_assignments(
9142        &self,
9143        asset_uuid: &str,
9144        requests: &[CreateAssetAssignmentRequest],
9145    ) -> Result<Vec<Assignment>, Error> {
9146        use crate::model::CreateAssetAssignmentRequestWrapper;
9147
9148        // The API only supports maximum length 1 per request, so we need to break
9149        // multiple assignments into separate CreateAssetAssignmentRequestWrapper instances
9150        let mut all_assignments = Vec::new();
9151
9152        for request in requests {
9153            let wrapper = CreateAssetAssignmentRequestWrapper {
9154                assignments: vec![request.clone()],
9155            };
9156
9157            let assignments: Vec<Assignment> = self
9158                .request_json(
9159                    Method::POST,
9160                    &["assets", asset_uuid, "assignments", "create"],
9161                    Some(&wrapper),
9162                )
9163                .await?;
9164
9165            all_assignments.extend(assignments);
9166        }
9167
9168        Ok(all_assignments)
9169    }
9170
9171    /// Gets a specific asset assignment by asset UUID and assignment ID.
9172    ///
9173    /// This method sends a GET request to retrieve detailed information about a specific asset
9174    /// assignment. Asset assignments represent the allocation of assets to users or entities,
9175    /// including information such as the assigned amount, recipient details, and assignment status.
9176    ///
9177    /// # Arguments
9178    /// * `asset_uuid` - The UUID of the asset for which to retrieve the assignment
9179    /// * `assignment_id` - The ID of the specific assignment to retrieve
9180    ///
9181    /// # Returns
9182    /// Returns an `Assignment` struct containing the assignment details including:
9183    /// - Assignment ID and amount
9184    /// - Recipient information
9185    /// - Assignment status and metadata
9186    /// - Creation and modification timestamps
9187    ///
9188    /// # Errors
9189    /// Returns an error if:
9190    /// - Authentication fails
9191    /// - The HTTP request fails
9192    /// - The server returns an error status
9193    /// - The asset UUID is invalid or does not exist
9194    /// - The assignment ID is invalid or does not exist
9195    /// - The assignment is not accessible to the current user
9196    /// - The response cannot be parsed as a valid Assignment
9197    ///
9198    /// # Example
9199    /// ```no_run
9200    /// # use amp_rs::ApiClient;
9201    /// # #[tokio::main]
9202    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9203    /// let client = ApiClient::new().await?;
9204    ///
9205    /// // Retrieve assignment with ID "123" for asset "550e8400-e29b-41d4-a716-446655440000"
9206    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
9207    /// let assignment_id = "123";
9208    ///
9209    /// let assignment = client.get_asset_assignment(asset_uuid, assignment_id).await?;
9210    ///
9211    /// println!("Assignment ID: {}", assignment.id);
9212    /// println!("Assigned amount: {}", assignment.amount);
9213    /// println!("Registered user: {}", assignment.registered_user);
9214    /// # Ok(())
9215    /// # }
9216    /// ```
9217    pub async fn get_asset_assignment(
9218        &self,
9219        asset_uuid: &str,
9220        assignment_id: &str,
9221    ) -> Result<Assignment, Error> {
9222        self.request_json(
9223            Method::GET,
9224            &["assets", asset_uuid, "assignments", assignment_id],
9225            None::<&()>,
9226        )
9227        .await
9228    }
9229
9230    /// Creates a distribution for an asset with the specified assignments.
9231    ///
9232    /// This method initiates the distribution creation process by sending assignment details
9233    /// to the AMP API. The API will return a distribution UUID and address mappings that
9234    /// can be used for subsequent transaction creation and confirmation steps.
9235    ///
9236    /// # Arguments
9237    /// * `asset_uuid` - The UUID of the asset to distribute
9238    /// * `assignments` - A vector of `AssetDistributionAssignment` structs containing user IDs, addresses, and amounts
9239    ///
9240    /// # Returns
9241    /// Returns a `DistributionResponse` containing:
9242    /// - `distribution_uuid` - Unique identifier for the created distribution
9243    /// - `map_address_amount` - Mapping of addresses to amounts to be distributed
9244    /// - `map_address_asset` - Mapping of addresses to asset IDs
9245    /// - `asset_id` - The asset ID for the distribution
9246    ///
9247    /// # Errors
9248    /// Returns an `AmpError` if:
9249    /// - Authentication fails or insufficient permissions
9250    /// - The asset UUID is invalid or does not exist
9251    /// - Assignment data is invalid (e.g., invalid user IDs, negative amounts, invalid addresses)
9252    /// - Insufficient asset balance for the requested distribution
9253    /// - The HTTP request fails
9254    /// - The server returns an error status
9255    /// - The response cannot be parsed
9256    ///
9257    /// # Examples
9258    /// ```no_run
9259    /// # use amp_rs::{ApiClient, model::AssetDistributionAssignment, AmpError};
9260    /// # #[tokio::main]
9261    /// # async fn main() -> Result<(), AmpError> {
9262    /// let client = ApiClient::new().await.map_err(AmpError::from)?;
9263    ///
9264    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
9265    /// let assignments = vec![
9266    ///     AssetDistributionAssignment {
9267    ///         user_id: "user123".to_string(),
9268    ///         address: "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
9269    ///         amount: 100.0,
9270    ///     },
9271    ///     AssetDistributionAssignment {
9272    ///         user_id: "user456".to_string(),
9273    ///         address: "lq1qq3xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
9274    ///         amount: 50.0,
9275    ///     },
9276    /// ];
9277    ///
9278    /// let distribution_response = client.create_distribution(asset_uuid, assignments).await?;
9279    /// println!("Created distribution: {}", distribution_response.distribution_uuid);
9280    /// println!("Asset ID: {}", distribution_response.asset_id);
9281    /// # Ok(())
9282    /// # }
9283    /// ```
9284    ///
9285    /// # Related Methods
9286    /// - [`get_asset_assignments`](Self::get_asset_assignments) - List assignments for an asset
9287    /// - [`create_asset_assignments`](Self::create_asset_assignments) - Create new assignments
9288    #[allow(clippy::cognitive_complexity, clippy::too_many_lines)]
9289    pub async fn create_distribution(
9290        &self,
9291        asset_uuid: &str,
9292        assignments: Vec<crate::model::AssetDistributionAssignment>,
9293    ) -> Result<crate::model::DistributionResponse, AmpError> {
9294        use crate::model::{CreateDistributionRequest, DistributionAssignmentRequest};
9295
9296        let create_span = tracing::debug_span!(
9297            "create_distribution",
9298            asset_uuid = %asset_uuid,
9299            assignment_count = assignments.len()
9300        );
9301        let _enter = create_span.enter();
9302
9303        tracing::debug!(
9304            "Creating distribution for asset {} with {} assignments",
9305            asset_uuid,
9306            assignments.len()
9307        );
9308
9309        // Validate inputs
9310        if asset_uuid.is_empty() {
9311            tracing::error!("Distribution creation failed: empty asset UUID");
9312            return Err(AmpError::validation("Asset UUID cannot be empty"));
9313        }
9314
9315        if assignments.is_empty() {
9316            tracing::error!("Distribution creation failed: empty assignments");
9317            return Err(AmpError::validation("Assignments cannot be empty"));
9318        }
9319
9320        // Convert AssetDistributionAssignment to DistributionAssignmentRequest
9321        // The API expects user_uuid field, but our input uses user_id
9322        tracing::trace!("Converting {} assignments to API format", assignments.len());
9323        let mut total_amount = 0.0;
9324        let api_assignments: Vec<DistributionAssignmentRequest> = assignments
9325            .into_iter()
9326            .enumerate()
9327            .map(
9328                #[allow(clippy::cognitive_complexity)]
9329                |(index, assignment)| {
9330                    tracing::trace!(
9331                        "Converting assignment {}: user_id={}, address={}, amount={}",
9332                        index,
9333                        assignment.user_id,
9334                        assignment.address,
9335                        assignment.amount
9336                    );
9337
9338                    // Validate assignment data
9339                    if assignment.user_id.is_empty() {
9340                        tracing::error!("Assignment {} has empty user_id", index);
9341                        return Err(AmpError::validation(format!(
9342                            "Assignment {index} has empty user_id"
9343                        )));
9344                    }
9345                    if assignment.address.is_empty() {
9346                        tracing::error!("Assignment {} has empty address", index);
9347                        return Err(AmpError::validation(format!(
9348                            "Assignment {index} has empty address"
9349                        )));
9350                    }
9351                    if assignment.amount <= 0.0 {
9352                        tracing::error!(
9353                            "Assignment {} has non-positive amount: {}",
9354                            index,
9355                            assignment.amount
9356                        );
9357                        return Err(AmpError::validation(format!(
9358                            "Assignment {} has non-positive amount: {}",
9359                            index, assignment.amount
9360                        )));
9361                    }
9362
9363                    total_amount += assignment.amount;
9364
9365                    Ok(DistributionAssignmentRequest {
9366                        user_uuid: assignment.user_id, // Map user_id to user_uuid for API
9367                        amount: assignment.amount,
9368                        address: assignment.address,
9369                    })
9370                },
9371            )
9372            .collect::<Result<Vec<_>, AmpError>>()?;
9373
9374        tracing::debug!(
9375            "Converted {} assignments successfully, total amount: {}",
9376            api_assignments.len(),
9377            total_amount
9378        );
9379
9380        let request = CreateDistributionRequest {
9381            assignments: api_assignments,
9382        };
9383
9384        tracing::debug!("Sending distribution creation request to AMP API");
9385        let api_call_start = std::time::Instant::now();
9386
9387        // Make the API call
9388        let response: crate::model::DistributionResponse = self
9389            .request_json(
9390                Method::GET,
9391                &["assets", asset_uuid, "distributions", "create"],
9392                Some(&request),
9393            )
9394            .await
9395            .map_err(
9396                #[allow(clippy::cognitive_complexity)]
9397                |e| {
9398                    let api_call_duration = api_call_start.elapsed();
9399                    let error_msg =
9400                        format!("Failed to create distribution after {api_call_duration:?}: {e}");
9401                    tracing::error!("{}", error_msg);
9402
9403                    // Check for specific API error patterns
9404                    let error_str = e.to_string();
9405                    if error_str.contains("404") || error_str.contains("not found") {
9406                        tracing::error!(
9407                            "Asset {} not found - verify asset UUID is correct",
9408                            asset_uuid
9409                        );
9410                    } else if error_str.contains("400") || error_str.contains("bad request") {
9411                        tracing::error!("Bad request - check assignment data format and values");
9412                    } else if error_str.contains("401") || error_str.contains("unauthorized") {
9413                        tracing::error!("Unauthorized - check API credentials and token validity");
9414                    } else if error_str.contains("403") || error_str.contains("forbidden") {
9415                        tracing::error!("Forbidden - check permissions for asset distribution");
9416                    } else if error_str.contains("429") || error_str.contains("rate limit") {
9417                        tracing::error!("Rate limited - wait before retrying");
9418                    } else if error_str.contains("500") || error_str.contains("internal server") {
9419                        tracing::error!(
9420                            "Server error - this may be a temporary issue, retry may help"
9421                        );
9422                    }
9423
9424                    AmpError::api(error_msg)
9425                },
9426            )?;
9427
9428        let api_call_duration = api_call_start.elapsed();
9429        tracing::info!(
9430            "Successfully created distribution: {} (took {:?})",
9431            response.distribution_uuid,
9432            api_call_duration
9433        );
9434
9435        // Validate response data
9436        if response.distribution_uuid.is_empty() {
9437            tracing::error!("API returned empty distribution UUID");
9438            return Err(AmpError::api("API returned empty distribution UUID"));
9439        }
9440
9441        if response.asset_id.is_empty() {
9442            tracing::error!("API returned empty asset ID");
9443            return Err(AmpError::api("API returned empty asset ID"));
9444        }
9445
9446        if response.map_address_amount.is_empty() {
9447            tracing::error!("API returned empty address mapping");
9448            return Err(AmpError::api("API returned empty address mapping"));
9449        }
9450
9451        tracing::debug!(
9452            "Distribution response validated - {} addresses mapped, asset_id: {}",
9453            response.map_address_amount.len(),
9454            response.asset_id
9455        );
9456
9457        Ok(response)
9458    }
9459
9460    /// Confirms a distribution with transaction and change data.
9461    ///
9462    /// This method submits the final confirmation for a distribution by providing
9463    /// the transaction details and any change UTXOs to the AMP API. This completes
9464    /// the distribution workflow after the transaction has been broadcast and confirmed
9465    /// on the blockchain.
9466    ///
9467    /// # Arguments
9468    /// * `asset_uuid` - The UUID of the asset being distributed
9469    /// * `distribution_uuid` - The UUID of the distribution to confirm (from `create_distribution` response)
9470    /// * `tx_data` - Transaction data containing details and txid from the blockchain
9471    /// * `change_data` - Vector of change UTXOs from the transaction
9472    ///
9473    /// # Errors
9474    /// Returns an error if:
9475    /// - Authentication fails
9476    /// - The asset UUID or distribution UUID is invalid
9477    /// - The transaction data is invalid or incomplete
9478    /// - The HTTP request fails
9479    /// - The server returns an error status
9480    /// - The response cannot be parsed
9481    ///
9482    /// # Examples
9483    /// ```no_run
9484    /// # use amp_rs::{ApiClient, model::{AmpTxData, Unspent}, AmpError};
9485    /// # #[tokio::main]
9486    /// # async fn main() -> Result<(), AmpError> {
9487    /// # let client = ApiClient::new().await?;
9488    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
9489    /// let distribution_uuid = "dist-550e8400-e29b-41d4-a716-446655440000";
9490    ///
9491    /// // Transaction data for AMP API confirmation
9492    /// let tx_data = AmpTxData {
9493    ///     details: serde_json::json!([{
9494    ///         "account": "",
9495    ///         "address": "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq",
9496    ///         "category": "send",
9497    ///         "amount": -100.0,
9498    ///         "vout": 0,
9499    ///         "fee": -0.001
9500    ///     }]),
9501    ///     txid: "abc123def456...".to_string(),
9502    /// };
9503    ///
9504    /// // Change UTXOs from Elements node listunspent call
9505    /// let change_data = vec![
9506    ///     Unspent {
9507    ///         txid: "abc123def456...".to_string(),
9508    ///         vout: 1,
9509    ///         amount: 25.0,
9510    ///         asset: "asset_id_hex".to_string(),
9511    ///         address: "change_address".to_string(),
9512    ///         spendable: true,
9513    ///         confirmations: Some(2),
9514    ///         scriptpubkey: Some("76a914...88ac".to_string()),
9515    ///         redeemscript: None,
9516    ///         witnessscript: None,
9517    ///         amountblinder: None,
9518    ///         assetblinder: None,
9519    ///     }
9520    /// ];
9521    ///
9522    /// client.confirm_distribution(asset_uuid, distribution_uuid, tx_data, change_data).await?;
9523    /// println!("Distribution confirmed successfully");
9524    /// # Ok(())
9525    /// # }
9526    /// ```
9527    ///
9528    /// # Related Methods
9529    /// - [`create_distribution`](Self::create_distribution) - Create a new distribution
9530    /// - [`get_asset_assignments`](Self::get_asset_assignments) - List assignments for an asset
9531    #[allow(clippy::cognitive_complexity, clippy::too_many_lines)]
9532    pub async fn confirm_distribution(
9533        &self,
9534        asset_uuid: &str,
9535        distribution_uuid: &str,
9536        tx_data: crate::model::AmpTxData,
9537        change_data: Vec<crate::model::Unspent>,
9538    ) -> Result<(), AmpError> {
9539        use crate::model::ConfirmDistributionRequest;
9540
9541        let confirm_span = tracing::debug_span!(
9542            "confirm_distribution",
9543            asset_uuid = %asset_uuid,
9544            distribution_uuid = %distribution_uuid,
9545            txid = %tx_data.txid,
9546            change_count = change_data.len()
9547        );
9548        let _enter = confirm_span.enter();
9549
9550        tracing::debug!(
9551            "Confirming distribution {} for asset {} with txid {} ({} change UTXOs)",
9552            distribution_uuid,
9553            asset_uuid,
9554            tx_data.txid,
9555            change_data.len()
9556        );
9557
9558        // Validate inputs
9559        if asset_uuid.is_empty() {
9560            tracing::error!("Distribution confirmation failed: empty asset UUID");
9561            return Err(AmpError::validation("Asset UUID cannot be empty"));
9562        }
9563
9564        if distribution_uuid.is_empty() {
9565            tracing::error!("Distribution confirmation failed: empty distribution UUID");
9566            return Err(AmpError::validation("Distribution UUID cannot be empty"));
9567        }
9568
9569        if tx_data.txid.is_empty() {
9570            tracing::error!("Distribution confirmation failed: empty transaction ID");
9571            return Err(AmpError::validation("Transaction ID cannot be empty"));
9572        }
9573
9574        // Log transaction details for debugging
9575        tracing::debug!("Transaction details array: {:?}", tx_data.details);
9576
9577        // Log change data details
9578        if change_data.is_empty() {
9579            tracing::debug!("No change UTXOs to include in confirmation");
9580        } else {
9581            let total_change: f64 = change_data.iter().map(|utxo| utxo.amount).sum();
9582            tracing::debug!(
9583                "Change data - {} UTXOs, total amount: {}",
9584                change_data.len(),
9585                total_change
9586            );
9587
9588            for (i, utxo) in change_data.iter().enumerate() {
9589                tracing::trace!(
9590                    "Change UTXO {}: txid={}, vout={}, amount={}, spendable={}",
9591                    i,
9592                    utxo.txid,
9593                    utxo.vout,
9594                    utxo.amount,
9595                    utxo.spendable
9596                );
9597            }
9598        }
9599
9600        let request = ConfirmDistributionRequest {
9601            tx_data: tx_data.clone(),
9602            change_data: change_data.clone(),
9603        };
9604
9605        tracing::debug!("Sending distribution confirmation request to AMP API");
9606        let api_call_start = std::time::Instant::now();
9607
9608        // Make the API call
9609        self.request_empty(
9610            Method::POST,
9611            &["assets", asset_uuid, "distributions", distribution_uuid, "confirm"],
9612            Some(&request),
9613        )
9614        .await
9615        .map_err(#[allow(clippy::cognitive_complexity)] |e| {
9616            let api_call_duration = api_call_start.elapsed();
9617            let error_msg = format!(
9618                "Failed to confirm distribution {} after {:?}: {}. IMPORTANT: Transaction {} was successful on blockchain. Use this txid to manually retry confirmation.",
9619                distribution_uuid, api_call_duration, e, tx_data.txid
9620            );
9621            tracing::error!("{}", error_msg);
9622
9623            // Check for specific API error patterns
9624            let error_str = e.to_string();
9625            if error_str.contains("404") || error_str.contains("not found") {
9626                tracing::error!("Distribution {} not found - verify distribution UUID is correct", distribution_uuid);
9627            } else if error_str.contains("400") || error_str.contains("bad request") {
9628                tracing::error!("Bad request - check transaction data format and change data");
9629            } else if error_str.contains("409") || error_str.contains("conflict") {
9630                tracing::error!("Conflict - distribution may already be confirmed");
9631            } else if error_str.contains("422") || error_str.contains("unprocessable") {
9632                tracing::error!("Unprocessable entity - check transaction confirmations and data validity");
9633            } else if error_str.contains("500") || error_str.contains("internal server") {
9634                tracing::error!("Server error - this may be a temporary issue, retry with txid: {}", tx_data.txid);
9635            }
9636
9637            AmpError::api(error_msg)
9638        })?;
9639
9640        let api_call_duration = api_call_start.elapsed();
9641        tracing::info!(
9642            "Successfully confirmed distribution: {} for asset: {} with txid: {} (took {:?})",
9643            distribution_uuid,
9644            asset_uuid,
9645            tx_data.txid,
9646            api_call_duration
9647        );
9648
9649        Ok(())
9650    }
9651
9652    /// Cancels an in-progress distribution for an asset.
9653    ///
9654    /// This method cancels a distribution that is currently in progress (unconfirmed status).
9655    /// Once a distribution is cancelled, it cannot be confirmed and the assigned amounts
9656    /// become available for new distributions.
9657    ///
9658    /// # Arguments
9659    /// * `asset_uuid` - The UUID of the asset
9660    /// * `distribution_uuid` - The UUID of the distribution to cancel
9661    ///
9662    /// # Returns
9663    /// Returns `Ok(())` if the distribution was successfully cancelled.
9664    ///
9665    /// # Errors
9666    /// Returns an error if:
9667    /// - Authentication fails
9668    /// - The HTTP request fails
9669    /// - The server returns an error status
9670    /// - The distribution is not found
9671    /// - The distribution is already confirmed and cannot be cancelled
9672    ///
9673    /// # Examples
9674    /// ```no_run
9675    /// use amp_rs::ApiClient;
9676    ///
9677    /// #[tokio::main]
9678    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
9679    ///     let client = ApiClient::new().await?;
9680    ///     
9681    ///     client.cancel_distribution(
9682    ///         "asset-uuid-123",
9683    ///         "distribution-uuid-456"
9684    ///     ).await?;
9685    ///     
9686    ///     println!("Distribution cancelled successfully");
9687    ///     Ok(())
9688    /// # }
9689    /// ```
9690    #[allow(clippy::cognitive_complexity)]
9691    pub async fn cancel_distribution(
9692        &self,
9693        asset_uuid: &str,
9694        distribution_uuid: &str,
9695    ) -> Result<(), AmpError> {
9696        let cancel_span = tracing::debug_span!(
9697            "cancel_distribution",
9698            asset_uuid = %asset_uuid,
9699            distribution_uuid = %distribution_uuid
9700        );
9701        let _enter = cancel_span.enter();
9702
9703        tracing::debug!(
9704            "Cancelling distribution {} for asset {}",
9705            distribution_uuid,
9706            asset_uuid
9707        );
9708
9709        // Validate inputs
9710        if asset_uuid.is_empty() {
9711            tracing::error!("Distribution cancellation failed: empty asset UUID");
9712            return Err(AmpError::validation("Asset UUID cannot be empty"));
9713        }
9714
9715        if distribution_uuid.is_empty() {
9716            tracing::error!("Distribution cancellation failed: empty distribution UUID");
9717            return Err(AmpError::validation("Distribution UUID cannot be empty"));
9718        }
9719
9720        let api_call_start = std::time::Instant::now();
9721
9722        self.request_empty(
9723            Method::DELETE,
9724            &[
9725                "assets",
9726                asset_uuid,
9727                "distributions",
9728                distribution_uuid,
9729                "cancel",
9730            ],
9731            None::<&()>,
9732        )
9733        .await
9734        .map_err(|e| {
9735            let api_call_duration = api_call_start.elapsed();
9736            let error_msg = format!(
9737                "Failed to cancel distribution {distribution_uuid} for asset {asset_uuid} after {api_call_duration:?}: {e}"
9738            );
9739            tracing::error!("{}", error_msg);
9740
9741            // Check for specific API error patterns
9742            let error_str = e.to_string();
9743            if error_str.contains("404") || error_str.contains("not found") {
9744                tracing::error!(
9745                    "Distribution {} not found - verify distribution UUID is correct",
9746                    distribution_uuid
9747                );
9748            } else if error_str.contains("400") || error_str.contains("bad request") {
9749                tracing::error!("Bad request - distribution may already be confirmed or invalid");
9750            } else if error_str.contains("409") || error_str.contains("conflict") {
9751                tracing::error!(
9752                    "Conflict - distribution may already be confirmed and cannot be cancelled"
9753                );
9754            } else if error_str.contains("422") || error_str.contains("unprocessable") {
9755                tracing::error!(
9756                    "Unprocessable entity - distribution is in a state that cannot be cancelled"
9757                );
9758            }
9759
9760            AmpError::api(error_msg)
9761        })?;
9762
9763        let api_call_duration = api_call_start.elapsed();
9764        tracing::info!(
9765            "Successfully cancelled distribution: {} for asset: {} (took {:?})",
9766            distribution_uuid,
9767            asset_uuid,
9768            api_call_duration
9769        );
9770
9771        Ok(())
9772    }
9773
9774    /// Gets all distributions for a specific asset.
9775    ///
9776    /// This method retrieves all distributions (both confirmed and unconfirmed) for the specified asset.
9777    /// This is useful for checking if there are any in-progress distributions before deleting an asset.
9778    ///
9779    /// # Arguments
9780    /// * `asset_uuid` - The UUID of the asset to get distributions for
9781    ///
9782    /// # Returns
9783    /// Returns a vector of `Distribution` objects for the asset.
9784    ///
9785    /// # Errors
9786    /// Returns an error if:
9787    /// - Authentication fails
9788    /// - The HTTP request fails
9789    /// - The server returns an error status
9790    /// - The response cannot be parsed
9791    ///
9792    /// # Examples
9793    /// ```no_run
9794    /// use amp_rs::ApiClient;
9795    ///
9796    /// #[tokio::main]
9797    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
9798    ///     let client = ApiClient::new().await?;
9799    ///     
9800    ///     let distributions = client.get_asset_distributions("asset-uuid-123").await?;
9801    ///     
9802    ///     for distribution in distributions {
9803    ///         println!("Distribution: {} - Status: {:?}",
9804    ///                  distribution.distribution_uuid,
9805    ///                  distribution.distribution_status);
9806    ///     }
9807    ///     Ok(())
9808    /// }
9809    /// ```
9810    pub async fn get_asset_distributions(
9811        &self,
9812        asset_uuid: &str,
9813    ) -> Result<Vec<crate::model::Distribution>, Error> {
9814        let distributions_span = tracing::debug_span!(
9815            "get_asset_distributions",
9816            asset_uuid = %asset_uuid
9817        );
9818        let _enter = distributions_span.enter();
9819
9820        tracing::debug!("Getting distributions for asset {}", asset_uuid);
9821
9822        // Validate input
9823        if asset_uuid.is_empty() {
9824            tracing::error!("Get distributions failed: empty asset UUID");
9825            return Err(Error::RequestFailed(
9826                "Asset UUID cannot be empty".to_string(),
9827            ));
9828        }
9829
9830        self.request_json(
9831            Method::GET,
9832            &["assets", asset_uuid, "distributions"],
9833            None::<&()>,
9834        )
9835        .await
9836    }
9837
9838    /// Gets a specific distribution by UUID for an asset.
9839    ///
9840    /// This method retrieves detailed information about a specific distribution,
9841    /// including its status, UUID, and associated transactions.
9842    ///
9843    /// # Arguments
9844    /// * `asset_uuid` - The UUID of the asset
9845    /// * `distribution_uuid` - The UUID of the distribution to retrieve
9846    ///
9847    /// # Returns
9848    /// Returns a `Distribution` struct containing:
9849    /// - `distribution_uuid` - The unique identifier for the distribution
9850    /// - `distribution_status` - Current status of the distribution
9851    /// - `transactions` - List of transactions associated with the distribution
9852    ///
9853    /// # Errors
9854    /// Returns an error if:
9855    /// - Authentication fails
9856    /// - The HTTP request fails
9857    /// - The server returns an error status
9858    /// - The response cannot be parsed as JSON
9859    /// - The asset UUID or distribution UUID is empty
9860    ///
9861    /// # Examples
9862    /// ```no_run
9863    /// # use amp_rs::ApiClient;
9864    /// # #[tokio::main]
9865    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9866    /// let client = ApiClient::new().await?;
9867    ///
9868    /// let distribution = client.get_asset_distribution(
9869    ///     "asset-uuid-123",
9870    ///     "distribution-uuid-456"
9871    /// ).await?;
9872    ///
9873    /// println!("Distribution: {} - Status: {:?}",
9874    ///          distribution.distribution_uuid,
9875    ///          distribution.distribution_status);
9876    /// # Ok(())
9877    /// # }
9878    /// ```
9879    ///
9880    /// # Related Methods
9881    /// - [`get_asset_distributions`](Self::get_asset_distributions) - List all distributions for an asset
9882    /// - [`create_distribution`](Self::create_distribution) - Create a new distribution
9883    /// - [`confirm_distribution`](Self::confirm_distribution) - Confirm a distribution
9884    /// - [`cancel_distribution`](Self::cancel_distribution) - Cancel a distribution
9885    #[allow(clippy::cognitive_complexity)]
9886    pub async fn get_asset_distribution(
9887        &self,
9888        asset_uuid: &str,
9889        distribution_uuid: &str,
9890    ) -> Result<crate::model::Distribution, Error> {
9891        let distribution_span = tracing::debug_span!(
9892            "get_asset_distribution",
9893            asset_uuid = %asset_uuid,
9894            distribution_uuid = %distribution_uuid
9895        );
9896        let _enter = distribution_span.enter();
9897
9898        tracing::debug!(
9899            "Getting distribution {} for asset {}",
9900            distribution_uuid,
9901            asset_uuid
9902        );
9903
9904        // Validate inputs
9905        if asset_uuid.is_empty() {
9906            tracing::error!("Get distribution failed: empty asset UUID");
9907            return Err(Error::RequestFailed(
9908                "Asset UUID cannot be empty".to_string(),
9909            ));
9910        }
9911
9912        if distribution_uuid.is_empty() {
9913            tracing::error!("Get distribution failed: empty distribution UUID");
9914            return Err(Error::RequestFailed(
9915                "Distribution UUID cannot be empty".to_string(),
9916            ));
9917        }
9918
9919        self.request_json(
9920            Method::GET,
9921            &["assets", asset_uuid, "distributions", distribution_uuid],
9922            None::<&()>,
9923        )
9924        .await
9925    }
9926
9927    /// Gets a specific manager by ID.
9928    ///
9929    /// # Arguments
9930    /// * `manager_id` - The ID of the manager to retrieve
9931    ///
9932    /// # Errors
9933    /// Returns an error if:
9934    /// - Authentication fails
9935    /// - The HTTP request fails
9936    /// - The server returns an error status
9937    /// - The response cannot be parsed as JSON
9938    pub async fn get_manager(&self, manager_id: i64) -> Result<crate::model::Manager, Error> {
9939        self.request_json(
9940            Method::GET,
9941            &["managers", &manager_id.to_string()],
9942            None::<&()>,
9943        )
9944        .await
9945    }
9946
9947    /// Removes a manager's permissions to modify a specific asset.
9948    ///
9949    /// This method revokes a manager's access to a specific asset, preventing them from
9950    /// performing asset management operations such as creating assignments, managing ownership,
9951    /// or modifying asset properties. The manager will no longer be able to access this asset
9952    /// through their management interface.
9953    ///
9954    /// # Arguments
9955    /// * `manager_id` - The ID of the manager to remove permissions from
9956    /// * `asset_uuid` - The UUID of the asset to remove permissions for
9957    ///
9958    /// # Returns
9959    /// Returns `Ok(())` on successful permission removal.
9960    ///
9961    /// # Errors
9962    /// Returns an error if:
9963    /// - Authentication fails or insufficient permissions
9964    /// - The manager ID is invalid or does not exist
9965    /// - The asset UUID is invalid or does not exist
9966    /// - The manager does not currently have permissions for this asset
9967    /// - The HTTP request fails
9968    /// - The server returns an error status
9969    ///
9970    /// # Examples
9971    /// ```no_run
9972    /// # use amp_rs::ApiClient;
9973    /// # #[tokio::main]
9974    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9975    /// let client = ApiClient::new().await?;
9976    ///
9977    /// let manager_id = 123;
9978    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
9979    ///
9980    /// client.manager_remove_asset(manager_id, asset_uuid).await?;
9981    /// println!("Removed asset {} from manager {}", asset_uuid, manager_id);
9982    /// # Ok(())
9983    /// # }
9984    /// ```
9985    ///
9986    /// # Related Methods
9987    /// - [`add_asset_to_manager`](Self::add_asset_to_manager) - Grant manager permissions for an asset
9988    /// - [`get_manager`](Self::get_manager) - Get manager information including current assets
9989    /// - [`revoke_manager`](Self::revoke_manager) - Remove all asset permissions from manager
9990    /// - [`lock_manager`](Self::lock_manager) - Lock manager account
9991    pub async fn manager_remove_asset(
9992        &self,
9993        manager_id: i64,
9994        asset_uuid: &str,
9995    ) -> Result<(), Error> {
9996        self.request_empty(
9997            Method::POST,
9998            &[
9999                "managers",
10000                &manager_id.to_string(),
10001                "assets",
10002                asset_uuid,
10003                "remove",
10004            ],
10005            None::<&()>,
10006        )
10007        .await
10008    }
10009
10010    /// Revokes all asset permissions for a manager.
10011    ///
10012    /// This method first retrieves the manager's current asset permissions,
10013    /// then removes the manager's access to each asset they currently have access to.
10014    ///
10015    /// # Arguments
10016    /// * `manager_id` - The ID of the manager to revoke permissions for
10017    ///
10018    /// # Errors
10019    /// Returns an error if:
10020    /// - Authentication fails
10021    /// - The HTTP request fails
10022    /// - The server returns an error status
10023    /// - Any individual asset removal fails
10024    pub async fn revoke_manager(&self, manager_id: i64) -> Result<(), Error> {
10025        // First, get the manager to see which assets they have access to
10026        let manager = self.get_manager(manager_id).await?;
10027
10028        // Remove the manager's access to each asset
10029        for asset_uuid in &manager.assets {
10030            self.manager_remove_asset(manager_id, asset_uuid).await?;
10031        }
10032
10033        Ok(())
10034    }
10035
10036    /// Gets the current manager information as raw JSON.
10037    ///
10038    /// This method calls the `/managers/me` endpoint to retrieve information
10039    /// about the currently authenticated manager.
10040    ///
10041    /// # Errors
10042    /// Returns an error if:
10043    /// - Authentication fails
10044    /// - The HTTP request fails
10045    /// - The server returns an error status
10046    /// - The response cannot be parsed as JSON
10047    pub async fn get_current_manager_raw(&self) -> Result<serde_json::Value, Error> {
10048        self.request_json(Method::GET, &["managers", "me"], None::<&()>)
10049            .await
10050    }
10051
10052    /// Locks a manager account to prevent further operations.
10053    ///
10054    /// This method sends a PUT request to lock the specified manager, preventing any further
10055    /// operations on that manager account. This is typically used for security purposes or
10056    /// when a manager needs to be temporarily disabled.
10057    ///
10058    /// # Arguments
10059    /// * `manager_id` - The ID of the manager to lock
10060    ///
10061    /// # Returns
10062    /// Returns `Ok(())` if the manager was successfully locked.
10063    ///
10064    /// # Errors
10065    /// Returns an error if:
10066    /// - Authentication fails
10067    /// - The HTTP request fails
10068    /// - The server returns an error status
10069    /// - The manager ID is invalid or does not exist
10070    /// - The manager is already locked
10071    ///
10072    /// # Example
10073    /// ```no_run
10074    /// # use amp_rs::ApiClient;
10075    /// # #[tokio::main]
10076    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
10077    /// let client = ApiClient::new().await?;
10078    ///
10079    /// // Lock manager with ID 123
10080    /// client.lock_manager(123).await?;
10081    /// println!("Manager 123 has been locked successfully");
10082    /// # Ok(())
10083    /// # }
10084    /// ```
10085    pub async fn lock_manager(&self, manager_id: i64) -> Result<(), Error> {
10086        self.request_empty(
10087            Method::PUT,
10088            &["managers", &manager_id.to_string(), "lock"],
10089            None::<&()>,
10090        )
10091        .await
10092    }
10093
10094    /// Unlocks a manager account.
10095    ///
10096    /// # Arguments
10097    /// * `manager_id` - The ID of the manager to unlock
10098    ///
10099    /// # Errors
10100    /// Returns an error if:
10101    /// - Authentication fails
10102    /// - The HTTP request fails
10103    /// - The server returns an error status
10104    pub async fn unlock_manager(&self, manager_id: i64) -> Result<(), Error> {
10105        self.request_empty(
10106            Method::PUT,
10107            &["managers", &manager_id.to_string(), "unlock"],
10108            None::<&()>,
10109        )
10110        .await
10111    }
10112
10113    /// Authorizes a manager to manage a specific asset.
10114    ///
10115    /// This method sends a PUT request to authorize the specified manager to manage the given asset.
10116    /// Once authorized, the manager will have permissions to perform operations on the asset such as
10117    /// creating assignments, managing ownership, and other asset-related operations.
10118    ///
10119    /// # Arguments
10120    /// * `manager_id` - The ID of the manager to authorize
10121    /// * `asset_uuid` - The UUID of the asset to add to the manager's authorized assets
10122    ///
10123    /// # Returns
10124    /// Returns `Ok(())` if the manager was successfully authorized for the asset.
10125    ///
10126    /// # Errors
10127    /// Returns an error if:
10128    /// - Authentication fails or insufficient permissions
10129    /// - The HTTP request fails
10130    /// - The server returns an error status
10131    /// - The manager ID is invalid or does not exist
10132    /// - The asset UUID is invalid or does not exist
10133    /// - The manager is already authorized for this asset
10134    /// - The manager is locked and cannot be modified
10135    ///
10136    /// # Examples
10137    /// ```no_run
10138    /// # use amp_rs::ApiClient;
10139    /// # #[tokio::main]
10140    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
10141    /// let client = ApiClient::new().await?;
10142    ///
10143    /// // Authorize manager 123 to manage asset with UUID "550e8400-e29b-41d4-a716-446655440000"
10144    /// let manager_id = 123;
10145    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
10146    ///
10147    /// client.add_asset_to_manager(manager_id, asset_uuid).await?;
10148    /// println!("Manager {} is now authorized to manage asset {}", manager_id, asset_uuid);
10149    /// # Ok(())
10150    /// # }
10151    /// ```
10152    ///
10153    /// # Related Methods
10154    /// - [`manager_remove_asset`](Self::manager_remove_asset) - Remove manager permissions for an asset
10155    /// - [`get_manager`](Self::get_manager) - Get manager information including current assets
10156    /// - [`get_manager_permissions`](Self::get_manager_permissions) - Get manager's current permissions
10157    /// - [`lock_manager`](Self::lock_manager) - Lock manager account
10158    pub async fn add_asset_to_manager(
10159        &self,
10160        manager_id: i64,
10161        asset_uuid: &str,
10162    ) -> Result<(), Error> {
10163        self.request_empty(
10164            Method::PUT,
10165            &[
10166                "managers",
10167                &manager_id.to_string(),
10168                "assets",
10169                asset_uuid,
10170                "add",
10171            ],
10172            None::<&()>,
10173        )
10174        .await
10175    }
10176
10177    /// Deletes a specific asset assignment.
10178    ///
10179    /// # Arguments
10180    /// * `asset_uuid` - The UUID of the asset
10181    /// * `assignment_id` - The ID of the assignment to delete
10182    ///
10183    /// # Errors
10184    /// Returns an error if:
10185    /// - Authentication fails
10186    /// - The HTTP request fails
10187    /// - The server returns an error status
10188    ///   Removes an asset assignment.
10189    ///
10190    /// This method permanently deletes an asset assignment, returning the allocated assets
10191    /// back to the available pool. This operation cannot be undone. If the assignment has
10192    /// already been distributed, this operation may fail.
10193    ///
10194    /// # Arguments
10195    /// * `asset_uuid` - The UUID of the asset containing the assignment
10196    /// * `assignment_id` - The ID of the assignment to delete
10197    ///
10198    /// # Returns
10199    /// Returns `Ok(())` on successful deletion.
10200    ///
10201    /// # Errors
10202    /// Returns an error if:
10203    /// - Authentication fails or insufficient permissions
10204    /// - The asset UUID is invalid or does not exist
10205    /// - The assignment ID is invalid or does not exist
10206    /// - The assignment has already been distributed and cannot be deleted
10207    /// - The assignment is locked and cannot be modified
10208    /// - The HTTP request fails
10209    /// - The server returns an error status
10210    ///
10211    /// # Examples
10212    /// ```no_run
10213    /// # use amp_rs::ApiClient;
10214    /// # #[tokio::main]
10215    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
10216    /// let client = ApiClient::new().await?;
10217    ///
10218    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
10219    /// let assignment_id = "123";
10220    ///
10221    /// client.delete_asset_assignment(asset_uuid, assignment_id).await?;
10222    /// println!("Successfully deleted assignment {}", assignment_id);
10223    /// # Ok(())
10224    /// # }
10225    /// ```
10226    ///
10227    /// # Related Methods
10228    /// - [`get_asset_assignment`](Self::get_asset_assignment) - Get assignment details before deletion
10229    /// - [`create_asset_assignments`](Self::create_asset_assignments) - Create new assignments
10230    /// - [`edit_asset_assignment`](Self::edit_asset_assignment) - Update assignment instead of deleting
10231    /// - [`lock_asset_assignment`](Self::lock_asset_assignment) - Lock assignment to prevent changes
10232    pub async fn delete_asset_assignment(
10233        &self,
10234        asset_uuid: &str,
10235        assignment_id: &str,
10236    ) -> Result<(), Error> {
10237        self.request_empty(
10238            Method::DELETE,
10239            &["assets", asset_uuid, "assignments", assignment_id, "delete"],
10240            None::<&()>,
10241        )
10242        .await
10243    }
10244
10245    /// Locks a specific asset assignment.
10246    ///
10247    /// # Arguments
10248    /// * `asset_uuid` - The UUID of the asset
10249    /// * `assignment_id` - The ID of the assignment to lock
10250    ///
10251    /// # Errors
10252    /// Returns an error if:
10253    /// - Authentication fails
10254    /// - The HTTP request fails
10255    /// - The server returns an error status
10256    pub async fn lock_asset_assignment(
10257        &self,
10258        asset_uuid: &str,
10259        assignment_id: &str,
10260    ) -> Result<Assignment, Error> {
10261        self.request_json(
10262            Method::PUT,
10263            &["assets", asset_uuid, "assignments", assignment_id, "lock"],
10264            None::<&()>,
10265        )
10266        .await
10267    }
10268
10269    /// Unlocks a specific asset assignment.
10270    ///
10271    /// # Arguments
10272    /// * `asset_uuid` - The UUID of the asset
10273    /// * `assignment_id` - The ID of the assignment to unlock
10274    ///
10275    /// # Errors
10276    /// Returns an error if:
10277    /// - Authentication fails
10278    /// - The HTTP request fails
10279    /// - The server returns an error status
10280    pub async fn unlock_asset_assignment(
10281        &self,
10282        asset_uuid: &str,
10283        assignment_id: &str,
10284    ) -> Result<Assignment, Error> {
10285        self.request_json(
10286            Method::PUT,
10287            &["assets", asset_uuid, "assignments", assignment_id, "unlock"],
10288            None::<&()>,
10289        )
10290        .await
10291    }
10292
10293    /// Adds categories to a registered user.
10294    ///
10295    /// # Arguments
10296    /// * `registered_user_id` - The ID of the registered user
10297    /// * `categories` - A slice of category IDs to add to the user
10298    ///
10299    /// # Errors
10300    /// Returns an error if:
10301    /// - Authentication fails
10302    /// - The HTTP request fails
10303    /// - The server returns an error status
10304    /// - The registered user ID is invalid
10305    /// - Any category ID is invalid
10306    pub async fn add_categories_to_registered_user(
10307        &self,
10308        registered_user_id: i64,
10309        categories: &[i64],
10310    ) -> Result<(), Error> {
10311        let request_body = CategoriesRequest {
10312            categories: categories.to_vec(),
10313        };
10314
10315        self.request_empty(
10316            Method::PUT,
10317            &[
10318                "registered_users",
10319                &registered_user_id.to_string(),
10320                "categories",
10321                "add",
10322            ],
10323            Some(request_body),
10324        )
10325        .await
10326    }
10327
10328    /// Removes categories from a registered user
10329    ///
10330    /// # Arguments
10331    /// * `registered_user_id` - The ID of the registered user
10332    /// * `categories` - A slice of category IDs to remove from the user
10333    ///
10334    /// # Returns
10335    /// Returns `Ok(())` if the categories are successfully removed, or an error if:
10336    /// - Authentication fails
10337    /// - The HTTP request fails
10338    /// - The server returns an error status
10339    /// - The registered user ID is invalid
10340    /// - Any category ID is not associated with the user
10341    pub async fn remove_categories_from_registered_user(
10342        &self,
10343        registered_user_id: i64,
10344        categories: &[i64],
10345    ) -> Result<(), Error> {
10346        let request_body = CategoriesRequest {
10347            categories: categories.to_vec(),
10348        };
10349
10350        self.request_empty(
10351            Method::PUT,
10352            &[
10353                "registered_users",
10354                &registered_user_id.to_string(),
10355                "categories",
10356                "delete",
10357            ],
10358            Some(request_body),
10359        )
10360        .await
10361    }
10362
10363    /// Distributes assets to multiple users through a comprehensive workflow
10364    ///
10365    /// This method orchestrates the complete asset distribution process:
10366    /// 1. Validates input parameters (asset UUID format, assignments structure)
10367    /// 2. Verifies `ElementsRpc` connection and signer interface availability
10368    /// 3. Authenticates with the AMP API using the client's token
10369    /// 4. Creates a distribution request via the AMP API
10370    /// 5. Constructs and signs the blockchain transaction using the provided signer
10371    /// 6. Broadcasts the transaction to the Elements network
10372    /// 7. Waits for blockchain confirmations (2 confirmations minimum)
10373    /// 8. Confirms the distribution with the AMP API
10374    ///
10375    /// # Arguments
10376    /// * `asset_uuid` - The UUID of the asset to distribute (must be valid UUID format)
10377    /// * `assignments` - Vector of assignments specifying `user_id`, address, and amount
10378    /// * `node_rpc` - `ElementsRpc` client for blockchain operations
10379    /// * `signer` - Signer implementation for transaction signing
10380    ///
10381    /// # Returns
10382    /// Returns `Ok(())` if the distribution completes successfully, or an `AmpError` if:
10383    /// - Input validation fails (invalid UUID format, empty assignments, etc.)
10384    /// - `ElementsRpc` connection cannot be established
10385    /// - Signer interface is not available
10386    /// - Authentication with AMP API fails
10387    /// - Distribution creation fails
10388    /// - Transaction construction or signing fails
10389    /// - Blockchain broadcasting fails
10390    /// - Confirmation timeout occurs
10391    /// - Distribution confirmation with AMP API fails
10392    ///
10393    /// # Examples
10394    /// ```no_run
10395    /// # use amp_rs::{ApiClient, ElementsRpc, AmpError};
10396    /// # use amp_rs::model::AssetDistributionAssignment;
10397    /// # use amp_rs::signer::{Signer, LwkSoftwareSigner};
10398    /// # #[tokio::main]
10399    /// # async fn main() -> Result<(), AmpError> {
10400    /// let client = ApiClient::new().await?;
10401    /// let elements_rpc = ElementsRpc::from_env()?;
10402    /// let (_, signer) = LwkSoftwareSigner::generate_new()?;
10403    ///
10404    /// let assignments = vec![
10405    ///     AssetDistributionAssignment {
10406    ///         user_id: "user123".to_string(),
10407    ///         address: "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
10408    ///         amount: 100.0,
10409    ///     },
10410    /// ];
10411    ///
10412    /// client.distribute_asset(
10413    ///     "550e8400-e29b-41d4-a716-446655440000",
10414    ///     assignments,
10415    ///     &elements_rpc,
10416    ///     "wallet_name",
10417    ///     &signer
10418    /// ).await?;
10419    /// # Ok(())
10420    /// # }
10421    /// ```
10422    ///
10423    /// # Requirements
10424    /// This method implements requirements:
10425    /// - 1.1: Single method for complete distribution workflow
10426    /// - 2.2: Assignment details validation
10427    /// - 2.4: Input validation for all parameters
10428    /// - 5.1: Comprehensive error handling with context
10429    #[allow(clippy::cognitive_complexity, clippy::too_many_lines)]
10430    pub async fn distribute_asset(
10431        &self,
10432        asset_uuid: &str,
10433        assignments: Vec<AssetDistributionAssignment>,
10434        node_rpc: &ElementsRpc,
10435        wallet_name: &str,
10436        signer: &dyn Signer,
10437    ) -> Result<(), AmpError> {
10438        let distribution_span = tracing::info_span!(
10439            "distribute_asset",
10440            asset_uuid = %asset_uuid,
10441            assignment_count = assignments.len()
10442        );
10443        let _enter = distribution_span.enter();
10444
10445        tracing::info!(
10446            "Starting asset distribution workflow for asset: {} with {} assignments",
10447            asset_uuid,
10448            assignments.len()
10449        );
10450
10451        // Step 1: Input validation - asset_uuid format
10452        tracing::debug!("Step 1: Validating asset UUID format");
10453        Self::validate_asset_uuid(asset_uuid).map_err(|e| {
10454            let error = AmpError::validation(format!("Invalid asset UUID: {e}"));
10455            tracing::error!("Asset UUID validation failed: {}", e);
10456            error.with_context("Step 1: Asset UUID validation")
10457        })?;
10458        tracing::debug!("Asset UUID validation passed");
10459
10460        // Step 2: Input validation - assignments data structure
10461        tracing::debug!("Step 2: Validating {} assignments", assignments.len());
10462        Self::validate_assignments(&assignments).map_err(|e| {
10463            let error = AmpError::validation(format!("Invalid assignments: {e}"));
10464            tracing::error!("Assignments validation failed: {}", e);
10465            error.with_context("Step 2: Assignments validation")
10466        })?;
10467        tracing::debug!("Assignments validation passed");
10468
10469        // Step 3: Check ElementsRpc connection availability
10470        tracing::debug!("Step 3: Validating Elements RPC connection");
10471        self.validate_elements_rpc_connection(node_rpc)
10472            .await
10473            .map_err(|e| {
10474                let error = AmpError::rpc(format!("ElementsRpc connection validation failed: {e}"));
10475                tracing::error!("Elements RPC connection validation failed: {}", e);
10476                error.with_context("Step 3: Elements RPC connection validation")
10477            })?;
10478        tracing::debug!("Elements RPC connection validation passed");
10479
10480        // Step 4: Check signer interface availability
10481        tracing::debug!("Step 4: Validating signer interface");
10482        self.validate_signer_interface(signer).await.map_err(|e| {
10483            let error = AmpError::validation(format!("Signer interface validation failed: {e}"));
10484            tracing::error!("Signer interface validation failed: {}", e);
10485            error.with_context("Step 4: Signer interface validation")
10486        })?;
10487        tracing::debug!("Signer interface validation passed");
10488
10489        tracing::info!("✓ All input validations completed successfully");
10490
10491        // Step 5: Authenticate with AMP API using existing TokenManager
10492        tracing::debug!("Step 5: Authenticating with AMP API");
10493        let _token = self.token_strategy.get_token().await.map_err(|e| {
10494            tracing::error!("AMP API authentication failed: {}", e);
10495            let amp_error = AmpError::Existing(e);
10496            if amp_error.is_retryable() {
10497                if let Some(instructions) = amp_error.retry_instructions() {
10498                    tracing::warn!("Retry instructions: {}", instructions);
10499                }
10500            }
10501            amp_error.with_context("Step 5: AMP API authentication")
10502        })?;
10503        tracing::info!("✓ Successfully authenticated with AMP API");
10504
10505        // Step 6: Create distribution request and parse response data
10506        tracing::debug!(
10507            "Step 6: Creating distribution request with {} assignments",
10508            assignments.len()
10509        );
10510        let distribution_response = self
10511            .create_distribution(asset_uuid, assignments)
10512            .await
10513            .map_err(|e| {
10514                tracing::error!("Distribution creation failed: {}", e);
10515                if e.is_retryable() {
10516                    if let Some(instructions) = e.retry_instructions() {
10517                        tracing::warn!("Retry instructions: {}", instructions);
10518                    }
10519                }
10520                e.with_context("Step 6: Distribution creation")
10521            })?;
10522
10523        tracing::info!(
10524            "✓ Distribution created successfully: {} with asset_id: {}",
10525            distribution_response.distribution_uuid,
10526            distribution_response.asset_id
10527        );
10528
10529        // Step 7: Verify Elements node status and execute transaction workflow
10530        tracing::debug!("Step 7: Verifying Elements node status");
10531        let (network_info, blockchain_info) = node_rpc.get_node_status().await.map_err(|e| {
10532            tracing::error!("Elements node status verification failed: {}", e);
10533            if e.is_retryable() {
10534                if let Some(instructions) = e.retry_instructions() {
10535                    tracing::warn!("Retry instructions: {}", instructions);
10536                }
10537            }
10538            e.with_context("Step 7: Elements node status verification")
10539        })?;
10540
10541        tracing::info!(
10542            "✓ Elements node verified - chain: {}, blocks: {}, connections: {}",
10543            blockchain_info.chain,
10544            blockchain_info.blocks,
10545            network_info.connections
10546        );
10547
10548        // Step 8: Send distribution transaction using Elements' sendmany
10549        tracing::debug!("Step 8: Sending distribution transaction using Elements sendmany");
10550
10551        // Create asset amounts map for sendmany (all outputs use the same asset)
10552        let mut asset_amounts = std::collections::HashMap::new();
10553        for address in distribution_response.map_address_amount.keys() {
10554            asset_amounts.insert(address.clone(), distribution_response.asset_id.clone());
10555        }
10556
10557        tracing::info!(
10558            "Using sendmany for {} outputs with asset {}",
10559            distribution_response.map_address_amount.len(),
10560            distribution_response.asset_id
10561        );
10562
10563        // Use Elements' sendmany which properly handles confidential transactions
10564        let txid = node_rpc
10565            .sendmany(
10566                wallet_name,
10567                distribution_response.map_address_amount.clone(),
10568                asset_amounts,
10569                Some(0), // min_conf: 0 to include unconfirmed UTXOs (matches Python implementation)
10570                Some("AMP asset distribution"), // comment
10571                None,    // subtract_fee_from: let Elements handle fees automatically
10572                Some(false), // replaceable: false for final transactions
10573                Some(1), // conf_target: 1 block for faster confirmation
10574                Some("UNSET"), // estimate_mode: let Elements choose
10575            )
10576            .await
10577            .map_err(|e| {
10578                tracing::error!("Sendmany transaction failed: {}", e);
10579                if e.is_retryable() {
10580                    if let Some(instructions) = e.retry_instructions() {
10581                        tracing::warn!("Retry instructions: {}", instructions);
10582                    }
10583                }
10584                e.with_context("Step 8: Sendmany transaction")
10585            })?;
10586
10587        tracing::info!("✓ Transaction sent successfully with ID: {}", txid);
10588
10589        // Step 9: Wait for confirmations
10590        tracing::debug!("Step 9: Waiting for blockchain confirmations (minimum 2 confirmations, 10-minute timeout)");
10591        let confirmation_start = std::time::Instant::now();
10592        let tx_detail = node_rpc.wait_for_confirmations(&txid, Some(2), Some(10)).await
10593            .map_err(|e| {
10594                let elapsed = confirmation_start.elapsed();
10595                tracing::error!(
10596                    "Confirmation waiting failed after {:?}: {}",
10597                    elapsed,
10598                    e
10599                );
10600
10601                if let AmpError::Timeout(_) = &e {
10602                    tracing::warn!(
10603                        "Confirmation timeout - transaction {} may still be pending. \
10604                        Use this txid to manually confirm the distribution if it gets confirmed later.",
10605                        txid
10606                    );
10607                    let timeout_error = AmpError::timeout(format!(
10608                        "Confirmation timeout for txid: {txid}. Use this txid to manually confirm the distribution."
10609                    ));
10610                    timeout_error.with_context("Step 9: Confirmation waiting")
10611                } else {
10612                    if e.is_retryable() {
10613                        if let Some(instructions) = e.retry_instructions() {
10614                            tracing::warn!("Retry instructions: {}", instructions);
10615                        }
10616                    }
10617                    e.with_context(format!("Step 9: Confirmation waiting for txid: {txid}"))
10618                }
10619            })?;
10620
10621        let confirmation_duration = confirmation_start.elapsed();
10622        tracing::info!(
10623            "✓ Transaction confirmed with {} confirmations at block height: {:?} (took {:?})",
10624            tx_detail.confirmations,
10625            tx_detail.blockheight,
10626            confirmation_duration
10627        );
10628
10629        // Step 10: Collect change data for confirmation
10630        tracing::debug!("Step 10: Collecting change data for distribution confirmation");
10631        let change_data = node_rpc
10632            .collect_change_data(
10633                &distribution_response.asset_id,
10634                &txid,
10635                node_rpc,
10636                wallet_name,
10637            )
10638            .await
10639            .map_err(|e| {
10640                tracing::error!("Change data collection failed: {}", e);
10641                if e.is_retryable() {
10642                    if let Some(instructions) = e.retry_instructions() {
10643                        tracing::warn!("Retry instructions: {}", instructions);
10644                    }
10645                }
10646                e.with_context("Step 10: Change data collection")
10647            })?;
10648
10649        tracing::info!("✓ Collected {} change UTXOs", change_data.len());
10650        if !change_data.is_empty() {
10651            tracing::debug!("Change UTXOs: {:?}", change_data);
10652        }
10653
10654        // Step 11: Submit final confirmation to AMP API
10655        tracing::debug!("Step 11: Submitting final confirmation to AMP API");
10656
10657        // Extract the details field from the transaction (matching Python implementation)
10658        // Python: details = rpc.call('gettransaction', txid).get('details')
10659        let transaction_details = tx_detail.details.unwrap_or_else(Vec::new);
10660        tracing::debug!(
10661            "Transaction details for confirmation: {:?}",
10662            transaction_details
10663        );
10664
10665        let amp_tx_data = crate::model::AmpTxData {
10666            details: serde_json::Value::Array(transaction_details),
10667            txid: txid.clone(),
10668        };
10669
10670        // Log the exact payload being sent to AMP for debugging
10671        tracing::info!("Sending confirmation payload to AMP:");
10672        tracing::info!("  tx_data.txid: {}", amp_tx_data.txid);
10673        tracing::info!("  tx_data.details: {:?}", amp_tx_data.details);
10674        tracing::info!("  change_data: {} UTXOs", change_data.len());
10675
10676        let confirmation_request = crate::model::ConfirmDistributionRequest {
10677            tx_data: amp_tx_data.clone(),
10678            change_data: change_data.clone(),
10679        };
10680
10681        if let Ok(payload_json) = serde_json::to_string_pretty(&confirmation_request) {
10682            tracing::debug!("Full confirmation payload: {}", payload_json);
10683        }
10684
10685        self.confirm_distribution(
10686            asset_uuid,
10687            &distribution_response.distribution_uuid,
10688            amp_tx_data,
10689            change_data,
10690        )
10691        .await
10692        .map_err(|e| {
10693            tracing::error!("Distribution confirmation failed: {}", e);
10694
10695            // For confirmation failures, always provide retry instructions with txid
10696            let confirmation_error = AmpError::api(format!(
10697                "Failed to confirm distribution {}: {}. \
10698                IMPORTANT: Transaction {} was successful on blockchain. \
10699                Use this txid to manually retry confirmation.",
10700                distribution_response.distribution_uuid, e, txid
10701            ));
10702
10703            if e.is_retryable() {
10704                if let Some(instructions) = e.retry_instructions() {
10705                    tracing::warn!("Retry instructions: {}", instructions);
10706                }
10707            }
10708
10709            confirmation_error.with_context("Step 11: Distribution confirmation")
10710        })?;
10711
10712        tracing::info!(
10713            "🎉 Asset distribution completed successfully for asset: {} with transaction: {}",
10714            asset_uuid,
10715            txid
10716        );
10717
10718        Ok(())
10719    }
10720
10721    /// Validates the asset UUID format
10722    ///
10723    /// Ensures the asset UUID follows the standard UUID format (8-4-4-4-12 hexadecimal digits)
10724    ///
10725    /// # Arguments
10726    /// * `asset_uuid` - The asset UUID string to validate
10727    ///
10728    /// # Returns
10729    /// Returns `Ok(())` if valid, or an error describing the validation failure
10730    ///
10731    /// # Errors
10732    /// - Empty or whitespace-only UUID
10733    /// - Invalid UUID format (not matching standard UUID pattern)
10734    /// - UUID contains invalid characters
10735    fn validate_asset_uuid(asset_uuid: &str) -> Result<(), String> {
10736        if asset_uuid.trim().is_empty() {
10737            return Err("Asset UUID cannot be empty".to_string());
10738        }
10739
10740        // Basic UUID format validation (8-4-4-4-12 pattern)
10741        // Expected format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
10742        let parts: Vec<&str> = asset_uuid.split('-').collect();
10743        if parts.len() != 5 {
10744            return Err(format!(
10745                "Asset UUID '{asset_uuid}' does not have 5 parts separated by hyphens"
10746            ));
10747        }
10748
10749        // Check each part has the correct length and contains only hex characters
10750        let expected_lengths = [8, 4, 4, 4, 12];
10751        for (i, (part, &expected_len)) in parts.iter().zip(expected_lengths.iter()).enumerate() {
10752            if part.len() != expected_len {
10753                return Err(format!(
10754                    "Asset UUID part {} has length {} but expected {}",
10755                    i + 1,
10756                    part.len(),
10757                    expected_len
10758                ));
10759            }
10760
10761            // Check if all characters are valid hexadecimal
10762            if !part.chars().all(|c| c.is_ascii_hexdigit()) {
10763                return Err(format!(
10764                    "Asset UUID part {} contains non-hexadecimal characters: '{}'",
10765                    i + 1,
10766                    part
10767                ));
10768            }
10769        }
10770
10771        tracing::debug!("Asset UUID validation passed: {}", asset_uuid);
10772        Ok(())
10773    }
10774
10775    /// Validates the assignments data structure
10776    ///
10777    /// Ensures assignments vector is not empty and each assignment has valid data
10778    ///
10779    /// # Arguments
10780    /// * `assignments` - Vector of assignments to validate
10781    ///
10782    /// # Returns
10783    /// Returns `Ok(())` if valid, or an error describing the validation failure
10784    ///
10785    /// # Errors
10786    /// - Empty assignments vector
10787    /// - Assignment with empty `user_id`
10788    /// - Assignment with empty address
10789    /// - Assignment with non-positive amount
10790    /// - Assignment with invalid address format
10791    #[allow(clippy::cognitive_complexity)]
10792    fn validate_assignments(assignments: &[AssetDistributionAssignment]) -> Result<(), String> {
10793        tracing::debug!("Validating {} assignments", assignments.len());
10794
10795        if assignments.is_empty() {
10796            tracing::error!("Assignments validation failed: empty assignments vector");
10797            return Err("Assignments vector cannot be empty".to_string());
10798        }
10799
10800        let mut total_amount = 0.0;
10801        let mut unique_addresses = std::collections::HashSet::new();
10802        let mut unique_users = std::collections::HashSet::new();
10803
10804        for (index, assignment) in assignments.iter().enumerate() {
10805            tracing::trace!(
10806                "Validating assignment {}: user_id={}, address={}, amount={}",
10807                index,
10808                assignment.user_id,
10809                assignment.address,
10810                assignment.amount
10811            );
10812
10813            // Validate user_id
10814            if assignment.user_id.trim().is_empty() {
10815                tracing::error!("Assignment {} validation failed: empty user_id", index);
10816                return Err(format!("Assignment {index} has empty user_id"));
10817            }
10818
10819            // Validate address
10820            if assignment.address.trim().is_empty() {
10821                tracing::error!("Assignment {} validation failed: empty address", index);
10822                return Err(format!("Assignment {index} has empty address"));
10823            }
10824
10825            // Basic address format validation (should start with appropriate prefix for Liquid)
10826            if !assignment.address.starts_with("lq")
10827                && !assignment.address.starts_with("vj")
10828                && !assignment.address.starts_with("VJ")
10829                && !assignment.address.starts_with("VT")
10830            {
10831                tracing::error!(
10832                    "Assignment {} validation failed: invalid address format '{}' (should start with 'lq', 'vj', 'VJ', or 'VT')",
10833                    index, assignment.address
10834                );
10835                return Err(format!(
10836                    "Assignment {} has invalid address format: '{}' (should start with 'lq', 'vj', 'VJ', or 'VT')",
10837                    index, assignment.address
10838                ));
10839            }
10840
10841            // Validate amount
10842            if assignment.amount <= 0.0 {
10843                tracing::error!(
10844                    "Assignment {} validation failed: non-positive amount {}",
10845                    index,
10846                    assignment.amount
10847                );
10848                return Err(format!(
10849                    "Assignment {} has non-positive amount: {}",
10850                    index, assignment.amount
10851                ));
10852            }
10853
10854            // Check for reasonable amount limits (prevent overflow issues)
10855            if assignment.amount > 21_000_000.0 {
10856                tracing::error!(
10857                    "Assignment {} validation failed: unreasonably large amount {} (max: 21,000,000)",
10858                    index, assignment.amount
10859                );
10860                return Err(format!(
10861                    "Assignment {} has unreasonably large amount: {} (max: 21,000,000)",
10862                    index, assignment.amount
10863                ));
10864            }
10865
10866            // Check for precision issues (more than 8 decimal places)
10867            let amount_str = format!("{:.8}", assignment.amount);
10868            if amount_str.len() > 20 {
10869                // Reasonable length check
10870                tracing::warn!(
10871                    "Assignment {} has high precision amount: {} - may cause precision issues",
10872                    index,
10873                    assignment.amount
10874                );
10875            }
10876
10877            // Track duplicates for warnings
10878            if !unique_addresses.insert(&assignment.address) {
10879                tracing::warn!(
10880                    "Assignment {} uses duplicate address: {} (this may be intentional)",
10881                    index,
10882                    assignment.address
10883                );
10884            }
10885
10886            if !unique_users.insert(&assignment.user_id) {
10887                tracing::warn!(
10888                    "Assignment {} uses duplicate user_id: {} (this may be intentional)",
10889                    index,
10890                    assignment.user_id
10891                );
10892            }
10893
10894            total_amount += assignment.amount;
10895        }
10896
10897        tracing::debug!(
10898            "Assignments validation passed - {} assignments, total amount: {}, unique addresses: {}, unique users: {}",
10899            assignments.len(),
10900            total_amount,
10901            unique_addresses.len(),
10902            unique_users.len()
10903        );
10904
10905        if total_amount > 100_000_000.0 {
10906            tracing::warn!(
10907                "Total distribution amount is very large: {} - ensure this is intentional",
10908                total_amount
10909            );
10910        }
10911
10912        Ok(())
10913    }
10914
10915    /// Validates `ElementsRpc` connection availability
10916    ///
10917    /// Attempts to connect to the Elements node and verify basic functionality
10918    ///
10919    /// # Arguments
10920    /// * `node_rpc` - `ElementsRpc` client to validate
10921    ///
10922    /// # Returns
10923    /// Returns `Ok(())` if connection is valid, or an error describing the failure
10924    ///
10925    /// # Errors
10926    /// - Cannot connect to Elements node
10927    /// - Node is not synchronized
10928    /// - Node version is incompatible
10929    /// - RPC authentication fails
10930    #[allow(clippy::cognitive_complexity)]
10931    async fn validate_elements_rpc_connection(&self, node_rpc: &ElementsRpc) -> Result<(), String> {
10932        tracing::debug!("Validating Elements RPC connection");
10933
10934        // Test basic connectivity by getting network info
10935        tracing::trace!("Testing Elements RPC connectivity with getnetworkinfo");
10936        let network_info = node_rpc.get_network_info().await.map_err(|e| {
10937            tracing::error!("Failed to get network info from Elements node: {}", e);
10938            format!("Failed to get network info: {e}")
10939        })?;
10940
10941        tracing::debug!(
10942            "Network info retrieved - version: {}, connections: {}, network_active: {}",
10943            network_info.version,
10944            network_info.connections,
10945            network_info.networkactive
10946        );
10947
10948        // Check if network is active
10949        if !network_info.networkactive {
10950            tracing::error!("Elements node network is not active");
10951            return Err("Elements node network is not active".to_string());
10952        }
10953
10954        // Verify we have active connections (for non-regtest environments)
10955        if network_info.connections == 0 {
10956            tracing::warn!("Elements node has no peer connections (may be regtest environment)");
10957        } else {
10958            tracing::debug!(
10959                "Elements node has {} peer connections",
10960                network_info.connections
10961            );
10962        }
10963
10964        // Test blockchain info to ensure node is operational
10965        tracing::trace!("Testing Elements RPC with getblockchaininfo");
10966        let blockchain_info = node_rpc.get_blockchain_info().await.map_err(|e| {
10967            tracing::error!("Failed to get blockchain info from Elements node: {}", e);
10968            format!("Failed to get blockchain info: {e}")
10969        })?;
10970
10971        let sync_progress = blockchain_info.verificationprogress.unwrap_or(1.0) * 100.0;
10972        tracing::debug!(
10973            "Blockchain info retrieved - chain: {}, blocks: {}, sync_progress: {:.2}%",
10974            blockchain_info.chain,
10975            blockchain_info.blocks,
10976            sync_progress
10977        );
10978
10979        // Check if node is still in initial block download
10980        if blockchain_info.initialblockdownload.unwrap_or(false) {
10981            tracing::error!(
10982                "Elements node is still in initial block download (sync progress: {:.2}%)",
10983                sync_progress
10984            );
10985            return Err(format!(
10986                "Elements node is still in initial block download (sync progress: {sync_progress:.2}%)"
10987            ));
10988        }
10989
10990        // Check sync progress
10991        if blockchain_info.verificationprogress.unwrap_or(1.0) < 0.99 {
10992            tracing::warn!(
10993                "Elements node may not be fully synced (sync progress: {:.2}%)",
10994                sync_progress
10995            );
10996        }
10997
10998        // Check for warnings
10999        if !network_info.warnings.is_empty() {
11000            tracing::warn!("Elements node network warnings: {}", network_info.warnings);
11001        }
11002
11003        if let Some(warnings) = &blockchain_info.warnings {
11004            if !warnings.is_empty() {
11005                tracing::warn!("Elements node blockchain warnings: {}", warnings);
11006            }
11007        }
11008
11009        tracing::debug!(
11010            "ElementsRpc connection validation passed - chain: {}, blocks: {}, connections: {}, sync: {:.2}%",
11011            blockchain_info.chain,
11012            blockchain_info.blocks,
11013            network_info.connections,
11014            sync_progress
11015        );
11016
11017        Ok(())
11018    }
11019
11020    /// Validates signer interface availability
11021    ///
11022    /// Tests the signer interface with a dummy transaction to ensure it's functional
11023    ///
11024    /// # Arguments
11025    /// * `signer` - Signer implementation to validate
11026    ///
11027    /// # Returns
11028    /// Returns `Ok(())` if signer is functional, or an error describing the failure
11029    ///
11030    /// # Errors
11031    /// - Signer interface is not responsive
11032    /// - Signer fails basic functionality test
11033    #[allow(clippy::cognitive_complexity)]
11034    async fn validate_signer_interface(&self, signer: &dyn Signer) -> Result<(), String> {
11035        tracing::debug!("Validating signer interface");
11036
11037        // Test signer with a minimal dummy transaction hex
11038        // This is a minimal Elements transaction structure that should parse but not be valid for signing
11039        let dummy_tx = "0200000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000";
11040
11041        tracing::trace!("Testing signer interface with dummy transaction");
11042
11043        // Attempt to sign the dummy transaction - we expect this to fail with a specific error
11044        // but the signer should be responsive and not panic
11045        let validation_start = std::time::Instant::now();
11046        match signer.sign_transaction(dummy_tx).await {
11047            Ok(signed_tx) => {
11048                // Unexpected success with dummy transaction - this might indicate an issue
11049                tracing::warn!(
11050                    "Signer unexpectedly succeeded with dummy transaction (returned: {} chars)",
11051                    signed_tx.len()
11052                );
11053                tracing::debug!("Signer validation passed despite unexpected success");
11054            }
11055            Err(SignerError::InvalidTransaction(msg)) => {
11056                // Expected error - signer is working and correctly identified invalid transaction
11057                tracing::debug!(
11058                    "Signer interface validation passed - correctly rejected dummy transaction: {}",
11059                    msg
11060                );
11061            }
11062            Err(SignerError::HexParse(msg)) => {
11063                // Also acceptable - signer is working and correctly identified parsing issue
11064                tracing::debug!(
11065                    "Signer interface validation passed - correctly identified hex parsing issue: {}",
11066                    msg
11067                );
11068            }
11069            Err(SignerError::Lwk(msg)) => {
11070                // LWK-specific errors might be acceptable depending on the message
11071                if msg.contains("invalid") || msg.contains("parse") || msg.contains("decode") {
11072                    tracing::debug!(
11073                        "Signer interface validation passed - LWK correctly identified invalid transaction: {}",
11074                        msg
11075                    );
11076                } else {
11077                    tracing::error!("Signer interface test failed with LWK error: {}", msg);
11078                    return Err(format!(
11079                        "Signer interface test failed with LWK error: {msg}"
11080                    ));
11081                }
11082            }
11083            Err(e) => {
11084                // Other errors might indicate signer interface issues
11085                tracing::error!("Signer interface test failed: {}", e);
11086                return Err(format!("Signer interface test failed: {e}"));
11087            }
11088        }
11089
11090        let validation_duration = validation_start.elapsed();
11091        tracing::debug!(
11092            "Signer interface validation completed in {:?}",
11093            validation_duration
11094        );
11095
11096        // Warn if signer is very slow (might indicate performance issues)
11097        if validation_duration > std::time::Duration::from_secs(5) {
11098            tracing::warn!(
11099                "Signer interface validation took {:?} - this may indicate performance issues",
11100                validation_duration
11101            );
11102        }
11103
11104        Ok(())
11105    }
11106}
11107
11108fn get_amp_api_base_url() -> Result<Url, Error> {
11109    let url_str = env::var("AMP_API_BASE_URL")
11110        .unwrap_or_else(|_| "https://amp-test.blockstream.com/api".to_string());
11111    Url::parse(&url_str).map_err(Error::from)
11112}
11113
11114/// Creates a token strategy based on automatic environment detection
11115///
11116/// This function detects the current environment and creates the appropriate strategy:
11117/// - Mock strategy for mock environments (isolated, no persistence)
11118/// - Live strategy for live environments (full token management)
11119///
11120/// # Arguments
11121/// * `mock_token` - Optional token to use for mock environments
11122///
11123/// # Errors
11124/// Returns an error if strategy creation fails
11125pub async fn create_auto_token_strategy(
11126    mock_token: Option<String>,
11127) -> Result<Box<dyn TokenStrategy>, Error> {
11128    TokenEnvironment::create_auto_strategy(mock_token).await
11129}
11130
11131/// Creates a mock token strategy with the specified token
11132///
11133/// # Arguments
11134/// * `token` - The mock token to use
11135#[must_use]
11136pub fn create_mock_token_strategy(token: String) -> Box<dyn TokenStrategy> {
11137    Box::new(MockTokenStrategy::new(token))
11138}
11139
11140/// Creates a live token strategy with default configuration
11141///
11142/// # Errors
11143/// Returns an error if the `TokenManager` cannot be initialized
11144pub async fn create_live_token_strategy() -> Result<Box<dyn TokenStrategy>, Error> {
11145    let strategy = LiveTokenStrategy::new().await?;
11146    Ok(Box::new(strategy))
11147}
11148
11149/// Creates a token strategy for the specified environment
11150///
11151/// # Arguments
11152/// * `environment` - The target environment
11153/// * `mock_token` - Optional token to use for mock environments
11154///
11155/// # Errors
11156/// Returns an error if strategy creation fails
11157pub async fn create_token_strategy_for_environment(
11158    environment: TokenEnvironment,
11159    mock_token: Option<String>,
11160) -> Result<Box<dyn TokenStrategy>, Error> {
11161    environment.create_strategy(mock_token).await
11162}
11163
11164#[cfg(test)]
11165mod tests {
11166    use super::*;
11167    use crate::signer::LwkSoftwareSigner;
11168    use tokio;
11169
11170    #[tokio::test]
11171    async fn test_mock_token_strategy_basic_functionality() {
11172        let mock_token = "mock_token_12_345".to_string();
11173        let strategy = MockTokenStrategy::new(mock_token.clone());
11174
11175        // Test get_token returns the mock token
11176        let result = strategy.get_token().await;
11177        assert!(result.is_ok());
11178        assert_eq!(result.unwrap(), mock_token);
11179
11180        // Test strategy type identification
11181        assert_eq!(strategy.strategy_type(), "mock");
11182
11183        // Test persistence is disabled
11184        assert!(!strategy.should_persist());
11185
11186        // Test clear_token is a no-op (should not fail)
11187        let clear_result = strategy.clear_token().await;
11188        assert!(clear_result.is_ok());
11189
11190        // Verify token is still available after clear (since it's a no-op for mock)
11191        let token_after_clear = strategy.get_token().await;
11192        assert!(token_after_clear.is_ok());
11193        assert_eq!(token_after_clear.unwrap(), mock_token);
11194    }
11195
11196    #[tokio::test]
11197    async fn test_mock_token_strategy_isolation() {
11198        let token1 = "token_instance_1".to_string();
11199        let token2 = "token_instance_2".to_string();
11200
11201        let strategy1 = MockTokenStrategy::new(token1.clone());
11202        let strategy2 = MockTokenStrategy::new(token2.clone());
11203
11204        // Test that different instances are isolated
11205        let result1 = strategy1.get_token().await.unwrap();
11206        let result2 = strategy2.get_token().await.unwrap();
11207
11208        assert_eq!(result1, token1);
11209        assert_eq!(result2, token2);
11210        assert_ne!(result1, result2);
11211
11212        // Test that operations on one don't affect the other
11213        let _ = strategy1.clear_token().await;
11214        let result2_after_clear = strategy2.get_token().await.unwrap();
11215        assert_eq!(result2_after_clear, token2);
11216    }
11217
11218    #[tokio::test]
11219    async fn test_live_token_strategy_creation() {
11220        // Test creating a live strategy with global instance
11221        let strategy_result = LiveTokenStrategy::new().await;
11222        assert!(strategy_result.is_ok());
11223
11224        let strategy = strategy_result.unwrap();
11225        assert_eq!(strategy.strategy_type(), "live");
11226        assert!(strategy.should_persist());
11227    }
11228
11229    #[tokio::test]
11230    async fn test_live_token_strategy_with_custom_manager() {
11231        // Create a custom token manager for testing
11232        let config = RetryConfig::for_tests();
11233        let base_url = Url::parse("http://localhost:8080").unwrap();
11234        let mock_token = "test_live_token".to_string();
11235
11236        let token_manager =
11237            Arc::new(TokenManager::with_mock_token(config, base_url, mock_token.clone()).unwrap());
11238
11239        let strategy = LiveTokenStrategy::with_token_manager(token_manager);
11240
11241        // Test strategy properties
11242        assert_eq!(strategy.strategy_type(), "live");
11243        assert!(strategy.should_persist());
11244
11245        // Test token retrieval
11246        let token_result = strategy.get_token().await;
11247        assert!(token_result.is_ok());
11248        assert_eq!(token_result.unwrap(), mock_token);
11249    }
11250
11251    #[tokio::test]
11252    async fn test_live_token_strategy_clear_token() {
11253        // Create a live strategy with a mock token manager
11254        let config = RetryConfig::for_tests();
11255        let base_url = Url::parse("http://localhost:8080").unwrap();
11256        let mock_token = "test_clear_token".to_string();
11257
11258        let token_manager =
11259            Arc::new(TokenManager::with_mock_token(config, base_url, mock_token.clone()).unwrap());
11260
11261        let strategy = LiveTokenStrategy::with_token_manager(token_manager);
11262
11263        // Verify token is available initially
11264        let initial_token = strategy.get_token().await;
11265        assert!(initial_token.is_ok());
11266        assert_eq!(initial_token.unwrap(), mock_token);
11267
11268        // Clear the token
11269        let clear_result = strategy.clear_token().await;
11270        assert!(clear_result.is_ok());
11271
11272        // Note: After clearing, the TokenManager would try to obtain a new token
11273        // In a real scenario, this would fail without proper credentials
11274        // But our mock token manager will still return the same token
11275    }
11276
11277    #[tokio::test]
11278    async fn test_strategy_type_identification() {
11279        let mock_strategy = MockTokenStrategy::new("test_token".to_string());
11280        let live_strategy = LiveTokenStrategy::new().await.unwrap();
11281
11282        // Test that we can identify strategy types for debugging
11283        assert_eq!(mock_strategy.strategy_type(), "mock");
11284        assert_eq!(live_strategy.strategy_type(), "live");
11285
11286        // Test persistence settings
11287        assert!(!mock_strategy.should_persist());
11288        assert!(live_strategy.should_persist());
11289    }
11290
11291    #[tokio::test]
11292    async fn test_strategy_debug_formatting() {
11293        let mock_strategy = MockTokenStrategy::new("debug_test_token".to_string());
11294        let debug_output = format!("{mock_strategy:?}");
11295
11296        // Verify debug output contains expected information
11297        assert!(debug_output.contains("MockTokenStrategy"));
11298        assert!(debug_output.contains("debug_test_token"));
11299    }
11300
11301    // Environment Detection Tests
11302
11303    #[test]
11304    fn test_token_environment_detect_live_via_amp_tests() {
11305        // Set up environment for live test detection
11306        env::set_var("AMP_TESTS", "live");
11307        env::set_var("AMP_USERNAME", "real_user");
11308        env::set_var("AMP_PASSWORD", "real_pass");
11309        env::remove_var("AMP_API_BASE_URL");
11310
11311        let environment = TokenEnvironment::detect();
11312        assert_eq!(environment, TokenEnvironment::Live);
11313
11314        // Clean up
11315        env::remove_var("AMP_TESTS");
11316        env::remove_var("AMP_USERNAME");
11317        env::remove_var("AMP_PASSWORD");
11318    }
11319
11320    #[test]
11321    fn test_token_environment_detect_mock_via_credentials() {
11322        // Set up environment for mock detection via username
11323        env::remove_var("AMP_TESTS");
11324        env::set_var("AMP_USERNAME", "mock_user");
11325        env::set_var("AMP_PASSWORD", "real_pass");
11326        env::remove_var("AMP_API_BASE_URL");
11327
11328        let environment = TokenEnvironment::detect();
11329        assert_eq!(environment, TokenEnvironment::Mock);
11330
11331        // Test mock detection via password
11332        env::set_var("AMP_USERNAME", "real_user");
11333        env::set_var("AMP_PASSWORD", "mock_pass");
11334
11335        let environment = TokenEnvironment::detect();
11336        assert_eq!(environment, TokenEnvironment::Mock);
11337
11338        // Clean up
11339        env::remove_var("AMP_USERNAME");
11340        env::remove_var("AMP_PASSWORD");
11341    }
11342
11343    #[test]
11344    fn test_token_environment_detect_mock_via_base_url() {
11345        // Set up environment for mock detection via localhost URL
11346        env::remove_var("AMP_TESTS");
11347        env::set_var("AMP_USERNAME", "real_user");
11348        env::set_var("AMP_PASSWORD", "real_pass");
11349        env::set_var("AMP_API_BASE_URL", "http://localhost:8080/api");
11350
11351        let environment = TokenEnvironment::detect();
11352        assert_eq!(environment, TokenEnvironment::Mock);
11353
11354        // Test with 127.0.0.1
11355        env::set_var("AMP_API_BASE_URL", "http://127.0.0.1:3000/api");
11356        let environment = TokenEnvironment::detect();
11357        assert_eq!(environment, TokenEnvironment::Mock);
11358
11359        // Test with mock in URL
11360        env::set_var("AMP_API_BASE_URL", "http://mock-server.example.com/api");
11361        let environment = TokenEnvironment::detect();
11362        assert_eq!(environment, TokenEnvironment::Mock);
11363
11364        // Clean up
11365        env::remove_var("AMP_USERNAME");
11366        env::remove_var("AMP_PASSWORD");
11367        env::remove_var("AMP_API_BASE_URL");
11368    }
11369
11370    #[test]
11371    fn test_token_environment_detect_live_via_real_credentials() {
11372        // Set up environment for live detection via real credentials
11373        env::remove_var("AMP_TESTS");
11374        env::set_var("AMP_USERNAME", "real_user");
11375        env::set_var("AMP_PASSWORD", "real_pass");
11376        env::set_var("AMP_API_BASE_URL", "https://amp-test.blockstream.com/api");
11377
11378        let environment = TokenEnvironment::detect();
11379        assert_eq!(environment, TokenEnvironment::Live);
11380
11381        // Clean up
11382        env::remove_var("AMP_USERNAME");
11383        env::remove_var("AMP_PASSWORD");
11384        env::remove_var("AMP_API_BASE_URL");
11385    }
11386
11387    #[test]
11388    fn test_token_environment_detect_mock_fallback() {
11389        // Set up environment with no credentials (fallback to mock)
11390        env::remove_var("AMP_TESTS");
11391        env::remove_var("AMP_USERNAME");
11392        env::remove_var("AMP_PASSWORD");
11393        env::remove_var("AMP_API_BASE_URL");
11394
11395        let environment = TokenEnvironment::detect();
11396        assert_eq!(environment, TokenEnvironment::Mock);
11397    }
11398
11399    #[test]
11400    fn test_has_mock_credentials() {
11401        // Test mock username detection
11402        assert!(TokenEnvironment::has_mock_credentials(
11403            "mock_user",
11404            "real_pass",
11405            ""
11406        ));
11407        assert!(TokenEnvironment::has_mock_credentials(
11408            "Mock_User",
11409            "real_pass",
11410            ""
11411        ));
11412        assert!(TokenEnvironment::has_mock_credentials(
11413            "user_mock",
11414            "real_pass",
11415            ""
11416        ));
11417
11418        // Test mock password detection
11419        assert!(TokenEnvironment::has_mock_credentials(
11420            "real_user",
11421            "mock_pass",
11422            ""
11423        ));
11424        assert!(TokenEnvironment::has_mock_credentials(
11425            "real_user",
11426            "Mock_Pass",
11427            ""
11428        ));
11429        assert!(TokenEnvironment::has_mock_credentials(
11430            "real_user",
11431            "pass_mock",
11432            ""
11433        ));
11434
11435        // Test mock URL detection
11436        assert!(TokenEnvironment::has_mock_credentials(
11437            "real_user",
11438            "real_pass",
11439            "http://localhost:8080"
11440        ));
11441        assert!(TokenEnvironment::has_mock_credentials(
11442            "real_user",
11443            "real_pass",
11444            "http://127.0.0.1:3000"
11445        ));
11446        assert!(TokenEnvironment::has_mock_credentials(
11447            "real_user",
11448            "real_pass",
11449            "http://mock-server.com"
11450        ));
11451        assert!(TokenEnvironment::has_mock_credentials(
11452            "real_user",
11453            "real_pass",
11454            "http://Mock-Server.com"
11455        ));
11456
11457        // Test non-mock credentials
11458        assert!(!TokenEnvironment::has_mock_credentials(
11459            "real_user",
11460            "real_pass",
11461            "https://amp-test.blockstream.com"
11462        ));
11463        assert!(!TokenEnvironment::has_mock_credentials("", "", ""));
11464    }
11465
11466    #[test]
11467    fn test_token_environment_should_persist_tokens() {
11468        assert!(!TokenEnvironment::Mock.should_persist_tokens());
11469        assert!(TokenEnvironment::Live.should_persist_tokens());
11470
11471        // Auto should delegate to detect()
11472        env::set_var("AMP_TESTS", "live");
11473        assert!(TokenEnvironment::Auto.should_persist_tokens());
11474
11475        env::set_var("AMP_USERNAME", "mock_user");
11476        env::set_var("AMP_PASSWORD", "some_password");
11477        env::remove_var("AMP_TESTS");
11478        env::remove_var("AMP_API_BASE_URL");
11479        assert!(!TokenEnvironment::Auto.should_persist_tokens());
11480
11481        // Clean up
11482        env::remove_var("AMP_USERNAME");
11483        env::remove_var("AMP_PASSWORD");
11484    }
11485
11486    #[test]
11487    fn test_token_environment_is_mock_and_is_live() {
11488        assert!(TokenEnvironment::Mock.is_mock());
11489        assert!(!TokenEnvironment::Mock.is_live());
11490
11491        assert!(!TokenEnvironment::Live.is_mock());
11492        assert!(TokenEnvironment::Live.is_live());
11493
11494        // Auto should delegate to detect()
11495        env::set_var("AMP_USERNAME", "mock_user");
11496        env::set_var("AMP_PASSWORD", "some_password");
11497        env::remove_var("AMP_TESTS");
11498        env::remove_var("AMP_API_BASE_URL");
11499        assert!(TokenEnvironment::Auto.is_mock());
11500        assert!(!TokenEnvironment::Auto.is_live());
11501
11502        env::set_var("AMP_TESTS", "live");
11503        assert!(!TokenEnvironment::Auto.is_mock());
11504        assert!(TokenEnvironment::Auto.is_live());
11505
11506        // Clean up
11507        env::remove_var("AMP_USERNAME");
11508        env::remove_var("AMP_PASSWORD");
11509        env::remove_var("AMP_TESTS");
11510    }
11511
11512    #[tokio::test]
11513    async fn test_token_environment_create_strategy_mock() {
11514        let mock_token = "test_mock_token".to_string();
11515        let strategy = TokenEnvironment::Mock
11516            .create_strategy(Some(mock_token.clone()))
11517            .await
11518            .unwrap();
11519
11520        assert_eq!(strategy.strategy_type(), "mock");
11521        assert!(!strategy.should_persist());
11522
11523        let token = strategy.get_token().await.unwrap();
11524        assert_eq!(token, mock_token);
11525    }
11526
11527    #[tokio::test]
11528    async fn test_token_environment_create_strategy_live() {
11529        let strategy = TokenEnvironment::Live.create_strategy(None).await.unwrap();
11530
11531        assert_eq!(strategy.strategy_type(), "live");
11532        assert!(strategy.should_persist());
11533    }
11534
11535    #[tokio::test]
11536    async fn test_token_environment_create_auto_strategy() {
11537        // Test with mock environment - need both username and password for proper detection
11538        env::set_var("AMP_USERNAME", "mock_user");
11539        env::set_var("AMP_PASSWORD", "some_password");
11540        env::remove_var("AMP_TESTS");
11541        env::remove_var("AMP_API_BASE_URL");
11542
11543        let mock_token = "auto_mock_token".to_string();
11544        let strategy = TokenEnvironment::create_auto_strategy(Some(mock_token.clone()))
11545            .await
11546            .unwrap();
11547
11548        assert_eq!(strategy.strategy_type(), "mock");
11549        let token = strategy.get_token().await.unwrap();
11550        assert_eq!(token, mock_token);
11551
11552        // Clean up
11553        env::remove_var("AMP_USERNAME");
11554        env::remove_var("AMP_PASSWORD");
11555    }
11556
11557    #[tokio::test]
11558    async fn test_mock_token_strategy_factory_methods() {
11559        // Test with_default_token
11560        let strategy = MockTokenStrategy::with_default_token();
11561        assert_eq!(strategy.strategy_type(), "mock");
11562        let token = strategy.get_token().await.unwrap();
11563        assert_eq!(token, "mock_token_default");
11564
11565        // Test for_test
11566        let strategy = MockTokenStrategy::for_test("my_test");
11567        let token = strategy.get_token().await.unwrap();
11568        assert_eq!(token, "mock_token_my_test");
11569    }
11570
11571    #[tokio::test]
11572    async fn test_live_token_strategy_factory_methods() {
11573        // Test for_testing
11574        let strategy = LiveTokenStrategy::for_testing().await.unwrap();
11575        assert_eq!(strategy.strategy_type(), "live");
11576        assert!(strategy.should_persist());
11577    }
11578
11579    #[tokio::test]
11580    async fn test_standalone_factory_functions() {
11581        // Test create_mock_token_strategy
11582        let mock_token = "standalone_mock".to_string();
11583        let strategy = create_mock_token_strategy(mock_token.clone());
11584        assert_eq!(strategy.strategy_type(), "mock");
11585        let token = strategy.get_token().await.unwrap();
11586        assert_eq!(token, mock_token);
11587
11588        // Test create_live_token_strategy
11589        let strategy = create_live_token_strategy().await.unwrap();
11590        assert_eq!(strategy.strategy_type(), "live");
11591
11592        // Test create_auto_token_strategy with mock environment
11593        env::set_var("AMP_USERNAME", "mock_user");
11594        env::set_var("AMP_PASSWORD", "some_password");
11595        env::remove_var("AMP_TESTS");
11596        env::remove_var("AMP_API_BASE_URL");
11597
11598        let auto_mock_token = "auto_standalone_mock".to_string();
11599        let strategy = create_auto_token_strategy(Some(auto_mock_token.clone()))
11600            .await
11601            .unwrap();
11602        assert_eq!(strategy.strategy_type(), "mock");
11603        let token = strategy.get_token().await.unwrap();
11604        assert_eq!(token, auto_mock_token);
11605
11606        // Test create_token_strategy_for_environment
11607        let env_mock_token = "env_mock".to_string();
11608        let strategy = create_token_strategy_for_environment(
11609            TokenEnvironment::Mock,
11610            Some(env_mock_token.clone()),
11611        )
11612        .await
11613        .unwrap();
11614        assert_eq!(strategy.strategy_type(), "mock");
11615        let token = strategy.get_token().await.unwrap();
11616        assert_eq!(token, env_mock_token);
11617
11618        // Clean up
11619        env::remove_var("AMP_USERNAME");
11620        env::remove_var("AMP_PASSWORD");
11621    }
11622
11623    #[test]
11624    fn test_environment_detection_with_various_credential_combinations() {
11625        // Test case 1: AMP_TESTS=live overrides everything
11626        env::set_var("AMP_TESTS", "live");
11627        env::set_var("AMP_USERNAME", "mock_user");
11628        env::set_var("AMP_PASSWORD", "mock_pass");
11629        env::set_var("AMP_API_BASE_URL", "http://localhost:8080");
11630        assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Live);
11631
11632        // Test case 2: Mock username with real password and URL
11633        env::remove_var("AMP_TESTS");
11634        env::set_var("AMP_USERNAME", "mock_user");
11635        env::set_var("AMP_PASSWORD", "real_password");
11636        env::set_var("AMP_API_BASE_URL", "https://amp-test.blockstream.com/api");
11637        assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Mock);
11638
11639        // Test case 3: Real username with mock password
11640        env::set_var("AMP_USERNAME", "real_user");
11641        env::set_var("AMP_PASSWORD", "mock_password");
11642        env::set_var("AMP_API_BASE_URL", "https://amp-test.blockstream.com/api");
11643        assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Mock);
11644
11645        // Test case 4: Real credentials with localhost URL
11646        env::set_var("AMP_USERNAME", "real_user");
11647        env::set_var("AMP_PASSWORD", "real_password");
11648        env::set_var("AMP_API_BASE_URL", "http://localhost:3000/api");
11649        assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Mock);
11650
11651        // Test case 5: All real credentials
11652        env::set_var("AMP_USERNAME", "real_user");
11653        env::set_var("AMP_PASSWORD", "real_password");
11654        env::set_var("AMP_API_BASE_URL", "https://amp-test.blockstream.com/api");
11655        assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Live);
11656
11657        // Test case 6: Empty credentials
11658        env::remove_var("AMP_USERNAME");
11659        env::remove_var("AMP_PASSWORD");
11660        env::remove_var("AMP_API_BASE_URL");
11661        assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Mock);
11662
11663        // Test case 7: Only username set
11664        env::set_var("AMP_USERNAME", "real_user");
11665        env::remove_var("AMP_PASSWORD");
11666        assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Mock);
11667
11668        // Test case 8: Only password set
11669        env::remove_var("AMP_USERNAME");
11670        env::set_var("AMP_PASSWORD", "real_password");
11671        assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Mock);
11672
11673        // Clean up all environment variables
11674        env::remove_var("AMP_TESTS");
11675        env::remove_var("AMP_USERNAME");
11676        env::remove_var("AMP_PASSWORD");
11677        env::remove_var("AMP_API_BASE_URL");
11678    }
11679
11680    #[tokio::test]
11681    async fn test_distribute_asset_input_validation() {
11682        // Create a mock client for testing
11683        let client = ApiClient::with_mock_token(
11684            reqwest::Url::parse("http://localhost:8080/api").unwrap(),
11685            "test_token".to_string(),
11686        )
11687        .unwrap();
11688
11689        // Test invalid asset UUID
11690        let assignments = vec![AssetDistributionAssignment {
11691            user_id: "user123".to_string(),
11692            address: "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
11693            amount: 100.0,
11694        }];
11695
11696        // Create a mock ElementsRpc (this will fail connection validation, but that's expected)
11697        let elements_rpc = ElementsRpc::new(
11698            "http://localhost:18884".to_string(),
11699            "user".to_string(),
11700            "pass".to_string(),
11701        );
11702
11703        // Create a mock signer
11704        let (_, signer) = LwkSoftwareSigner::generate_new().unwrap();
11705
11706        // Test with invalid UUID format
11707        let result = client
11708            .distribute_asset(
11709                "invalid-uuid",
11710                assignments.clone(),
11711                &elements_rpc,
11712                "test_wallet",
11713                &signer,
11714            )
11715            .await;
11716
11717        assert!(result.is_err());
11718        if let Err(AmpError::Validation(msg)) = result {
11719            assert!(msg.contains("Invalid asset UUID"));
11720        } else {
11721            panic!("Expected validation error for invalid UUID");
11722        }
11723
11724        // Test with empty assignments
11725        let result = client
11726            .distribute_asset(
11727                "550e8400-e29b-41d4-a716-446655440000",
11728                vec![],
11729                &elements_rpc,
11730                "test_wallet",
11731                &signer,
11732            )
11733            .await;
11734
11735        assert!(result.is_err());
11736        if let Err(AmpError::Validation(msg)) = result {
11737            assert!(msg.contains("Invalid assignments"));
11738        } else {
11739            panic!("Expected validation error for empty assignments");
11740        }
11741    }
11742
11743    #[test]
11744    fn test_validate_asset_uuid() {
11745        let _client = ApiClient::with_mock_token(
11746            reqwest::Url::parse("http://localhost:8080/api").unwrap(),
11747            "test_token".to_string(),
11748        )
11749        .unwrap();
11750
11751        // Valid UUID
11752        assert!(ApiClient::validate_asset_uuid("550e8400-e29b-41d4-a716-446655440000").is_ok());
11753
11754        // Invalid UUIDs
11755        assert!(ApiClient::validate_asset_uuid("").is_err());
11756        assert!(ApiClient::validate_asset_uuid("invalid").is_err());
11757        assert!(ApiClient::validate_asset_uuid("550e8400-e29b-41d4-a716").is_err()); // Too short
11758        assert!(
11759            ApiClient::validate_asset_uuid("550e8400-e29b-41d4-a716-446655440000-extra").is_err()
11760        ); // Too long
11761        assert!(ApiClient::validate_asset_uuid("550e8400xe29bx41d4xa716x446655440000").is_err()); // Wrong separators
11762        assert!(ApiClient::validate_asset_uuid("550e8400-e29g-41d4-a716-446655440000").is_err());
11763        // Invalid hex char
11764    }
11765
11766    #[test]
11767    fn test_validate_assignments() {
11768        let _client = ApiClient::with_mock_token(
11769            reqwest::Url::parse("http://localhost:8080/api").unwrap(),
11770            "test_token".to_string(),
11771        )
11772        .unwrap();
11773
11774        // Valid assignments
11775        let valid_assignments = vec![AssetDistributionAssignment {
11776            user_id: "user123".to_string(),
11777            address: "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
11778            amount: 100.0,
11779        }];
11780        assert!(ApiClient::validate_assignments(&valid_assignments).is_ok());
11781
11782        // Empty assignments
11783        assert!(ApiClient::validate_assignments(&[]).is_err());
11784
11785        // Assignment with empty user_id
11786        let invalid_assignments = vec![AssetDistributionAssignment {
11787            user_id: "".to_string(),
11788            address: "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
11789            amount: 100.0,
11790        }];
11791        assert!(ApiClient::validate_assignments(&invalid_assignments).is_err());
11792
11793        // Assignment with empty address
11794        let invalid_assignments = vec![AssetDistributionAssignment {
11795            user_id: "user123".to_string(),
11796            address: "".to_string(),
11797            amount: 100.0,
11798        }];
11799        assert!(ApiClient::validate_assignments(&invalid_assignments).is_err());
11800
11801        // Assignment with invalid address format
11802        let invalid_assignments = vec![AssetDistributionAssignment {
11803            user_id: "user123".to_string(),
11804            address: "invalid_address".to_string(),
11805            amount: 100.0,
11806        }];
11807        assert!(ApiClient::validate_assignments(&invalid_assignments).is_err());
11808
11809        // Assignment with non-positive amount
11810        let invalid_assignments = vec![AssetDistributionAssignment {
11811            user_id: "user123".to_string(),
11812            address: "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
11813            amount: 0.0,
11814        }];
11815        assert!(ApiClient::validate_assignments(&invalid_assignments).is_err());
11816
11817        // Assignment with unreasonably large amount
11818        let invalid_assignments = vec![AssetDistributionAssignment {
11819            user_id: "user123".to_string(),
11820            address: "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
11821            amount: 25_000_000.0,
11822        }];
11823        assert!(ApiClient::validate_assignments(&invalid_assignments).is_err());
11824    }
11825
11826    #[test]
11827    fn test_enhanced_error_handling_and_logging() {
11828        // Test AmpError creation and context enhancement
11829        let api_error = AmpError::api("Distribution creation failed");
11830        let contextual_error = api_error.with_context("Step 6: Distribution creation");
11831
11832        match contextual_error {
11833            AmpError::Api(msg) => {
11834                assert!(msg.contains("Step 6: Distribution creation"));
11835                assert!(msg.contains("Distribution creation failed"));
11836            }
11837            _ => panic!("Expected Api error variant"),
11838        }
11839
11840        // Test retry instructions for different error types
11841        let rpc_error = AmpError::rpc("Connection failed");
11842        assert!(rpc_error.is_retryable());
11843        assert!(rpc_error.retry_instructions().is_some());
11844        assert!(rpc_error
11845            .retry_instructions()
11846            .unwrap()
11847            .contains("Elements node"));
11848
11849        let validation_error = AmpError::validation("Invalid UUID");
11850        assert!(!validation_error.is_retryable());
11851        assert!(validation_error.retry_instructions().is_none());
11852
11853        let timeout_error = AmpError::timeout("Confirmation timeout for txid abc123");
11854        assert!(!timeout_error.is_retryable());
11855        let instructions = timeout_error.retry_instructions();
11856        assert!(instructions.is_some());
11857        assert!(instructions.unwrap().contains("transaction ID"));
11858
11859        // Test error helper methods
11860        let signer_error =
11861            AmpError::Signer(crate::signer::SignerError::Lwk("Test error".to_string()));
11862        assert!(!signer_error.is_retryable());
11863        assert!(signer_error.retry_instructions().is_none());
11864
11865        // Test serialization error
11866        let json_error = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
11867        let serialization_error = AmpError::from(json_error);
11868        assert!(matches!(serialization_error, AmpError::Serialization(_)));
11869        assert!(!serialization_error.is_retryable());
11870    }
11871}