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, IssuanceRequest, IssuanceResponse, Outpoint, Ownership, Password,
25    ReceivedByAddress, RegisterAssetResponse, TokenData, TokenInfo, TokenRequest, TokenResponse,
26    TransactionDetail, TxInput, 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("AMP request failed\n\nMethod: {method}\nEndpoint: {endpoint}\nStatus: {status}\n\nError: {error_message}")]
411    RequestFailedDetailed {
412        /// The HTTP method used (GET, POST, etc.)
413        method: String,
414        /// The full endpoint URL that was called
415        endpoint: String,
416        /// The HTTP status code
417        status: reqwest::StatusCode,
418        /// The error message or response body
419        error_message: String,
420    },
421    #[error("Failed to parse AMP response: {0}")]
422    ResponseParsingFailed(String),
423    #[error("Failed to parse AMP response: {serde_error}\n\nMethod: {method}\nEndpoint: {endpoint}\nExpected Type: {expected_type}\n\nRaw Response:\n{raw_response}")]
424    ResponseDeserializationFailed {
425        /// The HTTP method used (GET, POST, etc.)
426        method: String,
427        /// The full endpoint URL that was called
428        endpoint: String,
429        /// The expected Rust type name
430        expected_type: String,
431        /// The original serde deserialization error message
432        serde_error: String,
433        /// The complete raw response body text that failed to parse
434        raw_response: String,
435    },
436    #[error("AMP token request failed with status {status}: {error_text}")]
437    TokenRequestFailed {
438        status: reqwest::StatusCode,
439        error_text: String,
440    },
441    #[error("Failed to parse url: {0}")]
442    UrlParse(#[from] url::ParseError),
443    #[error("Reqwest error: {0}")]
444    Reqwest(#[from] reqwest::Error),
445    #[error("Invalid retry configuration: {0}")]
446    InvalidRetryConfig(String),
447    #[error("Token management error: {0}")]
448    Token(#[from] TokenError),
449}
450
451/// Enhanced error enum for distribution operations and `ElementsRpc`
452#[derive(Error, Debug)]
453pub enum AmpError {
454    #[error("API error: {0}")]
455    Api(String),
456    #[error("API error\n\nEndpoint: {endpoint}\nMethod: {method}\n\nError: {error_message}")]
457    ApiDetailed {
458        /// The API endpoint that was called
459        endpoint: String,
460        /// The HTTP method used
461        method: String,
462        /// The error message
463        error_message: String,
464    },
465
466    #[error("RPC error: {0}")]
467    Rpc(String),
468    #[error("RPC error: {error_message}\n\nMethod: {rpc_method}\nParameters: {params}\n\nRaw Response:\n{raw_response}")]
469    RpcDetailed {
470        /// The RPC method name that was called
471        rpc_method: String,
472        /// The parameters passed to the RPC method
473        params: String,
474        /// The error message
475        error_message: String,
476        /// The complete raw response from the RPC server
477        raw_response: String,
478    },
479
480    #[error("Signer error: {0}")]
481    Signer(#[from] SignerError),
482
483    #[error("Timeout waiting for confirmations: {0}")]
484    Timeout(String),
485
486    #[error("Validation error: {0}")]
487    Validation(String),
488
489    #[error("Network error: {0}")]
490    Network(#[from] reqwest::Error),
491
492    #[error("Serialization error: {0}")]
493    Serialization(#[from] serde_json::Error),
494    #[error("Serialization error: {serde_error}\n\nOperation: {operation}\nData Type: {data_type}\n\nContext: {context}")]
495    SerializationDetailed {
496        /// The serialization operation (serialize/deserialize)
497        operation: String,
498        /// The data type being processed
499        data_type: String,
500        /// Additional context about the operation
501        context: String,
502        /// The original serde error message
503        serde_error: String,
504    },
505
506    #[error(transparent)]
507    Existing(#[from] Error),
508}
509
510impl AmpError {
511    /// Creates a new API error
512    pub fn api<S: Into<String>>(message: S) -> Self {
513        Self::Api(message.into())
514    }
515
516    /// Creates a new RPC error
517    pub fn rpc<S: Into<String>>(message: S) -> Self {
518        Self::Rpc(message.into())
519    }
520
521    /// Creates a new timeout error
522    pub fn timeout<S: Into<String>>(message: S) -> Self {
523        Self::Timeout(message.into())
524    }
525
526    /// Creates a new validation error
527    pub fn validation<S: Into<String>>(message: S) -> Self {
528        Self::Validation(message.into())
529    }
530
531    /// Adds context to an error
532    #[must_use]
533    pub fn with_context<S: Into<String>>(self, context: S) -> Self {
534        let context_str = context.into();
535        match self {
536            Self::Api(msg) => Self::Api(format!("{context_str}: {msg}")),
537            Self::ApiDetailed {
538                endpoint,
539                method,
540                error_message,
541            } => Self::ApiDetailed {
542                endpoint,
543                method,
544                error_message: format!("{context_str}: {error_message}"),
545            },
546            Self::Rpc(msg) => Self::Rpc(format!("{context_str}: {msg}")),
547            Self::RpcDetailed {
548                rpc_method,
549                params,
550                error_message,
551                raw_response,
552            } => Self::RpcDetailed {
553                rpc_method,
554                params,
555                error_message: format!("{context_str}: {error_message}"),
556                raw_response,
557            },
558            Self::Timeout(msg) => Self::Timeout(format!("{context_str}: {msg}")),
559            Self::Validation(msg) => Self::Validation(format!("{context_str}: {msg}")),
560            other => other, // Don't modify other error types
561        }
562    }
563
564    /// Returns true if this error indicates a retryable condition
565    #[must_use]
566    pub const fn is_retryable(&self) -> bool {
567        match self {
568            Self::Network(_) | Self::Rpc(_) | Self::RpcDetailed { .. } => true, // RPC errors might be transient
569            Self::Existing(Error::Token(token_err)) => token_err.is_retryable(),
570            _ => false,
571        }
572    }
573
574    /// Provides user-friendly retry instructions when applicable
575    #[must_use]
576    pub fn retry_instructions(&self) -> Option<String> {
577        match self {
578            Self::Network(_) => Some("Check network connection and retry".to_string()),
579            Self::Rpc(_) | Self::RpcDetailed { .. } => {
580                Some("Check Elements node connection and retry".to_string())
581            }
582            Self::Timeout(msg) if msg.contains("txid") => {
583                Some("Use the transaction ID to manually confirm the distribution".to_string())
584            }
585            Self::Existing(Error::Token(TokenError::RateLimited {
586                retry_after_seconds,
587            })) => Some(format!(
588                "Rate limited. Retry after {retry_after_seconds} seconds"
589            )),
590            _ => None,
591        }
592    }
593}
594
595/// Detailed error types for token management operations
596#[derive(Error, Debug, Clone, PartialEq, Eq)]
597pub enum TokenError {
598    #[error("Token refresh failed: {0}")]
599    RefreshFailed(String),
600    #[error("Token obtain failed after {attempts} attempts: {last_error}")]
601    ObtainFailed { attempts: u32, last_error: String },
602    #[error("Rate limited: retry after {retry_after_seconds} seconds")]
603    RateLimited { retry_after_seconds: u64 },
604    #[error("Request timeout after {timeout_seconds} seconds")]
605    Timeout { timeout_seconds: u64 },
606    #[error("Serialization error: {0}")]
607    Serialization(String),
608    #[error("Token storage error: {0}")]
609    Storage(String),
610    #[error("Token validation error: {0}")]
611    Validation(String),
612}
613
614impl TokenError {
615    /// Creates a new `RefreshFailed` error
616    #[must_use]
617    pub fn refresh_failed<S: Into<String>>(message: S) -> Self {
618        Self::RefreshFailed(message.into())
619    }
620
621    /// Creates a new `ObtainFailed` error
622    #[must_use]
623    pub const fn obtain_failed(attempts: u32, last_error: String) -> Self {
624        Self::ObtainFailed {
625            attempts,
626            last_error,
627        }
628    }
629
630    /// Creates a new `RateLimited` error
631    #[must_use]
632    pub const fn rate_limited(retry_after_seconds: u64) -> Self {
633        Self::RateLimited {
634            retry_after_seconds,
635        }
636    }
637
638    /// Creates a new Timeout error
639    #[must_use]
640    pub const fn timeout(timeout_seconds: u64) -> Self {
641        Self::Timeout { timeout_seconds }
642    }
643
644    /// Creates a new Serialization error
645    #[must_use]
646    pub fn serialization<S: Into<String>>(message: S) -> Self {
647        Self::Serialization(message.into())
648    }
649
650    /// Creates a new Storage error
651    #[must_use]
652    pub fn storage<S: Into<String>>(message: S) -> Self {
653        Self::Storage(message.into())
654    }
655
656    /// Creates a new Validation error
657    #[must_use]
658    pub fn validation<S: Into<String>>(message: S) -> Self {
659        Self::Validation(message.into())
660    }
661
662    /// Returns true if this error indicates a retryable condition
663    #[must_use]
664    pub const fn is_retryable(&self) -> bool {
665        matches!(
666            self,
667            Self::RefreshFailed(_) | Self::RateLimited { .. } | Self::Timeout { .. }
668        )
669    }
670
671    /// Returns true if this error indicates a rate limiting condition
672    #[must_use]
673    pub const fn is_rate_limited(&self) -> bool {
674        matches!(self, Self::RateLimited { .. })
675    }
676
677    /// Returns the retry delay in seconds if this is a rate limited error
678    #[must_use]
679    pub const fn retry_after_seconds(&self) -> Option<u64> {
680        match self {
681            Self::RateLimited {
682                retry_after_seconds,
683            } => Some(*retry_after_seconds),
684            _ => None,
685        }
686    }
687}
688
689// Conversion from serde_json::Error for serialization errors
690impl From<serde_json::Error> for TokenError {
691    fn from(err: serde_json::Error) -> Self {
692        Self::Serialization(err.to_string())
693    }
694}
695
696#[cfg(test)]
697mod amp_error_tests {
698    use super::*;
699
700    #[test]
701    fn test_amp_error_creation_helpers() {
702        let api_error = AmpError::api("Failed to create distribution");
703        assert!(matches!(api_error, AmpError::Api(_)));
704
705        let rpc_error = AmpError::rpc("Elements node connection failed");
706        assert!(matches!(rpc_error, AmpError::Rpc(_)));
707
708        let validation_error = AmpError::validation("Invalid asset UUID format");
709        assert!(matches!(validation_error, AmpError::Validation(_)));
710
711        let timeout_error = AmpError::timeout("Confirmation timeout");
712        assert!(matches!(timeout_error, AmpError::Timeout(_)));
713    }
714
715    #[test]
716    fn test_amp_error_with_context() {
717        let api_error = AmpError::api("Failed to create distribution");
718        let contextual_error = api_error.with_context("During distribution creation");
719
720        match contextual_error {
721            AmpError::Api(msg) => {
722                assert!(msg.contains("During distribution creation"));
723                assert!(msg.contains("Failed to create distribution"));
724            }
725            _ => panic!("Expected Api error variant"),
726        }
727
728        // Test that context doesn't modify errors that already have good context
729        let signer_error = AmpError::Signer(SignerError::Lwk("Test error".to_string()));
730        let contextual_signer = signer_error.with_context("Additional context");
731        assert!(matches!(contextual_signer, AmpError::Signer(_)));
732    }
733
734    #[test]
735    fn test_amp_error_retryability() {
736        let api_error = AmpError::api("Failed to create distribution");
737        assert!(!api_error.is_retryable());
738
739        let rpc_error = AmpError::rpc("Elements node connection failed");
740        assert!(rpc_error.is_retryable());
741
742        let validation_error = AmpError::validation("Invalid asset UUID format");
743        assert!(!validation_error.is_retryable());
744
745        let timeout_error = AmpError::timeout("Confirmation timeout");
746        assert!(!timeout_error.is_retryable());
747
748        let signer_error = AmpError::Signer(SignerError::Lwk("Test error".to_string()));
749        assert!(!signer_error.is_retryable());
750    }
751
752    #[test]
753    fn test_amp_error_retry_instructions() {
754        let rpc_error = AmpError::rpc("Elements node connection failed");
755        let instructions = rpc_error.retry_instructions();
756        assert!(instructions.is_some());
757        assert!(instructions.unwrap().contains("Elements node"));
758
759        let validation_error = AmpError::validation("Invalid asset UUID format");
760        assert!(validation_error.retry_instructions().is_none());
761
762        let timeout_with_txid = AmpError::timeout("Confirmation timeout for txid abc123");
763        let timeout_instructions = timeout_with_txid.retry_instructions();
764        assert!(timeout_instructions.is_some());
765        assert!(timeout_instructions.unwrap().contains("transaction ID"));
766    }
767
768    #[test]
769    fn test_amp_error_display() {
770        let api_error = AmpError::api("Test API error");
771        assert_eq!(format!("{}", api_error), "API error: Test API error");
772
773        let rpc_error = AmpError::rpc("Test RPC error");
774        assert_eq!(format!("{}", rpc_error), "RPC error: Test RPC error");
775
776        let validation_error = AmpError::validation("Test validation error");
777        assert_eq!(
778            format!("{}", validation_error),
779            "Validation error: Test validation error"
780        );
781
782        let timeout_error = AmpError::timeout("Test timeout error");
783        assert_eq!(
784            format!("{}", timeout_error),
785            "Timeout waiting for confirmations: Test timeout error"
786        );
787    }
788
789    #[test]
790    fn test_amp_error_from_conversions() {
791        // Test conversion from SignerError
792        let signer_error = SignerError::Lwk("Test LWK error".to_string());
793        let amp_error = AmpError::from(signer_error);
794        assert!(matches!(amp_error, AmpError::Signer(_)));
795
796        // Test conversion from existing Error
797        let existing_error = Error::MissingEnvVar("TEST_VAR".to_string());
798        let amp_error = AmpError::from(existing_error);
799        assert!(matches!(amp_error, AmpError::Existing(_)));
800
801        // Test conversion from serde_json::Error
802        let json_error = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
803        let amp_error = AmpError::from(json_error);
804        assert!(matches!(amp_error, AmpError::Serialization(_)));
805    }
806}
807
808/// Elements RPC client for blockchain operations
809#[derive(Debug)]
810pub struct ElementsRpc {
811    client: reqwest::Client,
812    base_url: String,
813    username: String,
814    password: String,
815}
816
817/// Network information from Elements node
818#[derive(Debug, serde::Deserialize)]
819pub struct NetworkInfo {
820    pub version: i64,
821    pub subversion: String,
822    pub protocolversion: i64,
823    pub localservices: String,
824    pub localrelay: bool,
825    pub timeoffset: i64,
826    pub networkactive: bool,
827    pub connections: i64,
828    pub networks: Vec<serde_json::Value>,
829    pub relayfee: f64,
830    pub incrementalfee: f64,
831    pub localaddresses: Vec<serde_json::Value>,
832    pub warnings: String,
833}
834
835/// Blockchain information from Elements node
836#[derive(Debug, serde::Deserialize)]
837pub struct BlockchainInfo {
838    pub chain: String,
839    pub blocks: i64,
840    pub headers: i64,
841    pub bestblockhash: String,
842    #[serde(default)]
843    pub difficulty: Option<f64>,
844    #[serde(default)]
845    pub mediantime: Option<i64>,
846    #[serde(default)]
847    pub verificationprogress: Option<f64>,
848    #[serde(default)]
849    pub initialblockdownload: Option<bool>,
850    #[serde(default)]
851    pub chainwork: Option<String>,
852    #[serde(default)]
853    pub size_on_disk: Option<i64>,
854    #[serde(default)]
855    pub pruned: Option<bool>,
856    #[serde(default)]
857    pub softforks: Option<serde_json::Value>,
858    #[serde(default)]
859    pub warnings: Option<String>,
860}
861
862/// RPC request structure for Elements node
863#[derive(Debug, serde::Serialize)]
864struct RpcRequest {
865    jsonrpc: String,
866    id: String,
867    method: String,
868    params: serde_json::Value,
869}
870
871/// RPC response structure from Elements node
872#[derive(Debug, serde::Deserialize)]
873struct RpcResponse<T> {
874    #[allow(dead_code)]
875    jsonrpc: Option<String>, // Optional for JSON-RPC 1.0 compatib
876    #[allow(dead_code)]
877    id: String,
878    result: Option<T>,
879    error: Option<RpcError>,
880}
881
882/// RPC error structure from Elements node
883#[derive(Debug, serde::Deserialize)]
884struct RpcError {
885    code: i32,
886    message: String,
887}
888
889impl ElementsRpc {
890    /// Creates a new `ElementsRpc` client with connection parameters
891    ///
892    /// # Arguments
893    /// * `url` - The RPC endpoint URL (e.g., <http://localhost:18884>)
894    /// * `username` - RPC authentication username
895    /// * `password` - RPC authentication password
896    ///
897    /// # Examples
898    /// ```
899    /// use amp_rs::ElementsRpc;
900    ///
901    /// let rpc = ElementsRpc::new(
902    ///     "http://localhost:18884".to_string(),
903    ///     "user".to_string(),
904    ///     "pass".to_string()
905    /// );
906    /// ```
907    /// # Panics
908    ///
909    /// Panics if the HTTP client cannot be created.
910    #[must_use]
911    pub fn new(url: String, username: String, password: String) -> Self {
912        let client = reqwest::Client::builder()
913            .timeout(std::time::Duration::from_secs(30))
914            .build()
915            .expect("Failed to create HTTP client");
916
917        Self {
918            client,
919            base_url: url,
920            username,
921            password,
922        }
923    }
924
925    /// Creates a new `ElementsRpc` client from environment variables
926    ///
927    /// Expected environment variables:
928    /// - `ELEMENTS_RPC_URL`: RPC endpoint URL
929    /// - `ELEMENTS_RPC_USER`: RPC username
930    /// - `ELEMENTS_RPC_PASSWORD`: RPC password
931    ///
932    /// # Errors
933    /// Returns an error if any required environment variable is missing
934    ///
935    /// # Examples
936    /// ```no_run
937    /// use amp_rs::ElementsRpc;
938    ///
939    /// let rpc = ElementsRpc::from_env().unwrap();
940    /// ```
941    pub fn from_env() -> Result<Self, AmpError> {
942        let url = env::var("ELEMENTS_RPC_URL")
943            .map_err(|_| AmpError::validation("Missing ELEMENTS_RPC_URL environment variable"))?;
944        let username = env::var("ELEMENTS_RPC_USER")
945            .map_err(|_| AmpError::validation("Missing ELEMENTS_RPC_USER environment variable"))?;
946        let password = env::var("ELEMENTS_RPC_PASSWORD").map_err(|_| {
947            AmpError::validation("Missing ELEMENTS_RPC_PASSWORD environment variable")
948        })?;
949
950        Ok(Self::new(url, username, password))
951    }
952
953    /// Makes an RPC call to the Elements node
954    ///
955    /// # Arguments
956    /// * `method` - The RPC method name
957    /// * `params` - The parameters for the RPC call
958    ///
959    /// # Errors
960    /// Returns an error if the RPC call fails or returns an error
961    async fn rpc_call<T: serde::de::DeserializeOwned>(
962        &self,
963        method: &str,
964        params: serde_json::Value,
965    ) -> Result<T, AmpError> {
966        tracing::debug!("Making RPC call: {} with params: {:?}", method, params);
967
968        let request = RpcRequest {
969            jsonrpc: "1.0".to_string(),
970            id: "amp-client".to_string(),
971            method: method.to_string(),
972            params,
973        };
974
975        let response = self
976            .client
977            .post(&self.base_url)
978            .basic_auth(&self.username, Some(&self.password))
979            .json(&request)
980            .send()
981            .await
982            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
983
984        if !response.status().is_success() {
985            let status = response.status();
986            let error_body = response
987                .text()
988                .await
989                .unwrap_or_else(|_| "Unable to read error body".to_string());
990            return Err(AmpError::rpc(format!(
991                "RPC request failed with status: {status} - Body: {error_body}"
992            )));
993        }
994
995        let rpc_response: RpcResponse<T> = response
996            .json()
997            .await
998            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
999
1000        if let Some(error) = rpc_response.error {
1001            return Err(AmpError::rpc(format!(
1002                "RPC error {}: {}",
1003                error.code, error.message
1004            )));
1005        }
1006
1007        rpc_response
1008            .result
1009            .ok_or_else(|| AmpError::rpc("RPC response missing result field".to_string()))
1010    }
1011
1012    /// Retrieves network information from the Elements node
1013    ///
1014    /// # Errors
1015    /// Returns an error if the RPC call fails
1016    ///
1017    /// # Examples
1018    /// ```no_run
1019    /// # use amp_rs::ElementsRpc;
1020    /// # #[tokio::main]
1021    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1022    /// let rpc = ElementsRpc::from_env()?;
1023    /// let network_info = rpc.get_network_info().await?;
1024    /// println!("Node version: {}", network_info.version);
1025    /// # Ok(())
1026    /// # }
1027    /// ```
1028    pub async fn get_network_info(&self) -> Result<NetworkInfo, AmpError> {
1029        self.rpc_call("getnetworkinfo", serde_json::Value::Array(vec![]))
1030            .await
1031    }
1032
1033    /// Retrieves blockchain information from the Elements node
1034    ///
1035    /// # Errors
1036    /// Returns an error if the RPC call fails
1037    ///
1038    /// # Examples
1039    /// ```no_run
1040    /// # use amp_rs::ElementsRpc;
1041    /// # #[tokio::main]
1042    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1043    /// let rpc = ElementsRpc::from_env()?;
1044    /// let blockchain_info = rpc.get_blockchain_info().await?;
1045    /// println!("Current block height: {}", blockchain_info.blocks);
1046    /// # Ok(())
1047    /// # }
1048    /// ```
1049    pub async fn get_blockchain_info(&self) -> Result<BlockchainInfo, AmpError> {
1050        self.rpc_call("getblockchaininfo", serde_json::Value::Array(vec![]))
1051            .await
1052    }
1053
1054    /// Unlocks the wallet with a passphrase for the specified timeout
1055    ///
1056    /// # Arguments
1057    /// * `passphrase` - The wallet passphrase
1058    /// * `timeout` - Timeout in seconds for the unlock
1059    ///
1060    /// # Errors
1061    /// Returns an error if the RPC call fails
1062    ///
1063    /// # Examples
1064    /// ```no_run
1065    /// # use amp_rs::ElementsRpc;
1066    /// # #[tokio::main]
1067    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1068    /// let rpc = ElementsRpc::from_env()?;
1069    /// rpc.wallet_passphrase("my_passphrase", 300).await?;
1070    /// # Ok(())
1071    /// # }
1072    /// ```
1073    pub async fn wallet_passphrase(&self, passphrase: &str, timeout: u64) -> Result<(), AmpError> {
1074        let params = serde_json::json!([passphrase, timeout]);
1075
1076        // wallet_passphrase returns null on success, so we need to handle this specially
1077        let request = RpcRequest {
1078            jsonrpc: "1.0".to_string(),
1079            id: "amp-client".to_string(),
1080            method: "walletpassphrase".to_string(),
1081            params,
1082        };
1083
1084        let response = self
1085            .client
1086            .post(&self.base_url)
1087            .basic_auth(&self.username, Some(&self.password))
1088            .json(&request)
1089            .send()
1090            .await
1091            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
1092
1093        if !response.status().is_success() {
1094            return Err(AmpError::rpc(format!(
1095                "RPC request failed with status: {}",
1096                response.status()
1097            )));
1098        }
1099
1100        let rpc_response: RpcResponse<serde_json::Value> = response
1101            .json()
1102            .await
1103            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
1104
1105        if let Some(error) = rpc_response.error {
1106            return Err(AmpError::rpc(format!(
1107                "RPC error {}: {}",
1108                error.code, error.message
1109            )));
1110        }
1111
1112        // For wallet_passphrase, null result is success
1113        Ok(())
1114    }
1115
1116    /// Validates the connection to the Elements node
1117    ///
1118    /// This method performs basic connectivity and authentication checks by
1119    /// retrieving network information from the node.
1120    ///
1121    /// # Errors
1122    /// Returns an error if the connection validation fails
1123    ///
1124    /// # Examples
1125    /// ```no_run
1126    /// # use amp_rs::ElementsRpc;
1127    /// # #[tokio::main]
1128    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1129    /// let rpc = ElementsRpc::from_env()?;
1130    /// rpc.validate_connection().await?;
1131    /// println!("Connection to Elements node is valid");
1132    /// # Ok(())
1133    /// # }
1134    /// ```
1135    pub async fn validate_connection(&self) -> Result<(), AmpError> {
1136        tracing::info!(
1137            "Validating connection to Elements node at {}",
1138            self.base_url
1139        );
1140
1141        let network_info = self
1142            .get_network_info()
1143            .await
1144            .map_err(|e| e.with_context("Failed to validate Elements node connection"))?;
1145
1146        tracing::info!(
1147            "Successfully connected to Elements node - Version: {}, Connections: {}",
1148            network_info.version,
1149            network_info.connections
1150        );
1151
1152        Ok(())
1153    }
1154
1155    /// Retrieves comprehensive node status including network and blockchain information
1156    ///
1157    /// This method combines network and blockchain information to provide a complete
1158    /// status overview of the Elements node.
1159    ///
1160    /// # Errors
1161    /// Returns an error if any RPC call fails
1162    ///
1163    /// # Examples
1164    /// ```no_run
1165    /// # use amp_rs::ElementsRpc;
1166    /// # #[tokio::main]
1167    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1168    /// let rpc = ElementsRpc::from_env()?;
1169    /// let (network_info, blockchain_info) = rpc.get_node_status().await?;
1170    /// println!("Node version: {}, Block height: {}", network_info.version, blockchain_info.blocks);
1171    /// # Ok(())
1172    /// # }
1173    /// ```
1174    pub async fn get_node_status(&self) -> Result<(NetworkInfo, BlockchainInfo), AmpError> {
1175        let network_info = self.get_network_info().await?;
1176        let blockchain_info = self.get_blockchain_info().await?;
1177
1178        Ok((network_info, blockchain_info))
1179    }
1180
1181    /// Lists unspent transaction outputs (UTXOs) for a specific asset
1182    ///
1183    /// # Arguments
1184    /// * `asset_id` - Optional asset ID to filter UTXOs. If None, returns all UTXOs
1185    ///
1186    /// # Errors
1187    /// Returns an error if the RPC call fails
1188    ///
1189    /// # Panics
1190    /// May panic if `asset_id` is `Some` but the warning log message attempts to unwrap it.
1191    /// This is a known logging issue and does not affect normal operation.
1192    ///
1193    /// # Examples
1194    /// ```no_run
1195    /// # use amp_rs::ElementsRpc;
1196    /// # #[tokio::main]
1197    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1198    /// let rpc = ElementsRpc::from_env()?;
1199    /// let utxos = rpc.list_unspent(Some("asset_id_hex")).await?;
1200    /// println!("Found {} UTXOs", utxos.len());
1201    /// # Ok(())
1202    /// # }
1203    /// ```
1204    pub async fn list_unspent(&self, asset_id: Option<&str>) -> Result<Vec<Unspent>, AmpError> {
1205        tracing::debug!("Listing unspent outputs for asset: {:?}", asset_id);
1206
1207        let params = asset_id.map_or_else(
1208            || serde_json::json!([1, 9_999_999, [], true]),
1209            |asset| serde_json::json!([1, 9_999_999, [], true, {"asset": asset}]),
1210        );
1211
1212        let utxos: Vec<Unspent> = self
1213            .rpc_call("listunspent", params)
1214            .await
1215            .map_err(|e| {
1216                if let Some(asset) = asset_id {
1217                    e.with_context(format!(
1218                        "Failed to list unspent outputs for asset {asset}. \
1219                        This may indicate that the treasury address is not imported in the Elements node. \
1220                        Ensure the treasury address is properly imported as a watch-only address."
1221                    ))
1222                } else {
1223                    e.with_context("Failed to list unspent outputs")
1224                }
1225            })?;
1226
1227        tracing::debug!("Found {} unspent outputs", utxos.len());
1228
1229        // If we're looking for a specific asset and found no UTXOs, provide helpful context
1230        if utxos.is_empty() && asset_id.is_some() {
1231            tracing::warn!(
1232                "No UTXOs found for asset {}. This may indicate:\n\
1233                1. The treasury address is not imported in the Elements node\n\
1234                2. The asset issuance transaction hasn't been confirmed yet\n\
1235                3. The UTXOs have already been spent",
1236                asset_id.unwrap()
1237            );
1238        }
1239
1240        Ok(utxos)
1241    }
1242
1243    /// List unspent outputs for a specific wallet
1244    ///
1245    /// This method lists unspent transaction outputs (UTXOs) for a specific wallet,
1246    /// optionally filtered by asset ID.
1247    ///
1248    /// # Arguments
1249    ///
1250    /// * `wallet_name` - Name of the Elements wallet to query
1251    /// * `asset_id` - Optional asset ID to filter UTXOs by
1252    ///
1253    /// # Returns
1254    ///
1255    /// Returns a vector of unspent outputs
1256    ///
1257    /// # Errors
1258    /// Returns an error if the RPC call fails or the wallet cannot be loaded
1259    ///
1260    /// # Panics
1261    /// May panic when processing UTXO blinding data if scriptpubkey is unexpectedly missing.
1262    /// This should not occur under normal operation with valid Elements node responses.
1263    ///
1264    /// # Example
1265    ///
1266    /// ```no_run
1267    /// # use amp_rs::ElementsRpc;
1268    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1269    /// let rpc = ElementsRpc::from_env()?;
1270    /// // Note: This would need to be called in an async context
1271    /// // let utxos = rpc.list_unspent_for_wallet("test_wallet", None).await?;
1272    /// // println!("Found {} UTXOs", utxos.len());
1273    /// # Ok(())
1274    /// # }
1275    /// ```
1276    #[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
1277    pub async fn list_unspent_for_wallet(
1278        &self,
1279        wallet_name: &str,
1280        asset_id: Option<&str>,
1281    ) -> Result<Vec<Unspent>, AmpError> {
1282        tracing::debug!(
1283            "Listing unspent outputs for wallet {} and asset: {:?}",
1284            wallet_name,
1285            asset_id
1286        );
1287
1288        // First load the wallet to ensure it's available
1289        self.load_wallet(wallet_name).await?;
1290
1291        let params = asset_id.map_or_else(
1292            || serde_json::json!([1, 9_999_999, [], true]),
1293            |asset| serde_json::json!([1, 9_999_999, [], true, {"asset": asset}]),
1294        );
1295
1296        let request = RpcRequest {
1297            jsonrpc: "1.0".to_string(),
1298            id: "amp-client".to_string(),
1299            method: "listunspent".to_string(),
1300            params,
1301        };
1302
1303        // Use the wallet-specific RPC endpoint
1304        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
1305
1306        let response = self
1307            .client
1308            .post(&wallet_url)
1309            .basic_auth(&self.username, Some(&self.password))
1310            .json(&request)
1311            .send()
1312            .await
1313            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
1314
1315        if !response.status().is_success() {
1316            let status = response.status();
1317            let error_body = response
1318                .text()
1319                .await
1320                .unwrap_or_else(|_| "Unable to read error body".to_string());
1321            return Err(AmpError::rpc(format!(
1322                "RPC request failed with status: {status} - Body: {error_body}"
1323            )));
1324        }
1325
1326        let rpc_response: RpcResponse<Vec<Unspent>> = response
1327            .json()
1328            .await
1329            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
1330
1331        if let Some(error) = rpc_response.error {
1332            return Err(AmpError::rpc(format!(
1333                "RPC error listing unspent outputs: {} (code: {})",
1334                error.message, error.code
1335            )));
1336        }
1337
1338        let mut utxos = rpc_response.result.unwrap_or_default();
1339
1340        // Enrich UTXOs with scriptpubkey information if missing
1341        for utxo in &mut utxos {
1342            if utxo.scriptpubkey.is_none() {
1343                tracing::debug!(
1344                    "UTXO {}:{} missing scriptpubkey, attempting to derive from address",
1345                    utxo.txid,
1346                    utxo.vout
1347                );
1348
1349                // Try to derive scriptpubkey from the address
1350                if let Ok(address) = elements::Address::from_str(&utxo.address) {
1351                    let script_pubkey = address.script_pubkey();
1352                    utxo.scriptpubkey = Some(hex::encode(script_pubkey.as_bytes()));
1353                    tracing::info!(
1354                        "Derived scriptpubkey for UTXO {}:{} from address {}: {}",
1355                        utxo.txid,
1356                        utxo.vout,
1357                        utxo.address,
1358                        utxo.scriptpubkey.as_ref().unwrap()
1359                    );
1360                } else {
1361                    tracing::error!(
1362                        "Failed to parse address {} for UTXO {}:{}",
1363                        utxo.address,
1364                        utxo.txid,
1365                        utxo.vout
1366                    );
1367
1368                    // Fallback: try to get transaction details
1369                    match self.get_transaction(&utxo.txid).await {
1370                        Ok(tx_detail) => {
1371                            tracing::debug!(
1372                                "Retrieved transaction details for {} as fallback",
1373                                utxo.txid
1374                            );
1375                            // Parse the transaction hex to extract the scriptpubkey for this output
1376                            match hex::decode(&tx_detail.hex) {
1377                                Ok(tx_bytes) => {
1378                                    match elements::Transaction::consensus_decode(&tx_bytes[..]) {
1379                                        Ok(tx) => {
1380                                            if let Some(output) = tx.output.get(utxo.vout as usize)
1381                                            {
1382                                                utxo.scriptpubkey = Some(hex::encode(
1383                                                    output.script_pubkey.as_bytes(),
1384                                                ));
1385                                                tracing::info!("Enriched UTXO {}:{} with scriptpubkey from transaction: {}", 
1386                                                    utxo.txid, utxo.vout, utxo.scriptpubkey.as_ref().unwrap());
1387                                            } else {
1388                                                tracing::error!(
1389                                                    "Output {} not found in transaction {}",
1390                                                    utxo.vout,
1391                                                    utxo.txid
1392                                                );
1393                                            }
1394                                        }
1395                                        Err(e) => {
1396                                            tracing::error!(
1397                                                "Failed to decode transaction {}: {}",
1398                                                utxo.txid,
1399                                                e
1400                                            );
1401                                        }
1402                                    }
1403                                }
1404                                Err(e) => {
1405                                    tracing::error!(
1406                                        "Failed to decode hex for transaction {}: {}",
1407                                        utxo.txid,
1408                                        e
1409                                    );
1410                                }
1411                            }
1412                        }
1413                        Err(e) => {
1414                            tracing::error!(
1415                                "Failed to get transaction details for {}: {}",
1416                                utxo.txid,
1417                                e
1418                            );
1419                        }
1420                    }
1421                }
1422            } else {
1423                tracing::debug!("UTXO {}:{} already has scriptpubkey", utxo.txid, utxo.vout);
1424            }
1425        }
1426
1427        tracing::debug!(
1428            "Found {} unspent outputs for wallet {}",
1429            utxos.len(),
1430            wallet_name
1431        );
1432
1433        // If we're looking for a specific asset and found no UTXOs, provide helpful context
1434        if utxos.is_empty() && asset_id.is_some() {
1435            tracing::warn!(
1436                "No UTXOs found for asset {} in wallet {}. This may indicate:\n\
1437                1. The asset issuance transaction hasn't been confirmed yet\n\
1438                2. The UTXOs have already been spent\n\
1439                3. The wallet doesn't contain the expected addresses",
1440                asset_id.unwrap(),
1441                wallet_name
1442            );
1443        }
1444
1445        Ok(utxos)
1446    }
1447
1448    /// Creates a raw transaction with the specified inputs and outputs
1449    ///
1450    /// # Arguments
1451    /// * `inputs` - Vector of transaction inputs (UTXOs to spend)
1452    /// * `outputs` - Map of addresses to amounts for regular outputs
1453    /// * `assets` - Map of addresses to asset IDs for Liquid-specific outputs
1454    ///
1455    /// # Errors
1456    /// Returns an error if the RPC call fails or transaction creation fails
1457    ///
1458    /// # Examples
1459    /// ```no_run
1460    /// # use amp_rs::{ElementsRpc, model::{TxInput}};
1461    /// # use std::collections::HashMap;
1462    /// # #[tokio::main]
1463    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1464    /// let rpc = ElementsRpc::from_env()?;
1465    /// let inputs = vec![TxInput {
1466    ///     txid: "abc123".to_string(),
1467    ///     vout: 0,
1468    ///     sequence: None,
1469    /// }];
1470    /// let mut outputs = HashMap::new();
1471    /// outputs.insert("address1".to_string(), 100.0);
1472    /// let mut assets = HashMap::new();
1473    /// assets.insert("address1".to_string(), "asset_id".to_string());
1474    /// let raw_tx = rpc.create_raw_transaction(inputs, outputs, assets).await?;
1475    /// # Ok(())
1476    /// # }
1477    /// ```
1478    #[allow(clippy::cognitive_complexity)]
1479    pub async fn create_raw_transaction(
1480        &self,
1481        inputs: Vec<TxInput>,
1482        outputs: std::collections::HashMap<String, f64>,
1483        assets: std::collections::HashMap<String, String>,
1484    ) -> Result<String, AmpError> {
1485        tracing::debug!(
1486            "Creating raw transaction with {} inputs and {} outputs",
1487            inputs.len(),
1488            outputs.len()
1489        );
1490
1491        // Elements RPC createrawtransaction expects:
1492        // createrawtransaction inputs outputs locktime replaceable assets
1493        let params = serde_json::json!([
1494            inputs,  // inputs as TxInput array
1495            outputs, // outputs as address->amount map
1496            0,       // locktime (0 = no locktime)
1497            false,   // replaceable (false = not replaceable)
1498            assets   // assets as address->asset_id map
1499        ]);
1500
1501        // Debug: Log the exact parameters being sent to createrawtransaction
1502        tracing::error!("createrawtransaction parameters:");
1503        tracing::error!(
1504            "  inputs: {}",
1505            serde_json::to_string_pretty(&inputs).unwrap_or_default()
1506        );
1507        tracing::error!(
1508            "  outputs: {}",
1509            serde_json::to_string_pretty(&outputs).unwrap_or_default()
1510        );
1511        tracing::error!(
1512            "  assets: {}",
1513            serde_json::to_string_pretty(&assets).unwrap_or_default()
1514        );
1515
1516        let raw_tx: String = self
1517            .rpc_call("createrawtransaction", params)
1518            .await
1519            .map_err(|e| {
1520                tracing::error!("createrawtransaction RPC call failed: {}", e);
1521                e.with_context("Failed to create raw transaction")
1522            })?;
1523
1524        tracing::debug!("Created raw transaction: {}", raw_tx);
1525        Ok(raw_tx)
1526    }
1527
1528    /// Imports an address into a specific wallet as watch-only
1529    ///
1530    /// # Arguments
1531    /// * `wallet_name` - Name of the wallet to import into
1532    /// * `address` - The address to import
1533    /// * `label` - Optional label for the address
1534    /// * `rescan` - Whether to rescan the blockchain for transactions
1535    ///
1536    /// # Errors
1537    /// Returns an error if the RPC call fails
1538    async fn import_address_to_wallet(
1539        &self,
1540        wallet_name: &str,
1541        address: &str,
1542        label: Option<&str>,
1543        rescan: bool,
1544    ) -> Result<(), AmpError> {
1545        tracing::debug!("Importing address {} into wallet {}", address, wallet_name);
1546
1547        // First load the wallet to ensure it's available
1548        self.load_wallet(wallet_name).await?;
1549
1550        let params = serde_json::json!([address, label.unwrap_or(""), rescan]);
1551
1552        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
1553
1554        let request = RpcRequest {
1555            jsonrpc: "1.0".to_string(),
1556            id: "amp-client".to_string(),
1557            method: "importaddress".to_string(),
1558            params,
1559        };
1560
1561        let response = self
1562            .client
1563            .post(&wallet_url)
1564            .basic_auth(&self.username, Some(&self.password))
1565            .json(&request)
1566            .send()
1567            .await
1568            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
1569
1570        if !response.status().is_success() {
1571            let status = response.status();
1572            let error_body = response
1573                .text()
1574                .await
1575                .unwrap_or_else(|_| "Unable to read error body".to_string());
1576            return Err(AmpError::rpc(format!(
1577                "RPC request failed with status: {status} - Body: {error_body}"
1578            )));
1579        }
1580
1581        let rpc_response: RpcResponse<serde_json::Value> = response
1582            .json()
1583            .await
1584            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
1585
1586        if let Some(error) = rpc_response.error {
1587            // Ignore "already imported" errors
1588            if error.code != -4 {
1589                return Err(AmpError::rpc(format!(
1590                    "RPC error importing address: {} (code: {})",
1591                    error.message, error.code
1592                )));
1593            }
1594        }
1595
1596        tracing::debug!(
1597            "Successfully imported address {} into wallet {}",
1598            address,
1599            wallet_name
1600        );
1601        Ok(())
1602    }
1603
1604    /// Creates a raw transaction using a specific wallet context
1605    ///
1606    /// This method uses the wallet-specific RPC endpoint which is necessary
1607    /// for confidential transactions that require wallet context for blinding keys.
1608    ///
1609    /// # Arguments
1610    /// * `wallet_name` - Name of the wallet to use for transaction creation
1611    /// * `inputs` - Transaction inputs
1612    /// * `outputs` - Map of addresses to amounts
1613    /// * `assets` - Map of addresses to asset IDs
1614    ///
1615    /// # Returns
1616    /// Returns the raw transaction hex
1617    ///
1618    /// # Errors
1619    /// Returns an error if the RPC call fails
1620    #[allow(dead_code)]
1621    #[allow(clippy::cognitive_complexity)]
1622    async fn create_raw_transaction_with_wallet(
1623        &self,
1624        wallet_name: &str,
1625        inputs: Vec<TxInput>,
1626        outputs: std::collections::HashMap<String, f64>,
1627        assets: std::collections::HashMap<String, String>,
1628    ) -> Result<String, AmpError> {
1629        tracing::debug!(
1630            "Creating raw transaction with wallet {} - {} inputs and {} outputs",
1631            wallet_name,
1632            inputs.len(),
1633            outputs.len()
1634        );
1635
1636        // First load the wallet to ensure it's available
1637        self.load_wallet(wallet_name).await?;
1638
1639        // Elements RPC createrawtransaction expects outputs as an array of objects
1640        // Each output object should contain both address, amount, and asset
1641        let mut outputs_array = Vec::new();
1642
1643        for (address, amount) in &outputs {
1644            let asset_id = assets.get(address).ok_or_else(|| {
1645                AmpError::validation(format!("No asset ID found for address {address}"))
1646            })?;
1647
1648            // Convert amount to string with proper precision for Elements
1649            let amount_str = format!("{amount:.8}");
1650
1651            outputs_array.push(serde_json::json!({
1652                address.clone(): amount_str,
1653                "asset": asset_id
1654            }));
1655        }
1656
1657        let params = serde_json::json!([
1658            inputs,        // inputs as TxInput array
1659            outputs_array, // outputs as array of {address: amount, asset: id} objects
1660            0,             // locktime (0 = no locktime)
1661            false,         // replaceable (false = not replaceable)
1662        ]);
1663
1664        // Debug: Log the exact parameters being sent to createrawtransaction
1665        tracing::error!("createrawtransaction parameters (wallet-specific, corrected format):");
1666        tracing::error!("  wallet: {}", wallet_name);
1667        tracing::error!(
1668            "  inputs: {}",
1669            serde_json::to_string_pretty(&inputs).unwrap_or_default()
1670        );
1671        tracing::error!(
1672            "  outputs_array: {}",
1673            serde_json::to_string_pretty(&outputs_array).unwrap_or_default()
1674        );
1675
1676        // Use the wallet-specific RPC endpoint
1677        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
1678
1679        let request = RpcRequest {
1680            jsonrpc: "1.0".to_string(),
1681            id: "amp-client".to_string(),
1682            method: "createrawtransaction".to_string(),
1683            params,
1684        };
1685
1686        let response = self
1687            .client
1688            .post(&wallet_url)
1689            .basic_auth(&self.username, Some(&self.password))
1690            .json(&request)
1691            .send()
1692            .await
1693            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
1694
1695        if !response.status().is_success() {
1696            let status = response.status();
1697            let error_body = response
1698                .text()
1699                .await
1700                .unwrap_or_else(|_| "Unable to read error body".to_string());
1701            return Err(AmpError::rpc(format!(
1702                "RPC request failed with status: {status} - Body: {error_body}"
1703            )));
1704        }
1705
1706        let rpc_response: RpcResponse<String> = response
1707            .json()
1708            .await
1709            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
1710
1711        if let Some(error) = rpc_response.error {
1712            return Err(AmpError::rpc(format!(
1713                "RPC error creating raw transaction: {} (code: {})",
1714                error.message, error.code
1715            )));
1716        }
1717
1718        let raw_tx = rpc_response
1719            .result
1720            .ok_or_else(|| AmpError::rpc("No raw transaction returned".to_string()))?;
1721
1722        tracing::debug!(
1723            "Created raw transaction with wallet {}: {}",
1724            wallet_name,
1725            raw_tx
1726        );
1727        Ok(raw_tx)
1728    }
1729
1730    /// Imports an address into a specific wallet as watch-only
1731    ///
1732    /// # Arguments
1733    /// * `wallet_name` - Name of the wallet
1734    /// * `address` - The address to import
1735    /// * `label` - Optional label for the address
1736    /// * `rescan` - Optional whether to rescan the blockchain (default: false)
1737    ///
1738    /// # Errors
1739    /// Returns an error if the RPC call fails
1740    ///
1741    /// # Examples
1742    /// ```no_run
1743    /// # use amp_rs::ElementsRpc;
1744    /// # #[tokio::main]
1745    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1746    /// let rpc = ElementsRpc::from_env()?;
1747    /// rpc.import_address("my_wallet", "vjU8L4dKa1XyyVcPqKBbTgjT1tRC7qYp5VJGwndZSCFk4ntpWey1pQe6hcSGDMVurr9CsZ21EGsqGjWA", Some("test_address"), Some(false)).await?;
1748    /// # Ok(())
1749    /// # }
1750    /// ```
1751    pub async fn import_address(
1752        &self,
1753        wallet_name: &str,
1754        address: &str,
1755        label: Option<&str>,
1756        rescan: Option<bool>,
1757    ) -> Result<(), AmpError> {
1758        let rescan_value = rescan.unwrap_or(false);
1759        tracing::debug!(
1760            "Importing address: {} into wallet: {} with label: {:?}, rescan: {}",
1761            address,
1762            wallet_name,
1763            label,
1764            rescan_value
1765        );
1766
1767        let params = serde_json::json!([address, label.unwrap_or(""), rescan_value]);
1768
1769        // importaddress returns null on success
1770        let request = RpcRequest {
1771            jsonrpc: "1.0".to_string(),
1772            id: "amp-client".to_string(),
1773            method: "importaddress".to_string(),
1774            params,
1775        };
1776
1777        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
1778
1779        let response = self
1780            .client
1781            .post(&wallet_url)
1782            .basic_auth(&self.username, Some(&self.password))
1783            .json(&request)
1784            .send()
1785            .await
1786            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
1787
1788        if !response.status().is_success() {
1789            return Err(AmpError::rpc(format!(
1790                "RPC request failed with status: {}",
1791                response.status()
1792            )));
1793        }
1794
1795        let rpc_response: RpcResponse<serde_json::Value> = response
1796            .json()
1797            .await
1798            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
1799
1800        if let Some(error) = rpc_response.error {
1801            return Err(AmpError::rpc(format!(
1802                "RPC error {}: {}",
1803                error.code, error.message
1804            )));
1805        }
1806
1807        tracing::debug!(
1808            "Successfully imported address: {} into wallet: {}",
1809            address,
1810            wallet_name
1811        );
1812        Ok(())
1813    }
1814
1815    /// Rescans the blockchain for a wallet
1816    ///
1817    /// # Arguments
1818    /// * `wallet_name` - Name of the wallet to rescan
1819    /// * `start_height` - Optional start height for rescan (default: 0)
1820    ///
1821    /// # Errors
1822    /// Returns an error if the RPC call fails
1823    ///
1824    /// # Examples
1825    /// ```no_run
1826    /// # use amp_rs::ElementsRpc;
1827    /// # #[tokio::main]
1828    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1829    /// let rpc = ElementsRpc::from_env()?;
1830    /// let result = rpc.rescan_blockchain("my_wallet", None).await?;
1831    /// # Ok(())
1832    /// # }
1833    /// ```
1834    pub async fn rescan_blockchain(
1835        &self,
1836        wallet_name: &str,
1837        start_height: Option<u64>,
1838    ) -> Result<serde_json::Value, AmpError> {
1839        tracing::debug!("Rescanning blockchain for wallet: {}", wallet_name);
1840
1841        let params = start_height.map_or_else(
1842            || serde_json::json!([]),
1843            |height| serde_json::json!([height]),
1844        );
1845
1846        let request = RpcRequest {
1847            jsonrpc: "1.0".to_string(),
1848            id: "amp-client".to_string(),
1849            method: "rescanblockchain".to_string(),
1850            params,
1851        };
1852
1853        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
1854
1855        let response = self
1856            .client
1857            .post(&wallet_url)
1858            .basic_auth(&self.username, Some(&self.password))
1859            .json(&request)
1860            .send()
1861            .await
1862            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
1863
1864        if !response.status().is_success() {
1865            return Err(AmpError::rpc(format!(
1866                "RPC request failed with status: {}",
1867                response.status()
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            return Err(AmpError::rpc(format!(
1878                "RPC error rescanning blockchain: {} (code: {})",
1879                error.message, error.code
1880            )));
1881        }
1882
1883        let result = rpc_response
1884            .result
1885            .ok_or_else(|| AmpError::rpc("No result returned from rescanblockchain".to_string()))?;
1886
1887        tracing::debug!(
1888            "Successfully rescanned blockchain for wallet: {}",
1889            wallet_name
1890        );
1891        Ok(result)
1892    }
1893
1894    /// Creates or loads a wallet
1895    ///
1896    /// # Arguments
1897    /// * `wallet_name` - Name of the wallet to create or load
1898    /// * `disable_private_keys` - Whether to disable private keys (watch-only wallet)
1899    ///
1900    /// # Errors
1901    /// Returns an error if the RPC call fails
1902    ///
1903    /// # Examples
1904    /// ```no_run
1905    /// # use amp_rs::ElementsRpc;
1906    /// # #[tokio::main]
1907    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1908    /// let rpc = ElementsRpc::from_env()?;
1909    /// rpc.create_wallet("test_wallet", true).await?;
1910    /// # Ok(())
1911    /// # }
1912    /// ```
1913    pub async fn create_wallet(
1914        &self,
1915        wallet_name: &str,
1916        disable_private_keys: bool,
1917    ) -> Result<(), AmpError> {
1918        tracing::debug!(
1919            "Creating wallet: {} with disable_private_keys: {}",
1920            wallet_name,
1921            disable_private_keys
1922        );
1923
1924        let params = serde_json::json!([wallet_name, disable_private_keys]);
1925
1926        let request = RpcRequest {
1927            jsonrpc: "1.0".to_string(),
1928            id: "amp-client".to_string(),
1929            method: "createwallet".to_string(),
1930            params,
1931        };
1932
1933        let response = self
1934            .client
1935            .post(&self.base_url)
1936            .basic_auth(&self.username, Some(&self.password))
1937            .json(&request)
1938            .send()
1939            .await
1940            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
1941
1942        if !response.status().is_success() {
1943            return Err(AmpError::rpc(format!(
1944                "RPC request failed with status: {}",
1945                response.status()
1946            )));
1947        }
1948
1949        let rpc_response: RpcResponse<serde_json::Value> = response
1950            .json()
1951            .await
1952            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
1953
1954        if let Some(error) = rpc_response.error {
1955            // Ignore "wallet already exists" error
1956            if error.code != -4 {
1957                return Err(AmpError::rpc(format!(
1958                    "RPC error {}: {}",
1959                    error.code, error.message
1960                )));
1961            }
1962            tracing::debug!("Wallet {} already exists", wallet_name);
1963        } else {
1964            tracing::debug!("Successfully created wallet: {}", wallet_name);
1965        }
1966
1967        Ok(())
1968    }
1969
1970    /// Loads an existing wallet
1971    ///
1972    /// # Arguments
1973    /// * `wallet_name` - Name of the wallet to load
1974    ///
1975    /// # Errors
1976    /// Returns an error if the RPC call fails
1977    ///
1978    /// # Examples
1979    /// ```no_run
1980    /// # use amp_rs::ElementsRpc;
1981    /// # #[tokio::main]
1982    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1983    /// let rpc = ElementsRpc::from_env()?;
1984    /// rpc.load_wallet("test_wallet").await?;
1985    /// # Ok(())
1986    /// # }
1987    /// ```
1988    #[allow(clippy::cognitive_complexity)]
1989    pub async fn load_wallet(&self, wallet_name: &str) -> Result<(), AmpError> {
1990        tracing::debug!("Loading wallet: {}", wallet_name);
1991
1992        let params = serde_json::json!([wallet_name]);
1993
1994        let request = RpcRequest {
1995            jsonrpc: "1.0".to_string(),
1996            id: "amp-client".to_string(),
1997            method: "loadwallet".to_string(),
1998            params,
1999        };
2000
2001        let response = self
2002            .client
2003            .post(&self.base_url)
2004            .basic_auth(&self.username, Some(&self.password))
2005            .json(&request)
2006            .send()
2007            .await
2008            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
2009
2010        if !response.status().is_success() {
2011            let status = response.status();
2012            let error_body = response
2013                .text()
2014                .await
2015                .unwrap_or_else(|_| "Unable to read error body".to_string());
2016            tracing::debug!(
2017                "Load wallet failed with status: {} - Body: {}",
2018                status,
2019                error_body
2020            );
2021
2022            // For wallet loading, we want to be more permissive with errors
2023            // since the wallet might already be loaded
2024            if status == 500 && error_body.contains("already loaded") {
2025                tracing::debug!(
2026                    "Wallet {} appears to already be loaded (500 error)",
2027                    wallet_name
2028                );
2029                return Ok(());
2030            }
2031
2032            return Err(AmpError::rpc(format!(
2033                "RPC request failed with status: {status} - Body: {error_body}"
2034            )));
2035        }
2036
2037        let rpc_response: RpcResponse<serde_json::Value> = response
2038            .json()
2039            .await
2040            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
2041
2042        if let Some(error) = rpc_response.error {
2043            // Ignore "wallet already loaded" error
2044            if error.code != -35 {
2045                return Err(AmpError::rpc(format!(
2046                    "RPC error {}: {}",
2047                    error.code, error.message
2048                )));
2049            }
2050            tracing::debug!("Wallet {} already loaded", wallet_name);
2051        } else {
2052            tracing::debug!("Successfully loaded wallet: {}", wallet_name);
2053        }
2054
2055        Ok(())
2056    }
2057
2058    /// Unloads a wallet
2059    ///
2060    /// # Arguments
2061    /// * `wallet_name` - Name of the wallet to unload
2062    ///
2063    /// # Errors
2064    /// Returns an error if the RPC call fails
2065    ///
2066    /// # Examples
2067    /// ```no_run
2068    /// # use amp_rs::ElementsRpc;
2069    /// # #[tokio::main]
2070    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2071    /// let rpc = ElementsRpc::from_env()?;
2072    /// rpc.unload_wallet("test_wallet").await?;
2073    /// # Ok(())
2074    /// # }
2075    /// ```
2076    pub async fn unload_wallet(&self, wallet_name: &str) -> Result<(), AmpError> {
2077        tracing::debug!("Unloading wallet: {}", wallet_name);
2078
2079        let params = serde_json::json!([wallet_name]);
2080
2081        let request = RpcRequest {
2082            jsonrpc: "1.0".to_string(),
2083            id: "amp-client".to_string(),
2084            method: "unloadwallet".to_string(),
2085            params,
2086        };
2087
2088        let response = self
2089            .client
2090            .post(&self.base_url)
2091            .basic_auth(&self.username, Some(&self.password))
2092            .json(&request)
2093            .send()
2094            .await
2095            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
2096
2097        if !response.status().is_success() {
2098            return Err(AmpError::rpc(format!(
2099                "RPC request failed with status: {}",
2100                response.status()
2101            )));
2102        }
2103
2104        let rpc_response: RpcResponse<serde_json::Value> = response
2105            .json()
2106            .await
2107            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
2108
2109        if let Some(error) = rpc_response.error {
2110            return Err(AmpError::rpc(format!(
2111                "RPC error {}: {}",
2112                error.code, error.message
2113            )));
2114        }
2115
2116        tracing::debug!("Successfully unloaded wallet: {}", wallet_name);
2117        Ok(())
2118    }
2119
2120    /// Lists all available wallets
2121    ///
2122    /// # Errors
2123    /// Returns an error if the RPC call fails
2124    ///
2125    /// # Examples
2126    /// ```no_run
2127    /// # use amp_rs::ElementsRpc;
2128    /// # #[tokio::main]
2129    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2130    /// let rpc = ElementsRpc::from_env()?;
2131    /// let wallets = rpc.list_wallets().await?;
2132    /// println!("Available wallets: {:?}", wallets);
2133    /// # Ok(())
2134    /// # }
2135    /// ```
2136    pub async fn list_wallets(&self) -> Result<Vec<String>, AmpError> {
2137        tracing::debug!("Listing available wallets");
2138
2139        let params = serde_json::json!([]);
2140
2141        let wallets: Vec<String> = self
2142            .rpc_call("listwallets", params)
2143            .await
2144            .map_err(|e| e.with_context("Failed to list wallets"))?;
2145
2146        tracing::debug!("Found {} wallets", wallets.len());
2147        Ok(wallets)
2148    }
2149
2150    /// Sets up a watch-only wallet with the given address
2151    ///
2152    /// This is a convenience method that creates a watch-only wallet and imports the address
2153    ///
2154    /// # Arguments
2155    /// * `wallet_name` - Name of the wallet to create
2156    /// * `address` - Address to import as watch-only
2157    /// * `label` - Optional label for the address
2158    ///
2159    /// # Errors
2160    /// Returns an error if wallet creation or address import fails
2161    ///
2162    /// # Examples
2163    /// ```no_run
2164    /// # use amp_rs::ElementsRpc;
2165    /// # #[tokio::main]
2166    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2167    /// let rpc = ElementsRpc::from_env()?;
2168    /// rpc.setup_watch_only_wallet("test_wallet", "vjU8L4dKa1XyyVcPqKBbTgjT1tRC7qYp5VJGwndZSCFk4ntpWey1pQe6hcSGDMVurr9CsZ21EGsqGjWA", Some("treasury")).await?;
2169    /// # Ok(())
2170    /// # }
2171    /// ```
2172    #[allow(clippy::cognitive_complexity)]
2173    pub async fn setup_watch_only_wallet(
2174        &self,
2175        wallet_name: &str,
2176        address: &str,
2177        label: Option<&str>,
2178    ) -> Result<(), AmpError> {
2179        tracing::info!(
2180            "Setting up watch-only wallet '{}' with address: {}",
2181            wallet_name,
2182            address
2183        );
2184
2185        // Try the full wallet setup approach first
2186        match self
2187            .setup_wallet_with_address(wallet_name, address, label)
2188            .await
2189        {
2190            Ok(()) => {
2191                tracing::info!(
2192                    "Successfully set up watch-only wallet '{}' with address: {}",
2193                    wallet_name,
2194                    address
2195                );
2196                return Ok(());
2197            }
2198            Err(e) => {
2199                tracing::warn!(
2200                    "Full wallet setup failed: {}, trying direct address import",
2201                    e
2202                );
2203            }
2204        }
2205
2206        // Fallback: Try to import the address directly without wallet operations
2207        match self.import_address_direct(address, label) {
2208            Ok(()) => {
2209                tracing::info!("Successfully imported address directly: {}", address);
2210                Ok(())
2211            }
2212            Err(e) => {
2213                tracing::error!(
2214                    "Both wallet setup and direct import failed for address: {}",
2215                    address
2216                );
2217                Err(AmpError::rpc(format!(
2218                    "Failed to set up watch-only wallet or import address: wallet setup error: {e}, direct import error: {e}"
2219                )))
2220            }
2221        }
2222    }
2223
2224    /// Attempts to set up a wallet with address using the standard approach
2225    async fn setup_wallet_with_address(
2226        &self,
2227        wallet_name: &str,
2228        address: &str,
2229        label: Option<&str>,
2230    ) -> Result<(), AmpError> {
2231        // Try to create the wallet (will ignore if it already exists)
2232        self.create_wallet(wallet_name, true).await?;
2233
2234        // Try to load the wallet (will ignore if already loaded)
2235        self.load_wallet(wallet_name).await?;
2236
2237        // Import the address without rescanning (for faster setup)
2238        self.import_address(wallet_name, address, label, Some(false))
2239            .await?;
2240
2241        Ok(())
2242    }
2243
2244    /// Attempts to import an address directly without wallet operations (uses default wallet)
2245    #[allow(clippy::unused_self)]
2246    fn import_address_direct(&self, address: &str, _label: Option<&str>) -> Result<(), AmpError> {
2247        tracing::debug!("Attempting direct address import for: {}", address);
2248
2249        // This is a fallback method - we'll use empty string for wallet name to use default behavior
2250        // Note: This may not work as expected with the new signature, but kept for compatibility
2251        Err(AmpError::rpc(
2252            "Direct address import not supported with wallet-specific import_address".to_string(),
2253        ))
2254    }
2255
2256    /// Broadcasts a signed raw transaction to the network
2257    ///
2258    /// # Arguments
2259    /// * `hex` - The signed transaction in hexadecimal format
2260    ///
2261    /// # Errors
2262    /// Returns an error if the RPC call fails or transaction broadcast fails
2263    ///
2264    /// # Examples
2265    /// ```no_run
2266    /// # use amp_rs::ElementsRpc;
2267    /// # #[tokio::main]
2268    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2269    /// let rpc = ElementsRpc::from_env()?;
2270    /// let signed_tx_hex = "0200000000..."; // Signed transaction hex
2271    /// let txid = rpc.send_raw_transaction(signed_tx_hex).await?;
2272    /// println!("Transaction broadcast with ID: {}", txid);
2273    /// # Ok(())
2274    /// # }
2275    /// ```
2276    pub async fn send_raw_transaction(&self, hex: &str) -> Result<String, AmpError> {
2277        tracing::debug!(
2278            "Broadcasting raw transaction: {}",
2279            &hex[..std::cmp::min(hex.len(), 64)]
2280        );
2281
2282        let params = serde_json::json!([hex]);
2283
2284        let txid: String = self
2285            .rpc_call("sendrawtransaction", params)
2286            .await
2287            .map_err(|e| {
2288                tracing::error!("Raw transaction broadcast failed: {}", e);
2289                tracing::error!("Transaction hex (first 200 chars): {}", &hex[..std::cmp::min(hex.len(), 200)]);
2290
2291                // Provide specific guidance for blinding-related errors
2292                if e.to_string().contains("bad-txns-in-ne-out") || e.to_string().contains("value in != value out") {
2293                    AmpError::rpc(format!(
2294                        "Transaction broadcast failed due to confidential transaction blinding error. \
2295                        This indicates that the blinding factors don't balance properly. \
2296                        Possible solutions:\n\
2297                        1. Ensure all addresses have proper blinding keys in the wallet\n\
2298                        2. Verify that blindrawtransaction was called before signing\n\
2299                        3. Check that UTXO blinding factors match between Elements and LWK\n\
2300                        4. Try using unconfidential addresses for testing\n\
2301                        Original error: {e}"
2302                    ))
2303                } else {
2304                    e.with_context("Failed to broadcast raw transaction")
2305                }
2306            })?;
2307
2308        tracing::info!("Successfully broadcast transaction with ID: {}", txid);
2309        Ok(txid)
2310    }
2311
2312    /// Retrieves detailed information about a transaction
2313    ///
2314    /// # Arguments
2315    /// * `txid` - The transaction ID to retrieve
2316    ///
2317    /// # Errors
2318    /// Returns an error if the RPC call fails or transaction is not found
2319    ///
2320    /// # Examples
2321    /// ```no_run
2322    /// # use amp_rs::ElementsRpc;
2323    /// # #[tokio::main]
2324    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2325    /// let rpc = ElementsRpc::from_env()?;
2326    /// let tx_detail = rpc.get_transaction("abc123...").await?;
2327    /// println!("Transaction has {} confirmations", tx_detail.confirmations);
2328    /// # Ok(())
2329    /// # }
2330    /// ```
2331    pub async fn get_transaction(&self, txid: &str) -> Result<TransactionDetail, AmpError> {
2332        tracing::debug!("Retrieving transaction details for: {}", txid);
2333
2334        let params = serde_json::json!([txid, true]); // true for verbose output
2335
2336        let tx_detail: TransactionDetail = self
2337            .rpc_call("gettransaction", params)
2338            .await
2339            .map_err(|e| e.with_context(format!("Failed to get transaction details for {txid}")))?;
2340
2341        tracing::debug!(
2342            "Retrieved transaction {} with {} confirmations",
2343            txid,
2344            tx_detail.confirmations
2345        );
2346
2347        Ok(tx_detail)
2348    }
2349
2350    /// Sends multiple outputs to multiple addresses using Elements' sendmany RPC
2351    ///
2352    /// This method uses Elements' built-in sendmany command which properly handles
2353    /// confidential transactions and blinding. This is the recommended approach for
2354    /// asset distribution as it avoids manual transaction construction issues.
2355    ///
2356    /// # Arguments
2357    /// * `wallet_name` - Name of the Elements wallet to use
2358    /// * `address_amounts` - Map of addresses to amounts to send
2359    /// * `asset_amounts` - Map of addresses to asset IDs for each output
2360    /// * `min_conf` - Minimum confirmations for inputs (default: 1)
2361    /// * `comment` - Optional transaction comment
2362    /// * `subtract_fee_from` - Optional addresses to subtract fees from
2363    /// * `replaceable` - Whether transaction is replaceable (default: false)
2364    /// * `conf_target` - Confirmation target for fee estimation (default: 1)
2365    /// * `estimate_mode` - Fee estimation mode (default: "UNSET")
2366    ///
2367    /// # Returns
2368    /// Returns the transaction ID of the sent transaction
2369    ///
2370    /// # Errors
2371    /// Returns an error if the RPC call fails or transaction creation fails
2372    ///
2373    /// # Examples
2374    /// ```no_run
2375    /// # use amp_rs::ElementsRpc;
2376    /// # use std::collections::HashMap;
2377    /// # #[tokio::main]
2378    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2379    /// let rpc = ElementsRpc::from_env()?;
2380    ///
2381    /// let mut address_amounts = HashMap::new();
2382    /// address_amounts.insert("address1".to_string(), 100.0);
2383    /// address_amounts.insert("address2".to_string(), 50.0);
2384    ///
2385    /// let mut asset_amounts = HashMap::new();
2386    /// asset_amounts.insert("address1".to_string(), "asset_id_hex".to_string());
2387    /// asset_amounts.insert("address2".to_string(), "asset_id_hex".to_string());
2388    ///
2389    /// let txid = rpc.sendmany("wallet_name", address_amounts, asset_amounts, None, None, None, None, None, None).await?;
2390    /// println!("Transaction sent with ID: {}", txid);
2391    /// # Ok(())
2392    /// # }
2393    /// ```
2394    #[allow(clippy::too_many_arguments, clippy::cognitive_complexity)]
2395    pub async fn sendmany(
2396        &self,
2397        wallet_name: &str,
2398        address_amounts: std::collections::HashMap<String, f64>,
2399        asset_amounts: std::collections::HashMap<String, String>,
2400        min_conf: Option<u32>,
2401        comment: Option<&str>,
2402        subtract_fee_from: Option<Vec<String>>,
2403        replaceable: Option<bool>,
2404        conf_target: Option<u32>,
2405        estimate_mode: Option<&str>,
2406    ) -> Result<String, AmpError> {
2407        tracing::debug!(
2408            "Sending to {} addresses using sendmany for wallet {}",
2409            address_amounts.len(),
2410            wallet_name
2411        );
2412
2413        // First load the wallet to ensure it's available
2414        self.load_wallet(wallet_name).await?;
2415
2416        // Elements sendmany parameters:
2417        // 1. dummy (empty string for compatibility)
2418        // 2. amounts (map of address -> amount)
2419        // 3. minconf (minimum confirmations, default 1)
2420        // 4. comment (optional comment)
2421        // 5. subtractfeefrom (array of addresses to subtract fee from)
2422        // 6. replaceable (boolean, default false)
2423        // 7. conf_target (confirmation target for fee estimation)
2424        // 8. estimate_mode (fee estimation mode)
2425        // 9. assetlabel (map of address -> asset_id for multi-asset sends)
2426        let params = serde_json::json!([
2427            "",                                    // dummy (required for compatibility)
2428            address_amounts,                       // amounts map
2429            min_conf.unwrap_or(1),                 // minconf
2430            comment.unwrap_or(""),                 // comment
2431            subtract_fee_from.unwrap_or_default(), // subtractfeefrom
2432            replaceable.unwrap_or(false),          // replaceable
2433            conf_target.unwrap_or(1),              // conf_target
2434            estimate_mode.unwrap_or("UNSET"),      // estimate_mode
2435            asset_amounts                          // assetlabel (asset map)
2436        ]);
2437
2438        // Use the wallet-specific RPC endpoint
2439        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
2440
2441        let request = RpcRequest {
2442            jsonrpc: "1.0".to_string(),
2443            id: "amp-client".to_string(),
2444            method: "sendmany".to_string(),
2445            params,
2446        };
2447
2448        tracing::debug!("Sendmany request parameters:");
2449        tracing::debug!("  wallet: {}", wallet_name);
2450        tracing::debug!("  address_amounts: {:?}", address_amounts);
2451        tracing::debug!("  asset_amounts: {:?}", asset_amounts);
2452
2453        let response = self
2454            .client
2455            .post(&wallet_url)
2456            .basic_auth(&self.username, Some(&self.password))
2457            .json(&request)
2458            .send()
2459            .await
2460            .map_err(|e| AmpError::rpc(format!("Failed to send sendmany RPC request: {e}")))?;
2461
2462        if !response.status().is_success() {
2463            let status = response.status();
2464            let error_body = response
2465                .text()
2466                .await
2467                .unwrap_or_else(|_| "Unable to read error body".to_string());
2468            return Err(AmpError::rpc(format!(
2469                "Sendmany RPC request failed with status: {status} - Body: {error_body}"
2470            )));
2471        }
2472
2473        let rpc_response: RpcResponse<String> = response
2474            .json()
2475            .await
2476            .map_err(|e| AmpError::rpc(format!("Failed to parse sendmany RPC response: {e}")))?;
2477
2478        if let Some(error) = rpc_response.error {
2479            return Err(AmpError::rpc(format!(
2480                "Sendmany RPC error: {} (code: {})",
2481                error.message, error.code
2482            )));
2483        }
2484
2485        let txid = rpc_response.result.unwrap_or_default();
2486        tracing::info!("Successfully sent transaction with sendmany: {}", txid);
2487        Ok(txid)
2488    }
2489
2490    /// Waits for blockchain confirmations with configurable timeout
2491    ///
2492    /// This method polls the blockchain every 15 seconds to check for transaction confirmations.
2493    /// It waits for a minimum number of confirmations (default 2) before returning successfully.
2494    /// The method includes a configurable timeout to prevent indefinite waiting.
2495    ///
2496    /// # Arguments
2497    /// * `txid` - The transaction ID to monitor for confirmations
2498    /// * `min_confirmations` - Minimum number of confirmations required (default: 2)
2499    /// * `timeout_minutes` - Timeout in minutes (default: 10)
2500    ///
2501    /// # Returns
2502    /// Returns the final `TransactionDetail` when sufficient confirmations are reached
2503    ///
2504    /// # Errors
2505    /// Returns `AmpError::Timeout` if the timeout is exceeded before confirmations are received
2506    /// Returns `AmpError::Rpc` if there are issues communicating with the Elements node
2507    ///
2508    /// # Examples
2509    /// ```no_run
2510    /// # use amp_rs::ElementsRpc;
2511    /// # #[tokio::main]
2512    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2513    /// let rpc = ElementsRpc::from_env()?;
2514    /// let tx_detail = rpc.wait_for_confirmations("abc123...", Some(2), Some(10)).await?;
2515    /// println!("Transaction confirmed with {} confirmations", tx_detail.confirmations);
2516    /// # Ok(())
2517    /// # }
2518    /// ```
2519    pub async fn wait_for_confirmations(
2520        &self,
2521        txid: &str,
2522        min_confirmations: Option<u32>,
2523        timeout_minutes: Option<u64>,
2524    ) -> Result<TransactionDetail, AmpError> {
2525        self.wait_for_confirmations_with_interval(txid, min_confirmations, timeout_minutes, None)
2526            .await
2527    }
2528
2529    /// Internal method for waiting for confirmations with configurable poll interval
2530    /// This is primarily used for testing to avoid long waits
2531    ///
2532    /// # Errors
2533    ///
2534    /// Returns an error if:
2535    /// - The timeout is exceeded before confirmations are received
2536    /// - There are issues communicating with the Elements node
2537    /// - The transaction cannot be found or is invalid
2538    #[allow(clippy::cognitive_complexity)]
2539    pub async fn wait_for_confirmations_with_interval(
2540        &self,
2541        txid: &str,
2542        min_confirmations: Option<u32>,
2543        timeout_minutes: Option<u64>,
2544        poll_interval_secs: Option<u64>,
2545    ) -> Result<TransactionDetail, AmpError> {
2546        let min_confirmations = min_confirmations.unwrap_or(2);
2547        let timeout_minutes = timeout_minutes.unwrap_or(10);
2548        let timeout_duration = if timeout_minutes == 0 {
2549            std::time::Duration::from_secs(3) // Minimum 3 seconds for testing
2550        } else {
2551            std::time::Duration::from_secs(timeout_minutes * 60)
2552        };
2553        let poll_interval = std::time::Duration::from_secs(poll_interval_secs.unwrap_or(15));
2554
2555        tracing::info!(
2556            "Starting confirmation monitoring for transaction {} (min_confirmations: {}, timeout: {} minutes)",
2557            txid,
2558            min_confirmations,
2559            timeout_minutes
2560        );
2561
2562        let start_time = std::time::Instant::now();
2563
2564        loop {
2565            // Check if we've exceeded the timeout
2566            if start_time.elapsed() >= timeout_duration {
2567                let error_msg = format!(
2568                    "Timeout waiting for confirmations after {timeout_minutes} minutes. Transaction ID: {txid}. \
2569                    You can retry confirmation by calling the confirmation API with this txid."
2570                );
2571                tracing::error!("{}", error_msg);
2572                return Err(AmpError::Timeout(error_msg));
2573            }
2574
2575            // Get current transaction details
2576            match self.get_transaction(txid).await {
2577                Ok(tx_detail) => {
2578                    tracing::debug!(
2579                        "Transaction {} has {} confirmations (need {})",
2580                        txid,
2581                        tx_detail.confirmations,
2582                        min_confirmations
2583                    );
2584
2585                    if tx_detail.confirmations >= min_confirmations {
2586                        tracing::info!(
2587                            "Transaction {} confirmed with {} confirmations",
2588                            txid,
2589                            tx_detail.confirmations
2590                        );
2591                        return Ok(tx_detail);
2592                    }
2593
2594                    // Log progress every few polls to avoid spam
2595                    if start_time.elapsed().as_secs() % 60 < 15 {
2596                        tracing::info!(
2597                            "Waiting for confirmations: {}/{} (elapsed: {}s)",
2598                            tx_detail.confirmations,
2599                            min_confirmations,
2600                            start_time.elapsed().as_secs()
2601                        );
2602                    }
2603                }
2604                Err(e) => {
2605                    tracing::warn!(
2606                        "Failed to get transaction details for {}: {}. Retrying in {} seconds...",
2607                        txid,
2608                        e,
2609                        poll_interval.as_secs()
2610                    );
2611                    // Continue polling even if individual calls fail, as the transaction
2612                    // might not be visible immediately after broadcasting
2613                }
2614            }
2615
2616            // Wait before next poll
2617            tokio::time::sleep(poll_interval).await;
2618        }
2619    }
2620
2621    /// Selects appropriate UTXOs to cover the required amount plus fees
2622    ///
2623    /// This method implements a simple UTXO selection algorithm that:
2624    /// 1. Filters UTXOs by asset ID and spendability
2625    /// 2. Sorts UTXOs by amount (largest first) for efficiency
2626    /// 3. Selects UTXOs until the target amount plus estimated fees is covered
2627    ///
2628    /// # Arguments
2629    /// * `asset_id` - The asset ID to select UTXOs for
2630    /// * `target_amount` - The total amount needed for distribution
2631    /// * `estimated_fee` - Estimated transaction fee in the same asset
2632    ///
2633    /// # Returns
2634    /// Returns a tuple of (`selected_utxos`, `total_selected_amount`)
2635    ///
2636    /// # Errors
2637    /// Returns an error if insufficient UTXOs are available or RPC calls fail
2638    ///
2639    /// # Examples
2640    /// ```no_run
2641    /// # use amp_rs::ElementsRpc;
2642    /// # #[tokio::main]
2643    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2644    /// let rpc = ElementsRpc::from_env()?;
2645    /// let (selected_utxos, total_amount) = rpc.select_utxos_for_amount(
2646    ///     "wallet_name",
2647    ///     "asset_id_hex",
2648    ///     150.0,
2649    ///     0.001
2650    /// ).await?;
2651    /// println!("Selected {} UTXOs totaling {}", selected_utxos.len(), total_amount);
2652    /// # Ok(())
2653    /// # }
2654    /// ```
2655    pub async fn select_utxos_for_amount(
2656        &self,
2657        wallet_name: &str,
2658        asset_id: &str,
2659        target_amount: f64,
2660        estimated_fee: f64,
2661    ) -> Result<(Vec<Unspent>, f64), AmpError> {
2662        tracing::debug!(
2663            "Selecting UTXOs for asset {} from wallet {} - target: {}, fee: {}",
2664            asset_id,
2665            wallet_name,
2666            target_amount,
2667            estimated_fee
2668        );
2669
2670        // Get all UTXOs for this asset from the specified wallet
2671        let mut utxos = self
2672            .list_unspent_for_wallet(wallet_name, Some(asset_id))
2673            .await?;
2674
2675        // Filter for spendable UTXOs only
2676        utxos.retain(|utxo| utxo.spendable && utxo.asset == asset_id);
2677
2678        if utxos.is_empty() {
2679            return Err(AmpError::validation(format!(
2680                "No spendable UTXOs found for asset {asset_id}. \
2681                This typically means:\n\
2682                1. The treasury address is not imported in the Elements node as a watch-only address\n\
2683                2. The asset issuance transaction hasn't been confirmed yet\n\
2684                3. The UTXOs have already been spent\n\
2685                \n\
2686                To fix this:\n\
2687                - Ensure the treasury address is imported: `elements-cli importaddress <treasury_address> treasury false`\n\
2688                - Wait for the asset issuance transaction to be confirmed\n\
2689                - Check that the treasury address matches the one used for asset issuance"
2690            )));
2691        }
2692
2693        // Sort UTXOs by amount (largest first) for efficient selection
2694        utxos.sort_by(|a, b| {
2695            b.amount
2696                .partial_cmp(&a.amount)
2697                .unwrap_or(std::cmp::Ordering::Equal)
2698        });
2699
2700        let required_amount = target_amount + estimated_fee;
2701        let mut selected_utxos = Vec::new();
2702        let mut total_selected = 0.0;
2703
2704        // Select UTXOs until we have enough to cover the required amount
2705        for utxo in utxos {
2706            selected_utxos.push(utxo.clone());
2707            total_selected += utxo.amount;
2708
2709            if total_selected >= required_amount {
2710                break;
2711            }
2712        }
2713
2714        // Check if we have sufficient funds
2715        if total_selected < required_amount {
2716            return Err(AmpError::validation(format!(
2717                "Insufficient UTXOs: need {required_amount}, have {total_selected} (target: {target_amount}, fee: {estimated_fee})"
2718            )));
2719        }
2720
2721        tracing::info!(
2722            "Selected {} UTXOs totaling {} for target {} + fee {}",
2723            selected_utxos.len(),
2724            total_selected,
2725            target_amount,
2726            estimated_fee
2727        );
2728
2729        Ok((selected_utxos, total_selected))
2730    }
2731
2732    /// Builds a raw transaction for asset distribution with proper change handling
2733    ///
2734    /// This method orchestrates the complete transaction building process:
2735    /// 1. Selects appropriate UTXOs using `select_utxos_for_amount`
2736    /// 2. Creates transaction inputs from selected UTXOs
2737    /// 3. Creates outputs for distribution addresses
2738    /// 4. Calculates and creates change output if necessary
2739    /// 5. Builds the raw transaction using `create_raw_transaction`
2740    ///
2741    /// # Arguments
2742    /// * `asset_id` - The asset ID being distributed
2743    /// * `address_amounts` - Map of recipient addresses to amounts
2744    /// * `change_address` - Address to send change to (if any)
2745    /// * `estimated_fee` - Estimated transaction fee
2746    ///
2747    /// # Returns
2748    /// Returns a tuple of (`raw_transaction_hex`, `selected_utxos`, `change_amount`)
2749    ///
2750    /// # Errors
2751    /// Returns an error if UTXO selection fails or transaction building fails
2752    ///
2753    /// # Examples
2754    /// ```no_run
2755    /// # use amp_rs::ElementsRpc;
2756    /// # use std::collections::HashMap;
2757    /// # #[tokio::main]
2758    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2759    /// let rpc = ElementsRpc::from_env()?;
2760    /// let mut address_amounts = HashMap::new();
2761    /// address_amounts.insert("address1".to_string(), 100.0);
2762    /// address_amounts.insert("address2".to_string(), 50.0);
2763    ///
2764    /// let (raw_tx, utxos, change) = rpc.build_distribution_transaction(
2765    ///     "wallet_name",
2766    ///     "asset_id_hex",
2767    ///     address_amounts,
2768    ///     "change_address",
2769    ///     0.001
2770    /// ).await?;
2771    /// println!("Built transaction with {} inputs, change: {}", utxos.len(), change);
2772    /// # Ok(())
2773    /// # }
2774    /// ```
2775    #[allow(clippy::cognitive_complexity)]
2776    #[allow(clippy::too_many_lines)]
2777    pub async fn build_distribution_transaction(
2778        &self,
2779        wallet_name: &str,
2780        asset_id: &str,
2781        address_amounts: std::collections::HashMap<String, f64>,
2782        change_address: &str,
2783        _estimated_fee: f64,
2784    ) -> Result<(String, Vec<Unspent>, f64), AmpError> {
2785        const DUST_THRESHOLD: f64 = 0.00001;
2786        const LBTC_ASSET_ID: &str =
2787            "144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49"; // L-BTC on Liquid testnet
2788
2789        tracing::debug!(
2790            "Building distribution transaction for asset {} with {} outputs",
2791            asset_id,
2792            address_amounts.len()
2793        );
2794
2795        // Calculate total distribution amount
2796        let total_distribution: f64 = address_amounts.values().sum();
2797
2798        if total_distribution <= 0.0 {
2799            return Err(AmpError::validation(
2800                "Total distribution amount must be greater than zero".to_string(),
2801            ));
2802        }
2803
2804        // Select UTXOs to cover the distribution (custom asset)
2805        let (selected_asset_utxos, total_selected) = self
2806            .select_utxos_for_amount(wallet_name, asset_id, total_distribution, 0.0)
2807            .await?;
2808
2809        // Also select L-BTC UTXOs for transaction fees
2810        // Elements requires L-BTC inputs for fees even when distributing custom assets
2811        let min_lbtc_fee = 0.00001; // Minimum L-BTC needed for fees
2812        let (selected_lbtc_utxos, lbtc_total) = match self
2813            .select_utxos_for_amount(wallet_name, LBTC_ASSET_ID, 0.0, min_lbtc_fee)
2814            .await
2815        {
2816            Ok((utxos, total)) => {
2817                tracing::info!(
2818                    "Selected {} L-BTC UTXOs totaling {} for fees",
2819                    utxos.len(),
2820                    total
2821                );
2822                (utxos, total)
2823            }
2824            Err(e) => {
2825                tracing::warn!(
2826                    "Could not select L-BTC UTXOs for fees: {}. Transaction may fail.",
2827                    e
2828                );
2829                (Vec::new(), 0.0)
2830            }
2831        };
2832
2833        // Combine custom asset UTXOs and L-BTC UTXOs
2834        let mut all_utxos = selected_asset_utxos.clone();
2835        all_utxos.extend(selected_lbtc_utxos.clone());
2836
2837        if selected_lbtc_utxos.is_empty() {
2838            tracing::warn!(
2839                "No L-BTC UTXOs selected for fees. Transaction may fail during broadcast."
2840            );
2841        } else {
2842            tracing::info!(
2843                "Transaction includes {} custom asset UTXOs and {} L-BTC UTXOs for fees",
2844                selected_asset_utxos.len(),
2845                selected_lbtc_utxos.len()
2846            );
2847        }
2848
2849        // Create transaction inputs from all selected UTXOs
2850        let inputs: Vec<TxInput> = all_utxos
2851            .iter()
2852            .map(|utxo| TxInput {
2853                txid: utxo.txid.clone(),
2854                vout: utxo.vout,
2855                sequence: None, // Use default sequence
2856            })
2857            .collect();
2858
2859        // Create outputs for distribution (custom asset)
2860        // We need to track outputs as a vector since we may have multiple outputs to the same address
2861        // (e.g., custom asset change + L-BTC change to the same change address)
2862        let mut output_list = Vec::new();
2863
2864        // Add distribution outputs (custom asset)
2865        for (address, amount) in &address_amounts {
2866            output_list.push((address.clone(), *amount, asset_id.to_string()));
2867        }
2868
2869        // Calculate change amount for custom asset (total selected - distribution)
2870        let asset_change_amount = total_selected - total_distribution;
2871
2872        // Add asset change output if there's a significant amount left
2873        if asset_change_amount > DUST_THRESHOLD {
2874            output_list.push((
2875                change_address.to_string(),
2876                asset_change_amount,
2877                asset_id.to_string(),
2878            ));
2879
2880            tracing::debug!(
2881                "Adding asset change output: {} {} to address {}",
2882                asset_change_amount,
2883                asset_id,
2884                change_address
2885            );
2886        } else if asset_change_amount > 0.0 {
2887            tracing::warn!(
2888                "Asset change amount {} is below dust threshold {}, will be lost",
2889                asset_change_amount,
2890                DUST_THRESHOLD
2891            );
2892        }
2893
2894        // Handle L-BTC change if we selected L-BTC UTXOs for fees
2895        // In Elements, the fee is implicit - it's the difference between L-BTC inputs and outputs
2896        // We should NOT subtract the fee from outputs; Elements calculates it automatically
2897        if !selected_lbtc_utxos.is_empty() {
2898            tracing::debug!(
2899                "L-BTC input total: {}, minimum fee needed: {}",
2900                lbtc_total,
2901                min_lbtc_fee
2902            );
2903
2904            // Check if we have enough L-BTC for the minimum fee
2905            if lbtc_total < min_lbtc_fee {
2906                return Err(AmpError::validation(format!(
2907                    "Insufficient L-BTC for fees: have {lbtc_total}, need at least {min_lbtc_fee}"
2908                )));
2909            }
2910
2911            // For now, let's try NOT adding any L-BTC change output
2912            // and let Elements handle the fee automatically from the input/output difference
2913            tracing::info!(
2914                "Using L-BTC input {} for fees - no explicit L-BTC change output (Elements will handle fee automatically)",
2915                lbtc_total
2916            );
2917
2918            // Note: If this approach works, the entire L-BTC input will become the fee
2919            // If we need change, we'll need to figure out the correct way to handle it
2920        }
2921
2922        // For confidential addresses, we need to import them into the wallet first
2923        // so Elements knows about the blinding keys
2924        for address in address_amounts.keys() {
2925            if address.starts_with('v') {
2926                // Confidential address
2927                tracing::debug!("Importing confidential address into wallet: {}", address);
2928                if let Err(e) = self
2929                    .import_address_to_wallet(wallet_name, address, None, false)
2930                    .await
2931                {
2932                    tracing::warn!("Failed to import confidential address {}: {}", address, e);
2933                    // Continue anyway - the address might already be imported
2934                }
2935            }
2936        }
2937
2938        // Build the raw transaction using wallet-specific endpoint for confidential transactions
2939        // For confidential transactions, we need to use blindrawtransaction to properly handle blinding
2940        let raw_transaction = self
2941            .create_raw_transaction_with_outputs(wallet_name, inputs, output_list)
2942            .await
2943            .map_err(|e| {
2944                // Provide more helpful error message for the common L-BTC fee issue
2945                if e.to_string().contains("bad-txns-in-ne-out") || e.to_string().contains("value in != value out") {
2946                    AmpError::validation(format!(
2947                        "Transaction failed due to confidential transaction blinding mismatch. \
2948                        This occurs when Elements creates blinding factors that don't match LWK's expectations. \
2949                        To fix this:\n\
2950                        1. Ensure the wallet has proper blinding keys for all addresses\n\
2951                        2. Use blindrawtransaction before signing\n\
2952                        3. Verify UTXO blinding factors match between Elements and LWK\n\
2953                        4. Original error: {e}"
2954                    ))
2955                } else {
2956                    e.with_context("Failed to build distribution transaction")
2957                }
2958            })?;
2959
2960        // For confidential transactions, we need to blind the transaction properly
2961        // This ensures the blinding factors are compatible with LWK signing
2962        tracing::debug!("Blinding raw transaction for confidential asset distribution");
2963        let blinded_transaction = self
2964            .blind_raw_transaction(wallet_name, &raw_transaction)
2965            .await
2966            .map_err(|e| {
2967                tracing::warn!(
2968                    "Failed to blind transaction, proceeding with unblinded: {}",
2969                    e
2970                );
2971                // If blinding fails, we'll try to proceed with the unblinded transaction
2972                // This might work for some cases but could fail during broadcast
2973                e.with_context("Transaction blinding failed")
2974            })
2975            .unwrap_or_else(|_| {
2976                tracing::warn!("Using unblinded transaction - this may cause broadcast failures");
2977                raw_transaction.clone()
2978            });
2979
2980        tracing::info!(
2981            "Built distribution transaction: {} inputs, {} outputs, asset change: {}",
2982            all_utxos.len(),
2983            address_amounts.len() + usize::from(asset_change_amount > DUST_THRESHOLD),
2984            if asset_change_amount > DUST_THRESHOLD {
2985                asset_change_amount
2986            } else {
2987                0.0
2988            }
2989        );
2990
2991        Ok((blinded_transaction, all_utxos, asset_change_amount))
2992    }
2993
2994    /// Creates a raw transaction with multiple outputs that can handle multiple assets to the same address
2995    ///
2996    /// This method is similar to `create_raw_transaction_with_wallet` but handles the case where
2997    /// multiple outputs with different assets need to go to the same address (e.g., asset change + L-BTC change).
2998    ///
2999    /// # Arguments
3000    /// * `wallet_name` - Name of the Elements wallet to use
3001    /// * `inputs` - Vector of transaction inputs
3002    /// * `outputs` - Vector of (address, amount, `asset_id`) tuples
3003    ///
3004    /// # Returns
3005    /// Returns the raw transaction hex string
3006    #[allow(clippy::cognitive_complexity)]
3007    async fn create_raw_transaction_with_outputs(
3008        &self,
3009        wallet_name: &str,
3010        inputs: Vec<TxInput>,
3011        outputs: Vec<(String, f64, String)>, // (address, amount, asset_id)
3012    ) -> Result<String, AmpError> {
3013        tracing::debug!(
3014            "Creating raw transaction with wallet {} - {} inputs and {} outputs",
3015            wallet_name,
3016            inputs.len(),
3017            outputs.len()
3018        );
3019
3020        // First load the wallet to ensure it's available
3021        self.load_wallet(wallet_name).await?;
3022
3023        // Elements RPC createrawtransaction expects outputs as an array of objects
3024        // Each output object should contain both address, amount, and asset
3025        let mut outputs_array = Vec::new();
3026
3027        for (address, amount, asset_id) in &outputs {
3028            // Convert amount to string with proper precision for Elements
3029            let amount_str = format!("{amount:.8}");
3030
3031            outputs_array.push(serde_json::json!({
3032                address.clone(): amount_str,
3033                "asset": asset_id
3034            }));
3035        }
3036
3037        let params = serde_json::json!([
3038            inputs,        // inputs as TxInput array
3039            outputs_array, // outputs as array of {address: amount, asset: id} objects
3040            0,             // locktime (0 = no locktime)
3041            false,         // replaceable (false = not replaceable)
3042        ]);
3043
3044        // Debug: Log the exact parameters being sent to createrawtransaction
3045        tracing::error!("createrawtransaction parameters (wallet-specific, corrected format):");
3046        tracing::error!("  wallet: {}", wallet_name);
3047        tracing::error!(
3048            "  inputs: {}",
3049            serde_json::to_string_pretty(&inputs).unwrap_or_default()
3050        );
3051        tracing::error!(
3052            "  outputs_array: {}",
3053            serde_json::to_string_pretty(&outputs_array).unwrap_or_default()
3054        );
3055
3056        // Use the wallet-specific RPC endpoint
3057        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
3058
3059        let request = RpcRequest {
3060            jsonrpc: "1.0".to_string(),
3061            id: "amp-client".to_string(),
3062            method: "createrawtransaction".to_string(),
3063            params,
3064        };
3065
3066        let response = self
3067            .client
3068            .post(&wallet_url)
3069            .basic_auth(&self.username, Some(&self.password))
3070            .json(&request)
3071            .send()
3072            .await
3073            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
3074
3075        if !response.status().is_success() {
3076            let status = response.status();
3077            let error_body = response
3078                .text()
3079                .await
3080                .unwrap_or_else(|_| "Unable to read error body".to_string());
3081            return Err(AmpError::rpc(format!(
3082                "RPC request failed with status: {status} - Body: {error_body}"
3083            )));
3084        }
3085
3086        let rpc_response: RpcResponse<String> = response
3087            .json()
3088            .await
3089            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
3090
3091        if let Some(error) = rpc_response.error {
3092            return Err(AmpError::rpc(format!(
3093                "RPC error creating raw transaction: {} (code: {})",
3094                error.message, error.code
3095            )));
3096        }
3097
3098        Ok(rpc_response.result.unwrap_or_default())
3099    }
3100
3101    /// Blinds a raw transaction for confidential transactions
3102    ///
3103    /// This method uses Elements' blindrawtransaction RPC to properly blind a transaction
3104    /// for confidential asset transfers. This is crucial for Liquid transactions to ensure
3105    /// the blinding factors are properly balanced.
3106    ///
3107    /// # Arguments
3108    /// * `wallet_name` - Name of the Elements wallet to use for blinding
3109    /// * `raw_transaction` - The raw transaction hex to blind
3110    ///
3111    /// # Returns
3112    /// Returns the blinded transaction hex string
3113    ///
3114    /// # Errors
3115    /// Returns an error if the RPC call fails or blinding is not possible
3116    pub async fn blind_raw_transaction(
3117        &self,
3118        wallet_name: &str,
3119        raw_transaction: &str,
3120    ) -> Result<String, AmpError> {
3121        tracing::debug!(
3122            "Blinding raw transaction for wallet {} - tx length: {} chars",
3123            wallet_name,
3124            raw_transaction.len()
3125        );
3126
3127        // First load the wallet to ensure it's available
3128        self.load_wallet(wallet_name).await?;
3129
3130        // Elements blindrawtransaction parameters:
3131        // 1. Raw transaction hex
3132        // 2. Input blinding data (can be empty array for auto-detection)
3133        // 3. Input amounts (can be empty array for auto-detection from UTXOs)
3134        // 4. Input assets (can be empty array for auto-detection from UTXOs)
3135        // 5. Input asset blinders (can be empty array for auto-detection)
3136        // 6. Input amount blinders (can be empty array for auto-detection)
3137        let params = serde_json::json!([
3138            raw_transaction, // Raw transaction hex
3139            [],              // Input blinding data (empty for auto-detection)
3140            [],              // Input amounts (empty for auto-detection)
3141            [],              // Input assets (empty for auto-detection)
3142            [],              // Input asset blinders (empty for auto-detection)
3143            []               // Input amount blinders (empty for auto-detection)
3144        ]);
3145
3146        // Use the wallet-specific RPC endpoint
3147        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
3148
3149        let request = RpcRequest {
3150            jsonrpc: "1.0".to_string(),
3151            id: "amp-client".to_string(),
3152            method: "blindrawtransaction".to_string(),
3153            params,
3154        };
3155
3156        let response = self
3157            .client
3158            .post(&wallet_url)
3159            .basic_auth(&self.username, Some(&self.password))
3160            .json(&request)
3161            .send()
3162            .await
3163            .map_err(|e| {
3164                AmpError::rpc(format!("Failed to send blindrawtransaction request: {e}"))
3165            })?;
3166
3167        if !response.status().is_success() {
3168            let status = response.status();
3169            let error_body = response
3170                .text()
3171                .await
3172                .unwrap_or_else(|_| "Unable to read error body".to_string());
3173            return Err(AmpError::rpc(format!(
3174                "blindrawtransaction failed with status: {status} - Body: {error_body}"
3175            )));
3176        }
3177
3178        let rpc_response: RpcResponse<String> = response.json().await.map_err(|e| {
3179            AmpError::rpc(format!("Failed to parse blindrawtransaction response: {e}"))
3180        })?;
3181
3182        if let Some(error) = rpc_response.error {
3183            return Err(AmpError::rpc(format!(
3184                "RPC error blinding transaction: {} (code: {})",
3185                error.message, error.code
3186            )));
3187        }
3188
3189        let blinded_tx = rpc_response.result.unwrap_or_default();
3190
3191        tracing::info!(
3192            "Successfully blinded transaction - original: {} chars, blinded: {} chars",
3193            raw_transaction.len(),
3194            blinded_tx.len()
3195        );
3196
3197        Ok(blinded_tx)
3198    }
3199
3200    /// Signs a raw transaction using the provided signer callback
3201    ///
3202    /// This method integrates with the Signer trait to sign unsigned transactions.
3203    /// It handles the complete signing workflow including:
3204    /// 1. Validation of the unsigned transaction hex format
3205    /// 2. Calling the signer's `sign_transaction` method
3206    /// 3. Validation of the signed transaction format and structure
3207    /// 4. Proper error handling and context propagation
3208    ///
3209    /// # Arguments
3210    /// * `unsigned_tx_hex` - The unsigned transaction in hexadecimal format
3211    /// * `signer` - Implementation of the Signer trait for transaction signing
3212    ///
3213    /// # Returns
3214    /// Returns the signed transaction as a hex string
3215    ///
3216    /// # Errors
3217    /// Returns an error if:
3218    /// - The unsigned transaction hex is invalid or malformed
3219    /// - The signer fails to sign the transaction
3220    /// - The signed transaction format is invalid
3221    /// - Any validation checks fail
3222    ///
3223    /// # Examples
3224    /// ```no_run
3225    /// # use amp_rs::{ElementsRpc, signer::{Signer, LwkSoftwareSigner}};
3226    /// # #[tokio::main]
3227    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3228    /// let rpc = ElementsRpc::from_env()?;
3229    /// let (_, signer) = LwkSoftwareSigner::generate_new()?;
3230    /// let unsigned_tx = "020000000001..."; // Unsigned transaction hex
3231    /// let signed_tx = rpc.sign_transaction(unsigned_tx, &signer).await?;
3232    /// println!("Transaction signed successfully: {}", signed_tx);
3233    /// # Ok(())
3234    /// # }
3235    /// ```
3236    #[allow(clippy::cognitive_complexity)]
3237    pub async fn sign_transaction(
3238        &self,
3239        unsigned_tx_hex: &str,
3240        signer: &dyn crate::signer::Signer,
3241    ) -> Result<String, AmpError> {
3242        const MIN_TX_SIZE: usize = 10; // Minimum bytes for a valid transaction
3243
3244        tracing::debug!(
3245            "Signing transaction: {}...",
3246            &unsigned_tx_hex[..std::cmp::min(unsigned_tx_hex.len(), 64)]
3247        );
3248
3249        // Validate unsigned transaction hex format
3250        if unsigned_tx_hex.is_empty() {
3251            return Err(AmpError::validation(
3252                "Unsigned transaction hex cannot be empty".to_string(),
3253            ));
3254        }
3255
3256        // Check if hex string has valid format (even length, valid hex characters)
3257        if unsigned_tx_hex.len() % 2 != 0 {
3258            return Err(AmpError::validation(
3259                "Unsigned transaction hex must have even length".to_string(),
3260            ));
3261        }
3262
3263        // Validate hex characters
3264        if !unsigned_tx_hex.chars().all(|c| c.is_ascii_hexdigit()) {
3265            return Err(AmpError::validation(
3266                "Unsigned transaction contains invalid hex characters".to_string(),
3267            ));
3268        }
3269
3270        // Attempt to decode hex to validate transaction structure
3271        let tx_bytes = hex::decode(unsigned_tx_hex).map_err(|e| {
3272            AmpError::validation(format!("Failed to decode unsigned transaction hex: {e}"))
3273        })?;
3274
3275        tracing::debug!("Unsigned transaction validation passed, calling signer");
3276
3277        // Call the signer to sign the transaction
3278        let signed_tx_hex = signer
3279            .sign_transaction(unsigned_tx_hex)
3280            .await
3281            .map_err(|e| {
3282                tracing::error!("Transaction signing failed: {}", e);
3283                AmpError::Signer(e).with_context("Failed to sign transaction")
3284            })?;
3285
3286        tracing::debug!(
3287            "Signer returned signed transaction: {}...",
3288            &signed_tx_hex[..std::cmp::min(signed_tx_hex.len(), 64)]
3289        );
3290
3291        // Validate signed transaction format and structure
3292        if signed_tx_hex.is_empty() {
3293            return Err(AmpError::validation(
3294                "Signed transaction hex cannot be empty".to_string(),
3295            ));
3296        }
3297
3298        // Check if signed transaction has valid hex format
3299        if signed_tx_hex.len() % 2 != 0 {
3300            return Err(AmpError::validation(
3301                "Signed transaction hex must have even length".to_string(),
3302            ));
3303        }
3304
3305        // Validate hex characters in signed transaction
3306        if !signed_tx_hex.chars().all(|c| c.is_ascii_hexdigit()) {
3307            return Err(AmpError::validation(
3308                "Signed transaction contains invalid hex characters".to_string(),
3309            ));
3310        }
3311
3312        // Attempt to decode signed transaction to validate structure
3313        let signed_tx_bytes = hex::decode(&signed_tx_hex).map_err(|e| {
3314            AmpError::validation(format!("Failed to decode signed transaction hex: {e}"))
3315        })?;
3316
3317        // Basic validation: signed transaction should be at least as long as unsigned
3318        // (signatures add data, so signed tx should be larger or equal)
3319        if signed_tx_bytes.len() < tx_bytes.len() {
3320            return Err(AmpError::validation(
3321                "Signed transaction is shorter than unsigned transaction, which is invalid"
3322                    .to_string(),
3323            ));
3324        }
3325
3326        // Additional validation: check that the transaction structure is reasonable
3327        // Minimum transaction size for Elements (very basic check)
3328        if signed_tx_bytes.len() < MIN_TX_SIZE {
3329            return Err(AmpError::validation(format!(
3330                "Signed transaction does not meet minimum size ({} bytes), minimum is {} bytes",
3331                signed_tx_bytes.len(),
3332                MIN_TX_SIZE
3333            )));
3334        }
3335
3336        tracing::info!(
3337            "Transaction signed successfully - unsigned: {} bytes, signed: {} bytes",
3338            tx_bytes.len(),
3339            signed_tx_bytes.len()
3340        );
3341
3342        Ok(signed_tx_hex)
3343    }
3344
3345    /// Signs and broadcasts a transaction in a single operation
3346    ///
3347    /// This is a convenience method that combines transaction signing and broadcasting.
3348    /// It performs the complete workflow of signing an unsigned transaction and
3349    /// immediately broadcasting it to the network.
3350    ///
3351    /// # Arguments
3352    /// * `unsigned_tx_hex` - The unsigned transaction in hexadecimal format
3353    /// * `signer` - Implementation of the Signer trait for transaction signing
3354    ///
3355    /// # Returns
3356    /// Returns the transaction ID of the broadcast transaction
3357    ///
3358    /// # Errors
3359    /// Returns an error if signing or broadcasting fails
3360    ///
3361    /// # Examples
3362    /// ```no_run
3363    /// # use amp_rs::{ElementsRpc, signer::{Signer, LwkSoftwareSigner}};
3364    /// # #[tokio::main]
3365    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3366    /// let rpc = ElementsRpc::from_env()?;
3367    /// let (_, signer) = LwkSoftwareSigner::generate_new()?;
3368    /// let unsigned_tx = "020000000001..."; // Unsigned transaction hex
3369    /// let txid = rpc.sign_and_broadcast_transaction(unsigned_tx, &signer).await?;
3370    /// println!("Transaction broadcast with ID: {}", txid);
3371    /// # Ok(())
3372    /// # }
3373    /// ```
3374    pub async fn sign_and_broadcast_transaction(
3375        &self,
3376        unsigned_tx_hex: &str,
3377        signer: &dyn crate::signer::Signer,
3378    ) -> Result<String, AmpError> {
3379        tracing::info!("Signing and broadcasting transaction");
3380
3381        // Sign the transaction
3382        let signed_tx_hex = self
3383            .sign_transaction(unsigned_tx_hex, signer)
3384            .await
3385            .map_err(|e| e.with_context("Failed during transaction signing phase"))?;
3386
3387        // Broadcast the signed transaction
3388        let txid = self
3389            .send_raw_transaction(&signed_tx_hex)
3390            .await
3391            .map_err(|e| e.with_context("Failed during transaction broadcast phase"))?;
3392
3393        tracing::info!("Successfully signed and broadcast transaction: {}", txid);
3394        Ok(txid)
3395    }
3396
3397    /// Signs and broadcasts a transaction with UTXO information for proper PSBT construction
3398    ///
3399    /// This method provides UTXO information to the signer for proper PSBT construction,
3400    /// which is required for confidential transactions where the signer needs to know
3401    /// the previous transaction outputs being spent.
3402    ///
3403    /// # Arguments
3404    /// * `unsigned_tx_hex` - The unsigned transaction in hexadecimal format
3405    /// * `utxos` - Vector of UTXOs being spent in the transaction
3406    /// * `signer` - Implementation of the Signer trait for transaction signing
3407    ///
3408    /// # Returns
3409    /// Returns the transaction ID of the broadcast transaction
3410    ///
3411    /// # Errors
3412    /// Returns an error if signing or broadcasting fails
3413    #[allow(clippy::cognitive_complexity)]
3414    pub async fn sign_and_broadcast_transaction_with_utxos(
3415        &self,
3416        unsigned_tx_hex: &str,
3417        utxos: &[Unspent],
3418        signer: &dyn crate::signer::Signer,
3419    ) -> Result<String, AmpError> {
3420        tracing::info!(
3421            "Signing and broadcasting transaction with {} UTXOs",
3422            utxos.len()
3423        );
3424
3425        // Try to use the enhanced signing method if the signer supports it
3426        let signed_tx_hex = if let Some(lwk_signer) = signer
3427            .as_any()
3428            .downcast_ref::<crate::signer::LwkSoftwareSigner>(
3429        ) {
3430            // Use the enhanced signing method with UTXO information
3431            tracing::debug!("Using LWK signer with UTXO information");
3432            lwk_signer
3433                .sign_transaction_with_utxos(unsigned_tx_hex, utxos)
3434                .await
3435                .map_err(|e| {
3436                    AmpError::Signer(e)
3437                        .with_context("Failed during enhanced transaction signing phase")
3438                })?
3439        } else {
3440            // Fall back to standard signing method
3441            tracing::debug!("Using standard signing method (no UTXO information)");
3442            self.sign_transaction(unsigned_tx_hex, signer)
3443                .await
3444                .map_err(|e| e.with_context("Failed during transaction signing phase"))?
3445        };
3446
3447        // Broadcast the signed transaction
3448        let txid = self
3449            .send_raw_transaction(&signed_tx_hex)
3450            .await
3451            .map_err(|e| e.with_context("Failed during transaction broadcast phase"))?;
3452
3453        tracing::info!("Successfully signed and broadcast transaction: {}", txid);
3454        Ok(txid)
3455    }
3456
3457    /// Collects change data from a confirmed transaction for distribution confirmation
3458    ///
3459    /// This method queries the Elements node to find change UTXOs from a specific transaction
3460    /// that belong to the specified asset. It's used after a distribution transaction is
3461    /// confirmed to collect the change outputs for the final confirmation API call.
3462    ///
3463    /// # Arguments
3464    /// * `asset_id` - The asset ID to filter change UTXOs for
3465    /// * `txid` - The transaction ID to filter change UTXOs from
3466    ///
3467    /// # Returns
3468    /// Returns a vector of Unspent UTXOs that represent change outputs from the transaction.
3469    /// Returns an empty vector if no change outputs exist for the specified asset and transaction.
3470    ///
3471    /// # Errors
3472    /// Returns an error if the RPC call fails or if there are issues querying the Elements node
3473    ///
3474    /// # Examples
3475    /// ```no_run
3476    /// # use amp_rs::ElementsRpc;
3477    /// # #[tokio::main]
3478    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3479    /// let rpc = ElementsRpc::from_env()?;
3480    /// let change_data = rpc.collect_change_data(
3481    ///     "asset_id_hex",
3482    ///     "transaction_id_hex",
3483    ///     &rpc,
3484    ///     "wallet_name"
3485    /// ).await?;
3486    ///
3487    /// if change_data.is_empty() {
3488    ///     println!("No change outputs found for this transaction");
3489    /// } else {
3490    ///     println!("Found {} change outputs", change_data.len());
3491    /// }
3492    /// # Ok(())
3493    /// # }
3494    /// ```
3495    #[allow(clippy::cognitive_complexity)]
3496    pub async fn collect_change_data(
3497        &self,
3498        asset_id: &str,
3499        txid: &str,
3500        node_rpc: &Self,
3501        wallet_name: &str,
3502    ) -> Result<Vec<Unspent>, AmpError> {
3503        tracing::debug!(
3504            "Collecting change data for asset {} from transaction {}",
3505            asset_id,
3506            txid
3507        );
3508
3509        // Use the raw listunspent RPC call to get full blinding information
3510        // This is essential for confidential transactions as the AMP API requires
3511        // both amountblinder and assetblinder fields
3512        let all_utxos = node_rpc
3513            .list_unspent_with_blinding_data(wallet_name)
3514            .await
3515            .map_err(|e| {
3516                e.with_context(
3517                    "Failed to query unspent outputs with blinding data for change data collection",
3518                )
3519            })?;
3520
3521        // Filter UTXOs to only include those from the specified transaction
3522        let change_utxos: Vec<Unspent> = all_utxos
3523            .into_iter()
3524            .filter(|utxo| {
3525                // Match UTXOs that:
3526                // 1. Come from the specified transaction (txid matches)
3527                // 2. Are for the correct asset
3528                // 3. Are spendable
3529                utxo.txid == txid && utxo.asset == asset_id && utxo.spendable
3530            })
3531            .collect();
3532
3533        tracing::info!(
3534            "Collected {} change UTXOs for asset {} from transaction {}",
3535            change_utxos.len(),
3536            asset_id,
3537            txid
3538        );
3539
3540        // Log details of found change UTXOs for debugging
3541        for (index, utxo) in change_utxos.iter().enumerate() {
3542            tracing::debug!(
3543                "Change UTXO {}: txid={}, vout={}, amount={}, asset={}, amountblinder={:?}, assetblinder={:?}",
3544                index + 1,
3545                utxo.txid,
3546                utxo.vout,
3547                utxo.amount,
3548                utxo.asset,
3549                utxo.amountblinder,
3550                utxo.assetblinder
3551            );
3552        }
3553
3554        // Handle the case where no change outputs exist
3555        if change_utxos.is_empty() {
3556            tracing::info!(
3557                "No change outputs found for asset {} in transaction {} - this is normal if all funds were distributed",
3558                asset_id,
3559                txid
3560            );
3561        }
3562
3563        Ok(change_utxos)
3564    }
3565
3566    /// Lists unspent outputs with full blinding data for confidential transactions
3567    ///
3568    /// This method calls the raw `listunspent` RPC to get complete UTXO information
3569    /// including blinding data (amountblinder and assetblinder) which is required
3570    /// for confidential transaction confirmation with the AMP API.
3571    ///
3572    /// # Arguments
3573    /// * `wallet_name` - Name of the Elements wallet to query
3574    ///
3575    /// # Returns
3576    /// Returns a vector of `Unspent` structs with complete blinding information
3577    ///
3578    /// # Errors
3579    /// Returns an error if the RPC call fails
3580    ///
3581    /// # Examples
3582    /// ```no_run
3583    /// # use amp_rs::ElementsRpc;
3584    /// # #[tokio::main]
3585    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3586    /// let rpc = ElementsRpc::from_env()?;
3587    /// let utxos = rpc.list_unspent_with_blinding_data("wallet_name").await?;
3588    /// for utxo in utxos {
3589    ///     println!("UTXO: {} with blinders: {:?}, {:?}",
3590    ///              utxo.txid, utxo.amountblinder, utxo.assetblinder);
3591    /// }
3592    /// # Ok(())
3593    /// # }
3594    /// ```
3595    pub async fn list_unspent_with_blinding_data(
3596        &self,
3597        wallet_name: &str,
3598    ) -> Result<Vec<Unspent>, AmpError> {
3599        tracing::debug!(
3600            "Listing unspent outputs with blinding data for wallet: {}",
3601            wallet_name
3602        );
3603
3604        // First load the wallet to ensure it's available
3605        self.load_wallet(wallet_name).await?;
3606
3607        // Call listunspent with parameters to get all UTXOs
3608        // Parameters: minconf, maxconf, addresses, include_unsafe, query_options
3609        let params = serde_json::json!([
3610            0,         // minconf: include unconfirmed
3611            9_999_999, // maxconf: include all confirmed
3612            [],        // addresses: empty array means all addresses
3613            true,      // include_unsafe: include unconfirmed transactions
3614            {}         // query_options: empty object for default options
3615        ]);
3616
3617        // Use the wallet-specific RPC endpoint
3618        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
3619
3620        let request = RpcRequest {
3621            jsonrpc: "1.0".to_string(),
3622            id: "amp-client".to_string(),
3623            method: "listunspent".to_string(),
3624            params,
3625        };
3626
3627        let response = self
3628            .client
3629            .post(&wallet_url)
3630            .basic_auth(&self.username, Some(&self.password))
3631            .json(&request)
3632            .send()
3633            .await
3634            .map_err(|e| AmpError::rpc(format!("Failed to send listunspent RPC request: {e}")))?;
3635
3636        if !response.status().is_success() {
3637            let status = response.status();
3638            let error_body = response
3639                .text()
3640                .await
3641                .unwrap_or_else(|_| "Unable to read error body".to_string());
3642            return Err(AmpError::rpc(format!(
3643                "Listunspent RPC request failed with status: {status} - Body: {error_body}"
3644            )));
3645        }
3646
3647        let rpc_response: RpcResponse<Vec<Unspent>> = response
3648            .json()
3649            .await
3650            .map_err(|e| AmpError::rpc(format!("Failed to parse listunspent RPC response: {e}")))?;
3651
3652        if let Some(error) = rpc_response.error {
3653            return Err(AmpError::rpc(format!(
3654                "Listunspent RPC error: {} (code: {})",
3655                error.message, error.code
3656            )));
3657        }
3658
3659        let utxos = rpc_response.result.unwrap_or_default();
3660        tracing::info!(
3661            "Retrieved {} UTXOs with blinding data from wallet {}",
3662            utxos.len(),
3663            wallet_name
3664        );
3665
3666        Ok(utxos)
3667    }
3668
3669    /// Creates a standard wallet in Elements (Elements-first approach)
3670    ///
3671    /// This method creates a new standard wallet in the Elements node that can generate
3672    /// addresses and private keys. This is part of the Elements-first approach where
3673    /// we create the wallet in Elements first, then export keys to LWK.
3674    ///
3675    /// # Arguments
3676    /// * `wallet_name` - Name for the new wallet
3677    ///
3678    /// # Errors
3679    /// Returns an error if the RPC call fails
3680    ///
3681    /// # Examples
3682    /// ```no_run
3683    /// # use amp_rs::ElementsRpc;
3684    /// # #[tokio::main]
3685    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3686    /// let rpc = ElementsRpc::from_env()?;
3687    /// rpc.create_elements_wallet("test_wallet").await?;
3688    /// # Ok(())
3689    /// # }
3690    /// ```
3691    pub async fn create_elements_wallet(&self, wallet_name: &str) -> Result<(), AmpError> {
3692        let params = serde_json::json!([wallet_name]);
3693
3694        let _result: serde_json::Value = self.rpc_call("createwallet", params).await?;
3695
3696        tracing::info!("Successfully created Elements wallet: {}", wallet_name);
3697        Ok(())
3698    }
3699
3700    /// Get a new address from an Elements wallet
3701    ///
3702    /// This method requests a new address from the specified Elements wallet.
3703    /// The address will be generated by Elements and can be used for receiving funds.
3704    /// Defaults to native segwit (bech32) addresses for optimal compatibility.
3705    ///
3706    /// # Arguments
3707    /// * `wallet_name` - Name of the wallet to get address from
3708    /// * `address_type` - Optional address type ("bech32", "legacy", "p2sh-segwit"). Defaults to "bech32"
3709    ///
3710    /// # Errors
3711    /// Returns an error if the RPC call fails or the response format is unexpected
3712    ///
3713    /// # Examples
3714    /// ```no_run
3715    /// # use amp_rs::ElementsRpc;
3716    /// # #[tokio::main]
3717    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3718    /// let rpc = ElementsRpc::from_env()?;
3719    ///
3720    /// // Generate native segwit address (default)
3721    /// let address = rpc.get_new_address("test_wallet", None).await?;
3722    ///
3723    /// // Or explicitly request native segwit
3724    /// let bech32_address = rpc.get_new_address("test_wallet", Some("bech32")).await?;
3725    ///
3726    /// println!("Native segwit address: {}", address);
3727    /// # Ok(())
3728    /// # }
3729    /// ```
3730    pub async fn get_new_address(
3731        &self,
3732        wallet_name: &str,
3733        address_type: Option<&str>,
3734    ) -> Result<String, AmpError> {
3735        // First load the wallet to ensure it's available
3736        self.load_wallet(wallet_name).await?;
3737
3738        // Set default to native segwit (bech32) for Elements
3739        let addr_type = address_type.unwrap_or("bech32");
3740
3741        // For Elements, we need to use the correct parameters for getnewaddress
3742        // getnewaddress [label] [address_type]
3743        let params = serde_json::json!(["", addr_type]);
3744
3745        // Create RPC request for getnewaddress
3746        let request = RpcRequest {
3747            jsonrpc: "1.0".to_string(),
3748            id: "amp-client".to_string(),
3749            method: "getnewaddress".to_string(),
3750            params,
3751        };
3752
3753        // Use the wallet-specific RPC endpoint
3754        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
3755
3756        let response = self
3757            .client
3758            .post(&wallet_url)
3759            .basic_auth(&self.username, Some(&self.password))
3760            .json(&request)
3761            .send()
3762            .await
3763            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
3764
3765        if !response.status().is_success() {
3766            let status = response.status();
3767            let error_body = response
3768                .text()
3769                .await
3770                .unwrap_or_else(|_| "Unable to read error body".to_string());
3771            return Err(AmpError::rpc(format!(
3772                "RPC request failed with status: {status} - Body: {error_body}"
3773            )));
3774        }
3775
3776        let rpc_response: RpcResponse<serde_json::Value> = response
3777            .json()
3778            .await
3779            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
3780
3781        if let Some(error) = rpc_response.error {
3782            return Err(AmpError::rpc(format!(
3783                "RPC error getting new address: {} (code: {})",
3784                error.message, error.code
3785            )));
3786        }
3787
3788        if let Some(result) = rpc_response.result {
3789            if let Some(address) = result.as_str() {
3790                tracing::info!("Generated new {} address: {}", addr_type, address);
3791                return Ok(address.to_string());
3792            }
3793        }
3794
3795        Err(AmpError::rpc(format!(
3796            "Failed to get new address from wallet '{wallet_name}': unexpected response format"
3797        )))
3798    }
3799
3800    /// Get the confidential version of an address from Elements wallet
3801    ///
3802    /// This method takes a regular (unconfidential) address and returns its confidential
3803    /// counterpart, which includes blinding keys for confidential transactions.
3804    ///
3805    /// # Arguments
3806    ///
3807    /// * `wallet_name` - Name of the Elements wallet
3808    /// * `address` - The unconfidential address to get info for
3809    ///
3810    /// # Returns
3811    ///
3812    /// Returns the confidential address string
3813    ///
3814    /// # Example
3815    ///
3816    /// ```no_run
3817    /// # use amp_rs::ElementsRpc;
3818    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
3819    /// let rpc = ElementsRpc::from_env()?;
3820    /// let unconfidential_address = "tex1q...";
3821    /// // Note: This would need to be called in an async context
3822    /// // let confidential_address = rpc.get_confidential_address("test_wallet", unconfidential_address).await?;
3823    /// // println!("Confidential address: {}", confidential_address);
3824    /// # Ok(())
3825    /// # }
3826    /// ```
3827    /// Gets the confidential address for a given unconfidential address from a wallet
3828    ///
3829    /// # Errors
3830    /// Returns an error if the RPC call fails or the response format is unexpected
3831    pub async fn get_confidential_address(
3832        &self,
3833        wallet_name: &str,
3834        address: &str,
3835    ) -> Result<String, AmpError> {
3836        // First load the wallet to ensure it's available
3837        self.load_wallet(wallet_name).await?;
3838
3839        let params = serde_json::json!([address]);
3840
3841        // Create RPC request for getaddressinfo
3842        let request = RpcRequest {
3843            jsonrpc: "1.0".to_string(),
3844            id: "amp-client".to_string(),
3845            method: "getaddressinfo".to_string(),
3846            params,
3847        };
3848
3849        // Use the wallet-specific RPC endpoint
3850        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
3851
3852        let response = self
3853            .client
3854            .post(&wallet_url)
3855            .basic_auth(&self.username, Some(&self.password))
3856            .json(&request)
3857            .send()
3858            .await
3859            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
3860
3861        if !response.status().is_success() {
3862            let status = response.status();
3863            let error_body = response
3864                .text()
3865                .await
3866                .unwrap_or_else(|_| "Unable to read error body".to_string());
3867            return Err(AmpError::rpc(format!(
3868                "RPC request failed with status: {status} - Body: {error_body}"
3869            )));
3870        }
3871
3872        let rpc_response: RpcResponse<serde_json::Value> = response
3873            .json()
3874            .await
3875            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
3876
3877        if let Some(error) = rpc_response.error {
3878            return Err(AmpError::rpc(format!(
3879                "RPC error getting address info: {} (code: {})",
3880                error.message, error.code
3881            )));
3882        }
3883
3884        if let Some(result) = rpc_response.result {
3885            if let Some(confidential_address) = result.get("confidential").and_then(|v| v.as_str())
3886            {
3887                tracing::info!("Retrieved confidential address for: {}", address);
3888                return Ok(confidential_address.to_string());
3889            }
3890        }
3891
3892        Err(AmpError::rpc(format!(
3893            "Failed to get confidential address for '{address}': unexpected response format"
3894        )))
3895    }
3896
3897    /// Get the private key for an address from Elements wallet
3898    ///
3899    /// This method exports the private key for a specific address from the Elements wallet.
3900    /// The private key can then be imported into LWK for signing.
3901    ///
3902    /// Note: This is a simplified implementation that returns a placeholder private key.
3903    /// For production use, implement proper wallet-specific RPC calls.
3904    ///
3905    /// # Arguments
3906    /// * `wallet_name` - Name of the wallet containing the address
3907    /// * `address` - The address to get the private key for
3908    ///
3909    /// # Errors
3910    /// Returns an error if the RPC call fails
3911    ///
3912    /// # Examples
3913    /// ```no_run
3914    /// # use amp_rs::ElementsRpc;
3915    /// # #[tokio::main]
3916    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3917    /// let rpc = ElementsRpc::from_env()?;
3918    /// let address = rpc.get_new_address("test_wallet", None).await?;
3919    /// let private_key = rpc.dump_private_key("test_wallet", &address).await?;
3920    /// println!("Private key: {}", private_key);
3921    /// # Ok(())
3922    /// # }
3923    /// ```
3924    pub async fn dump_private_key(
3925        &self,
3926        wallet_name: &str,
3927        address: &str,
3928    ) -> Result<String, AmpError> {
3929        // First load the wallet to ensure it's available
3930        self.load_wallet(wallet_name).await?;
3931
3932        let params = serde_json::json!([address]);
3933
3934        // Create RPC request for dumpprivkey
3935        let request = RpcRequest {
3936            jsonrpc: "1.0".to_string(),
3937            id: "amp-client".to_string(),
3938            method: "dumpprivkey".to_string(),
3939            params,
3940        };
3941
3942        // Use the wallet-specific RPC endpoint
3943        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
3944
3945        let response = self
3946            .client
3947            .post(&wallet_url)
3948            .basic_auth(&self.username, Some(&self.password))
3949            .json(&request)
3950            .send()
3951            .await
3952            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
3953
3954        if !response.status().is_success() {
3955            return Err(AmpError::rpc(format!(
3956                "RPC request failed with status: {}",
3957                response.status()
3958            )));
3959        }
3960
3961        let rpc_response: RpcResponse<serde_json::Value> = response
3962            .json()
3963            .await
3964            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
3965
3966        if let Some(error) = rpc_response.error {
3967            return Err(AmpError::rpc(format!(
3968                "RPC error dumping private key: {} (code: {})",
3969                error.message, error.code
3970            )));
3971        }
3972
3973        if let Some(result) = rpc_response.result {
3974            if let Some(private_key) = result.as_str() {
3975                tracing::info!("Successfully exported private key for address: {}", address);
3976                return Ok(private_key.to_string());
3977            }
3978        }
3979
3980        Err(AmpError::rpc(format!(
3981            "Failed to dump private key for address '{address}': unexpected response format"
3982        )))
3983    }
3984
3985    /// Creates a descriptor wallet in Elements
3986    ///
3987    /// # Arguments
3988    /// * `wallet_name` - Name for the new wallet
3989    ///
3990    /// # Errors
3991    /// Returns an error if the RPC call fails
3992    ///
3993    /// # Examples
3994    /// ```no_run
3995    /// # use amp_rs::ElementsRpc;
3996    /// # #[tokio::main]
3997    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3998    /// let rpc = ElementsRpc::from_env()?;
3999    /// rpc.create_descriptor_wallet("test_wallet").await?;
4000    /// # Ok(())
4001    /// # }
4002    /// ```
4003    pub async fn create_descriptor_wallet(&self, wallet_name: &str) -> Result<(), AmpError> {
4004        let params = serde_json::json!([wallet_name, true]); // true enables descriptors
4005
4006        let _result: serde_json::Value = self.rpc_call("createwallet", params).await?;
4007
4008        tracing::info!("Successfully created descriptor wallet: {}", wallet_name);
4009        Ok(())
4010    }
4011
4012    /// Imports a single descriptor into an Elements wallet
4013    ///
4014    /// This method imports a descriptor that enables the wallet to scan and recognize
4015    /// addresses/UTXOs from a mnemonic. For LWK descriptors with `<0;1>/*` format,
4016    /// a single descriptor covers both receive and change addresses.
4017    ///
4018    /// # Arguments
4019    /// * `wallet_name` - Name of the wallet to import descriptor into
4020    /// * `descriptor` - The descriptor to import
4021    ///
4022    /// # Errors
4023    /// Returns an error if the RPC call fails
4024    ///
4025    /// # Examples
4026    /// ```no_run
4027    /// # use amp_rs::ElementsRpc;
4028    /// # #[tokio::main]
4029    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4030    /// let rpc = ElementsRpc::from_env()?;
4031    /// let descriptor = "ct(slip77(...),elwpkh([...]/84h/1h/0h]tpub.../<0;1>/*))#checksum";
4032    /// rpc.import_descriptor("test_wallet", descriptor).await?;
4033    /// # Ok(())
4034    /// # }
4035    /// ```
4036    pub async fn import_descriptor(
4037        &self,
4038        wallet_name: &str,
4039        descriptor: &str,
4040    ) -> Result<(), AmpError> {
4041        tracing::info!("Importing descriptor into wallet: {}", wallet_name);
4042        tracing::debug!("Descriptor: {}", descriptor);
4043
4044        let descriptors = serde_json::json!([
4045            {
4046                "desc": descriptor,
4047                "timestamp": "now",
4048                "active": true,
4049                "internal": false  // For LWK descriptors with <0;1>/*, this covers both chains
4050            }
4051        ]);
4052
4053        // Use -rpcwallet parameter to specify the wallet
4054        let request = RpcRequest {
4055            jsonrpc: "1.0".to_string(),
4056            id: "amp-client".to_string(),
4057            method: "importdescriptors".to_string(),
4058            params: descriptors,
4059        };
4060
4061        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
4062
4063        let response = self
4064            .client
4065            .post(&wallet_url)
4066            .basic_auth(&self.username, Some(&self.password))
4067            .json(&request)
4068            .send()
4069            .await
4070            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
4071
4072        if !response.status().is_success() {
4073            return Err(AmpError::rpc(format!(
4074                "RPC request failed with status: {}",
4075                response.status()
4076            )));
4077        }
4078
4079        let rpc_response: RpcResponse<serde_json::Value> = response
4080            .json()
4081            .await
4082            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
4083
4084        if let Some(error) = rpc_response.error {
4085            return Err(AmpError::rpc(format!(
4086                "RPC error {}: {}",
4087                error.code, error.message
4088            )));
4089        }
4090
4091        let result = rpc_response
4092            .result
4093            .ok_or_else(|| AmpError::rpc("RPC response missing result field".to_string()))?;
4094
4095        // Check if descriptor was imported successfully
4096        if let Some(results) = result.as_array() {
4097            if let Some(result) = results.first() {
4098                if let Some(success) = result.get("success").and_then(serde_json::Value::as_bool) {
4099                    if !success {
4100                        let error_msg = result
4101                            .get("error")
4102                            .and_then(|e| e.get("message"))
4103                            .and_then(|m| m.as_str())
4104                            .unwrap_or("Unknown error");
4105                        return Err(AmpError::rpc(format!(
4106                            "Failed to import descriptor: {error_msg}"
4107                        )));
4108                    }
4109                } else {
4110                    return Err(AmpError::rpc(format!(
4111                        "Invalid response format for descriptor import: {result:?}"
4112                    )));
4113                }
4114            }
4115        } else {
4116            return Err(AmpError::rpc(format!(
4117                "Invalid response format: expected array, got {result:?}"
4118            )));
4119        }
4120
4121        tracing::info!(
4122            "Successfully imported descriptor into wallet: {}",
4123            wallet_name
4124        );
4125        Ok(())
4126    }
4127
4128    /// Imports descriptors into an Elements wallet (legacy method for compatibility)
4129    ///
4130    /// This method imports descriptors that enable the wallet to scan and recognize
4131    /// addresses/UTXOs from a mnemonic. If both descriptors are the same (as with LWK
4132    /// descriptors using `<0;1>/*` format), only one descriptor is imported.
4133    ///
4134    /// # Arguments
4135    /// * `wallet_name` - Name of the wallet to import descriptors into
4136    /// * `receive_descriptor` - The receive descriptor
4137    /// * `change_descriptor` - The change descriptor
4138    ///
4139    /// # Errors
4140    /// Returns an error if the RPC call fails
4141    ///
4142    /// # Examples
4143    /// ```no_run
4144    /// # use amp_rs::ElementsRpc;
4145    /// # #[tokio::main]
4146    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4147    /// let rpc = ElementsRpc::from_env()?;
4148    /// let descriptor = "ct(slip77(...),elwpkh([...]/84h/1h/0h]tpub.../<0;1>/*))#checksum";
4149    /// rpc.import_descriptors("test_wallet", descriptor, descriptor).await?;
4150    /// # Ok(())
4151    /// # }
4152    /// ```
4153    #[allow(clippy::cognitive_complexity)]
4154    pub async fn import_descriptors(
4155        &self,
4156        wallet_name: &str,
4157        receive_descriptor: &str,
4158        change_descriptor: &str,
4159    ) -> Result<(), AmpError> {
4160        // If both descriptors are the same (LWK case), import only once
4161        if receive_descriptor == change_descriptor {
4162            return self
4163                .import_descriptor(wallet_name, receive_descriptor)
4164                .await;
4165        }
4166
4167        tracing::info!(
4168            "Importing separate receive and change descriptors into wallet: {}",
4169            wallet_name
4170        );
4171        tracing::debug!("Receive descriptor: {}", receive_descriptor);
4172        tracing::debug!("Change descriptor: {}", change_descriptor);
4173
4174        let descriptors = serde_json::json!([
4175            {
4176                "desc": receive_descriptor,
4177                "timestamp": "now",
4178                "active": true,
4179                "internal": false
4180            },
4181            {
4182                "desc": change_descriptor,
4183                "timestamp": "now",
4184                "active": true,
4185                "internal": true
4186            }
4187        ]);
4188
4189        // Use -rpcwallet parameter to specify the wallet
4190        let request = RpcRequest {
4191            jsonrpc: "1.0".to_string(),
4192            id: "amp-client".to_string(),
4193            method: "importdescriptors".to_string(),
4194            params: descriptors,
4195        };
4196
4197        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
4198
4199        let response = self
4200            .client
4201            .post(&wallet_url)
4202            .basic_auth(&self.username, Some(&self.password))
4203            .json(&request)
4204            .send()
4205            .await
4206            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
4207
4208        if !response.status().is_success() {
4209            return Err(AmpError::rpc(format!(
4210                "RPC request failed with status: {}",
4211                response.status()
4212            )));
4213        }
4214
4215        let rpc_response: RpcResponse<serde_json::Value> = response
4216            .json()
4217            .await
4218            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
4219
4220        if let Some(error) = rpc_response.error {
4221            return Err(AmpError::rpc(format!(
4222                "RPC error {}: {}",
4223                error.code, error.message
4224            )));
4225        }
4226
4227        let result = rpc_response
4228            .result
4229            .ok_or_else(|| AmpError::rpc("RPC response missing result field".to_string()))?;
4230
4231        // Check if both descriptors were imported successfully
4232        if let Some(results) = result.as_array() {
4233            for (i, result) in results.iter().enumerate() {
4234                if let Some(success) = result.get("success").and_then(serde_json::Value::as_bool) {
4235                    if !success {
4236                        let desc_type = if i == 0 { "receive" } else { "change" };
4237                        let error_msg = result
4238                            .get("error")
4239                            .and_then(|e| e.get("message"))
4240                            .and_then(|m| m.as_str())
4241                            .unwrap_or("Unknown error");
4242                        return Err(AmpError::rpc(format!(
4243                            "Failed to import {desc_type} descriptor: {error_msg}"
4244                        )));
4245                    }
4246                } else {
4247                    return Err(AmpError::rpc(format!(
4248                        "Invalid response format for descriptor import: {result:?}"
4249                    )));
4250                }
4251            }
4252        } else {
4253            return Err(AmpError::rpc(format!(
4254                "Invalid response format: expected array, got {result:?}"
4255            )));
4256        }
4257
4258        tracing::info!(
4259            "Successfully imported descriptors into wallet: {}",
4260            wallet_name
4261        );
4262        Ok(())
4263    }
4264
4265    /// Sets up a wallet with descriptors from a mnemonic
4266    ///
4267    /// This is a convenience method that combines wallet creation and descriptor import.
4268    /// It creates a descriptor wallet and imports the receive and change descriptors
4269    /// generated from the provided mnemonic.
4270    ///
4271    /// # Arguments
4272    /// * `wallet_name` - Name for the new wallet
4273    /// * `receive_descriptor` - The receive descriptor (external chain /0/*)
4274    /// * `change_descriptor` - The change descriptor (internal chain /1/*)
4275    ///
4276    /// # Errors
4277    /// Returns an error if wallet creation or descriptor import fails
4278    ///
4279    /// # Examples
4280    /// ```no_run
4281    /// # use amp_rs::ElementsRpc;
4282    /// # #[tokio::main]
4283    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4284    /// let rpc = ElementsRpc::from_env()?;
4285    /// let receive_desc = "wpkh([d34db33f/84h/1h/0h]xprv.../0/*)#checksum";
4286    /// let change_desc = "wpkh([d34db33f/84h/1h/0h]xprv.../1/*)#checksum";
4287    /// rpc.setup_wallet_with_descriptors("test_wallet", receive_desc, change_desc).await?;
4288    /// # Ok(())
4289    /// # }
4290    /// ```
4291    #[allow(clippy::cognitive_complexity)]
4292    pub async fn setup_wallet_with_descriptors(
4293        &self,
4294        wallet_name: &str,
4295        receive_descriptor: &str,
4296        change_descriptor: &str,
4297    ) -> Result<(), AmpError> {
4298        tracing::info!("Setting up wallet with descriptors: {}", wallet_name);
4299
4300        // Try to create the wallet (may fail if it already exists)
4301        match self.create_descriptor_wallet(wallet_name).await {
4302            Ok(()) => {
4303                tracing::info!("Created new descriptor wallet: {}", wallet_name);
4304            }
4305            Err(e) => {
4306                let error_msg = e.to_string();
4307                if error_msg.contains("already exists")
4308                    || error_msg.contains("Database already exists")
4309                {
4310                    tracing::info!(
4311                        "Wallet {} already exists, proceeding with descriptor import",
4312                        wallet_name
4313                    );
4314                } else {
4315                    return Err(e);
4316                }
4317            }
4318        }
4319
4320        // Import the descriptors
4321        self.import_descriptors(wallet_name, receive_descriptor, change_descriptor)
4322            .await?;
4323
4324        tracing::info!(
4325            "Successfully set up wallet with descriptors: {}",
4326            wallet_name
4327        );
4328        Ok(())
4329    }
4330
4331    /// Exports a wallet to a file using dumpwallet RPC
4332    ///
4333    /// # Arguments
4334    /// * `wallet_name` - Name of the wallet to export
4335    /// * `file_path` - Path where the wallet dump file will be created
4336    ///
4337    /// # Errors
4338    /// Returns an error if the RPC call fails or the wallet cannot be exported
4339    ///
4340    /// # Examples
4341    /// ```no_run
4342    /// # use amp_rs::ElementsRpc;
4343    /// # #[tokio::main]
4344    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4345    /// let rpc = ElementsRpc::from_env()?;
4346    /// rpc.dump_wallet("my_wallet", "/tmp/wallet_export.dat").await?;
4347    /// # Ok(())
4348    /// # }
4349    /// ```
4350    pub async fn dump_wallet(&self, wallet_name: &str, file_path: &str) -> Result<(), AmpError> {
4351        // First load the wallet to ensure it's available
4352        self.load_wallet(wallet_name).await?;
4353
4354        let params = serde_json::json!([file_path]);
4355
4356        // Create RPC request for dumpwallet
4357        let request = RpcRequest {
4358            jsonrpc: "1.0".to_string(),
4359            id: "amp-client".to_string(),
4360            method: "dumpwallet".to_string(),
4361            params,
4362        };
4363
4364        // Use the wallet-specific RPC endpoint
4365        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
4366
4367        let response = self
4368            .client
4369            .post(&wallet_url)
4370            .basic_auth(&self.username, Some(&self.password))
4371            .json(&request)
4372            .send()
4373            .await
4374            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
4375
4376        if !response.status().is_success() {
4377            return Err(AmpError::rpc(format!(
4378                "RPC request failed with status: {}",
4379                response.status()
4380            )));
4381        }
4382
4383        let rpc_response: RpcResponse<serde_json::Value> = response
4384            .json()
4385            .await
4386            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
4387
4388        if let Some(error) = rpc_response.error {
4389            return Err(AmpError::rpc(format!(
4390                "RPC error dumping wallet: {} (code: {})",
4391                error.message, error.code
4392            )));
4393        }
4394
4395        tracing::info!(
4396            "Successfully exported wallet {} to {}",
4397            wallet_name,
4398            file_path
4399        );
4400        Ok(())
4401    }
4402
4403    /// Imports a wallet from a file using importwallet RPC
4404    ///
4405    /// # Arguments
4406    /// * `wallet_name` - Name of the wallet to import into
4407    /// * `file_path` - Path to the wallet dump file to import
4408    ///
4409    /// # Errors
4410    /// Returns an error if the RPC call fails or the wallet cannot be imported
4411    ///
4412    /// # Examples
4413    /// ```no_run
4414    /// # use amp_rs::ElementsRpc;
4415    /// # #[tokio::main]
4416    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4417    /// let rpc = ElementsRpc::from_env()?;
4418    /// rpc.import_wallet("my_wallet", "/tmp/wallet_export.dat").await?;
4419    /// # Ok(())
4420    /// # }
4421    /// ```
4422    pub async fn import_wallet(&self, wallet_name: &str, file_path: &str) -> Result<(), AmpError> {
4423        // First load the wallet to ensure it's available
4424        self.load_wallet(wallet_name).await?;
4425
4426        let params = serde_json::json!([file_path]);
4427
4428        // Create RPC request for importwallet
4429        let request = RpcRequest {
4430            jsonrpc: "1.0".to_string(),
4431            id: "amp-client".to_string(),
4432            method: "importwallet".to_string(),
4433            params,
4434        };
4435
4436        // Use the wallet-specific RPC endpoint
4437        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
4438
4439        let response = self
4440            .client
4441            .post(&wallet_url)
4442            .basic_auth(&self.username, Some(&self.password))
4443            .json(&request)
4444            .send()
4445            .await
4446            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
4447
4448        if !response.status().is_success() {
4449            return Err(AmpError::rpc(format!(
4450                "RPC request failed with status: {}",
4451                response.status()
4452            )));
4453        }
4454
4455        let rpc_response: RpcResponse<serde_json::Value> = response
4456            .json()
4457            .await
4458            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
4459
4460        if let Some(error) = rpc_response.error {
4461            return Err(AmpError::rpc(format!(
4462                "RPC error importing wallet: {} (code: {})",
4463                error.message, error.code
4464            )));
4465        }
4466
4467        tracing::info!(
4468            "Successfully imported wallet {} from {}",
4469            wallet_name,
4470            file_path
4471        );
4472        Ok(())
4473    }
4474
4475    /// Exports a blinding key for a confidential address using dumpblindingkey RPC
4476    ///
4477    /// # Arguments
4478    /// * `wallet_name` - Name of the wallet containing the address
4479    /// * `address` - The confidential address to export the blinding key for
4480    ///
4481    /// # Errors
4482    /// Returns an error if the RPC call fails or the address doesn't have a blinding key
4483    ///
4484    /// # Examples
4485    /// ```no_run
4486    /// # use amp_rs::ElementsRpc;
4487    /// # #[tokio::main]
4488    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4489    /// let rpc = ElementsRpc::from_env()?;
4490    /// let key = rpc.dump_blinding_key("my_wallet", "VTpz...").await?;
4491    /// println!("Blinding key: {}", key);
4492    /// # Ok(())
4493    /// # }
4494    /// ```
4495    pub async fn dump_blinding_key(
4496        &self,
4497        wallet_name: &str,
4498        address: &str,
4499    ) -> Result<String, AmpError> {
4500        // First load the wallet to ensure it's available
4501        self.load_wallet(wallet_name).await?;
4502
4503        let params = serde_json::json!([address]);
4504
4505        // Create RPC request for dumpblindingkey
4506        let request = RpcRequest {
4507            jsonrpc: "1.0".to_string(),
4508            id: "amp-client".to_string(),
4509            method: "dumpblindingkey".to_string(),
4510            params,
4511        };
4512
4513        // Use the wallet-specific RPC endpoint
4514        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
4515
4516        let response = self
4517            .client
4518            .post(&wallet_url)
4519            .basic_auth(&self.username, Some(&self.password))
4520            .json(&request)
4521            .send()
4522            .await
4523            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
4524
4525        if !response.status().is_success() {
4526            return Err(AmpError::rpc(format!(
4527                "RPC request failed with status: {}",
4528                response.status()
4529            )));
4530        }
4531
4532        let rpc_response: RpcResponse<serde_json::Value> = response
4533            .json()
4534            .await
4535            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
4536
4537        if let Some(error) = rpc_response.error {
4538            return Err(AmpError::rpc(format!(
4539                "RPC error dumping blinding key: {} (code: {})",
4540                error.message, error.code
4541            )));
4542        }
4543
4544        if let Some(result) = rpc_response.result {
4545            if let Some(blinding_key) = result.as_str() {
4546                tracing::info!(
4547                    "Successfully exported blinding key for address: {}",
4548                    address
4549                );
4550                return Ok(blinding_key.to_string());
4551            }
4552        }
4553
4554        Err(AmpError::rpc(format!(
4555            "Failed to dump blinding key for address '{address}': unexpected response format"
4556        )))
4557    }
4558
4559    /// Imports a blinding key for a confidential address using importblindingkey RPC
4560    ///
4561    /// # Arguments
4562    /// * `wallet_name` - Name of the wallet to import the blinding key into
4563    /// * `address` - The confidential address to import the blinding key for
4564    /// * `blinding_key` - The blinding key to import
4565    ///
4566    /// # Errors
4567    /// Returns an error if the RPC call fails or the blinding key cannot be imported
4568    ///
4569    /// # Examples
4570    /// ```no_run
4571    /// # use amp_rs::ElementsRpc;
4572    /// # #[tokio::main]
4573    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4574    /// let rpc = ElementsRpc::from_env()?;
4575    /// rpc.import_blinding_key("my_wallet", "VTpz...", "blinding_key_hex").await?;
4576    /// # Ok(())
4577    /// # }
4578    /// ```
4579    pub async fn import_blinding_key(
4580        &self,
4581        wallet_name: &str,
4582        address: &str,
4583        blinding_key: &str,
4584    ) -> Result<(), AmpError> {
4585        // First load the wallet to ensure it's available
4586        self.load_wallet(wallet_name).await?;
4587
4588        let params = serde_json::json!([address, blinding_key]);
4589
4590        // Create RPC request for importblindingkey
4591        let request = RpcRequest {
4592            jsonrpc: "1.0".to_string(),
4593            id: "amp-client".to_string(),
4594            method: "importblindingkey".to_string(),
4595            params,
4596        };
4597
4598        // Use the wallet-specific RPC endpoint
4599        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
4600
4601        let response = self
4602            .client
4603            .post(&wallet_url)
4604            .basic_auth(&self.username, Some(&self.password))
4605            .json(&request)
4606            .send()
4607            .await
4608            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
4609
4610        if !response.status().is_success() {
4611            return Err(AmpError::rpc(format!(
4612                "RPC request failed with status: {}",
4613                response.status()
4614            )));
4615        }
4616
4617        let rpc_response: RpcResponse<serde_json::Value> = response
4618            .json()
4619            .await
4620            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
4621
4622        if let Some(error) = rpc_response.error {
4623            return Err(AmpError::rpc(format!(
4624                "RPC error importing blinding key: {} (code: {})",
4625                error.message, error.code
4626            )));
4627        }
4628
4629        tracing::info!(
4630            "Successfully imported blinding key for address: {}",
4631            address
4632        );
4633        Ok(())
4634    }
4635
4636    /// Gets wallet information using getwalletinfo RPC
4637    ///
4638    /// # Arguments
4639    /// * `wallet_name` - Name of the wallet to get information for
4640    ///
4641    /// # Errors
4642    /// Returns an error if the RPC call fails
4643    ///
4644    /// # Examples
4645    /// ```no_run
4646    /// # use amp_rs::ElementsRpc;
4647    /// # #[tokio::main]
4648    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4649    /// let rpc = ElementsRpc::from_env()?;
4650    /// let info = rpc.get_wallet_info("my_wallet").await?;
4651    /// println!("Wallet info: {:?}", info);
4652    /// # Ok(())
4653    /// # }
4654    /// ```
4655    pub async fn get_wallet_info(&self, wallet_name: &str) -> Result<serde_json::Value, AmpError> {
4656        // First load the wallet to ensure it's available
4657        self.load_wallet(wallet_name).await?;
4658
4659        let params = serde_json::json!([]);
4660
4661        // Create RPC request for getwalletinfo
4662        let request = RpcRequest {
4663            jsonrpc: "1.0".to_string(),
4664            id: "amp-client".to_string(),
4665            method: "getwalletinfo".to_string(),
4666            params,
4667        };
4668
4669        // Use the wallet-specific RPC endpoint
4670        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
4671
4672        let response = self
4673            .client
4674            .post(&wallet_url)
4675            .basic_auth(&self.username, Some(&self.password))
4676            .json(&request)
4677            .send()
4678            .await
4679            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
4680
4681        if !response.status().is_success() {
4682            return Err(AmpError::rpc(format!(
4683                "RPC request failed with status: {}",
4684                response.status()
4685            )));
4686        }
4687
4688        let rpc_response: RpcResponse<serde_json::Value> = response
4689            .json()
4690            .await
4691            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
4692
4693        if let Some(error) = rpc_response.error {
4694            return Err(AmpError::rpc(format!(
4695                "RPC error getting wallet info: {} (code: {})",
4696                error.message, error.code
4697            )));
4698        }
4699
4700        if let Some(result) = rpc_response.result {
4701            tracing::info!("Successfully retrieved wallet info for: {}", wallet_name);
4702            return Ok(result);
4703        }
4704
4705        Err(AmpError::rpc(format!(
4706            "Failed to get wallet info for '{wallet_name}': unexpected response format"
4707        )))
4708    }
4709
4710    /// Gets the unconfidential address for a confidential address
4711    ///
4712    /// # Arguments
4713    /// * `wallet_name` - Name of the wallet
4714    /// * `confidential_address` - The confidential address to convert
4715    ///
4716    /// # Errors
4717    /// Returns an error if the RPC call fails
4718    ///
4719    /// # Examples
4720    /// ```no_run
4721    /// # use amp_rs::ElementsRpc;
4722    /// # #[tokio::main]
4723    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4724    /// let rpc = ElementsRpc::from_env()?;
4725    /// let unconf = rpc.get_unconfidential_address("my_wallet", "VTpz...").await?;
4726    /// println!("Unconfidential address: {}", unconf);
4727    /// # Ok(())
4728    /// # }
4729    /// ```
4730    pub async fn get_unconfidential_address(
4731        &self,
4732        wallet_name: &str,
4733        confidential_address: &str,
4734    ) -> Result<String, AmpError> {
4735        // First load the wallet to ensure it's available
4736        self.load_wallet(wallet_name).await?;
4737
4738        let params = serde_json::json!([confidential_address]);
4739
4740        // Create RPC request for getunconfidentialaddress
4741        let request = RpcRequest {
4742            jsonrpc: "1.0".to_string(),
4743            id: "amp-client".to_string(),
4744            method: "getunconfidentialaddress".to_string(),
4745            params,
4746        };
4747
4748        // Use the wallet-specific RPC endpoint
4749        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
4750
4751        let response = self
4752            .client
4753            .post(&wallet_url)
4754            .basic_auth(&self.username, Some(&self.password))
4755            .json(&request)
4756            .send()
4757            .await
4758            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
4759
4760        if !response.status().is_success() {
4761            return Err(AmpError::rpc(format!(
4762                "RPC request failed with status: {}",
4763                response.status()
4764            )));
4765        }
4766
4767        let rpc_response: RpcResponse<serde_json::Value> = response
4768            .json()
4769            .await
4770            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
4771
4772        if let Some(error) = rpc_response.error {
4773            return Err(AmpError::rpc(format!(
4774                "RPC error getting unconfidential address: {} (code: {})",
4775                error.message, error.code
4776            )));
4777        }
4778
4779        if let Some(result) = rpc_response.result {
4780            if let Some(address) = result.as_str() {
4781                tracing::info!(
4782                    "Successfully got unconfidential address for: {}",
4783                    confidential_address
4784                );
4785                return Ok(address.to_string());
4786            }
4787        }
4788
4789        Err(AmpError::rpc(format!(
4790            "Failed to get unconfidential address for '{confidential_address}': unexpected response format"
4791        )))
4792    }
4793
4794    /// Imports a private key into the wallet using importprivkey RPC
4795    ///
4796    /// # Arguments
4797    /// * `wallet_name` - Name of the wallet to import into
4798    /// * `private_key` - The private key in WIF format
4799    /// * `label` - Optional label for the address
4800    /// * `rescan` - Whether to rescan the blockchain for transactions
4801    ///
4802    /// # Errors
4803    /// Returns an error if the RPC call fails
4804    ///
4805    /// # Examples
4806    /// ```no_run
4807    /// # use amp_rs::ElementsRpc;
4808    /// # #[tokio::main]
4809    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4810    /// let rpc = ElementsRpc::from_env()?;
4811    /// rpc.import_private_key("my_wallet", "cT1...", Some("my_address"), Some(false)).await?;
4812    /// # Ok(())
4813    /// # }
4814    /// ```
4815    pub async fn import_private_key(
4816        &self,
4817        wallet_name: &str,
4818        private_key: &str,
4819        label: Option<&str>,
4820        rescan: Option<bool>,
4821    ) -> Result<(), AmpError> {
4822        // First load the wallet to ensure it's available
4823        self.load_wallet(wallet_name).await?;
4824
4825        let params = serde_json::json!([private_key, label.unwrap_or(""), rescan.unwrap_or(false)]);
4826
4827        // Create RPC request for importprivkey
4828        let request = RpcRequest {
4829            jsonrpc: "1.0".to_string(),
4830            id: "amp-client".to_string(),
4831            method: "importprivkey".to_string(),
4832            params,
4833        };
4834
4835        // Use the wallet-specific RPC endpoint
4836        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
4837
4838        let response = self
4839            .client
4840            .post(&wallet_url)
4841            .basic_auth(&self.username, Some(&self.password))
4842            .json(&request)
4843            .send()
4844            .await
4845            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
4846
4847        if !response.status().is_success() {
4848            return Err(AmpError::rpc(format!(
4849                "RPC request failed with status: {}",
4850                response.status()
4851            )));
4852        }
4853
4854        let rpc_response: RpcResponse<serde_json::Value> = response
4855            .json()
4856            .await
4857            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
4858
4859        if let Some(error) = rpc_response.error {
4860            return Err(AmpError::rpc(format!(
4861                "RPC error importing private key: {} (code: {})",
4862                error.message, error.code
4863            )));
4864        }
4865
4866        tracing::info!("Successfully imported private key");
4867        Ok(())
4868    }
4869
4870    /// Lists all descriptors in a wallet using listdescriptors RPC
4871    ///
4872    /// # Arguments
4873    /// * `wallet_name` - Name of the wallet
4874    /// * `private_keys` - Whether to include private keys in the output
4875    ///
4876    /// # Errors
4877    /// Returns an error if the RPC call fails
4878    ///
4879    /// # Examples
4880    /// ```no_run
4881    /// # use amp_rs::ElementsRpc;
4882    /// # #[tokio::main]
4883    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4884    /// let rpc = ElementsRpc::from_env()?;
4885    /// let descriptors = rpc.list_descriptors("my_wallet", Some(true)).await?;
4886    /// for desc in descriptors {
4887    ///     println!("Descriptor: {}", desc);
4888    /// }
4889    /// # Ok(())
4890    /// # }
4891    /// ```
4892    pub async fn list_descriptors(
4893        &self,
4894        wallet_name: &str,
4895        private_keys: Option<bool>,
4896    ) -> Result<Vec<String>, AmpError> {
4897        // First load the wallet to ensure it's available
4898        self.load_wallet(wallet_name).await?;
4899
4900        let params = serde_json::json!([private_keys.unwrap_or(false)]);
4901
4902        // Create RPC request for listdescriptors
4903        let request = RpcRequest {
4904            jsonrpc: "1.0".to_string(),
4905            id: "amp-client".to_string(),
4906            method: "listdescriptors".to_string(),
4907            params,
4908        };
4909
4910        // Use the wallet-specific RPC endpoint
4911        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
4912
4913        let response = self
4914            .client
4915            .post(&wallet_url)
4916            .basic_auth(&self.username, Some(&self.password))
4917            .json(&request)
4918            .send()
4919            .await
4920            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
4921
4922        if !response.status().is_success() {
4923            return Err(AmpError::rpc(format!(
4924                "RPC request failed with status: {}",
4925                response.status()
4926            )));
4927        }
4928
4929        let rpc_response: RpcResponse<serde_json::Value> = response
4930            .json()
4931            .await
4932            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
4933
4934        if let Some(error) = rpc_response.error {
4935            return Err(AmpError::rpc(format!(
4936                "RPC error listing descriptors: {} (code: {})",
4937                error.message, error.code
4938            )));
4939        }
4940
4941        if let Some(result) = rpc_response.result {
4942            // Result has a "descriptors" array with objects containing "desc" field
4943            if let Some(descriptors_array) = result.get("descriptors").and_then(|v| v.as_array()) {
4944                let descriptors: Vec<String> = descriptors_array
4945                    .iter()
4946                    .filter_map(|d| d.get("desc").and_then(|v| v.as_str()).map(String::from))
4947                    .collect();
4948                tracing::info!(
4949                    "Successfully retrieved {} descriptors for wallet: {}",
4950                    descriptors.len(),
4951                    wallet_name
4952                );
4953                return Ok(descriptors);
4954            }
4955        }
4956
4957        Ok(Vec::new())
4958    }
4959
4960    /// Gets all addresses in a wallet by label using getaddressesbylabel RPC
4961    ///
4962    /// # Arguments
4963    /// * `wallet_name` - Name of the wallet
4964    /// * `label` - Label to filter by (empty string for all addresses)
4965    ///
4966    /// # Errors
4967    /// Returns an error if the RPC call fails
4968    ///
4969    /// # Examples
4970    /// ```no_run
4971    /// # use amp_rs::ElementsRpc;
4972    /// # #[tokio::main]
4973    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4974    /// let rpc = ElementsRpc::from_env()?;
4975    /// let addresses = rpc.get_addresses_by_label("my_wallet", "").await?;
4976    /// for addr in addresses {
4977    ///     println!("Address: {}", addr);
4978    /// }
4979    /// # Ok(())
4980    /// # }
4981    /// ```
4982    pub async fn get_addresses_by_label(
4983        &self,
4984        wallet_name: &str,
4985        label: &str,
4986    ) -> Result<Vec<String>, AmpError> {
4987        // First load the wallet to ensure it's available
4988        self.load_wallet(wallet_name).await?;
4989
4990        let params = serde_json::json!([label]);
4991
4992        // Create RPC request for getaddressesbylabel
4993        let request = RpcRequest {
4994            jsonrpc: "1.0".to_string(),
4995            id: "amp-client".to_string(),
4996            method: "getaddressesbylabel".to_string(),
4997            params,
4998        };
4999
5000        // Use the wallet-specific RPC endpoint
5001        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
5002
5003        let response = self
5004            .client
5005            .post(&wallet_url)
5006            .basic_auth(&self.username, Some(&self.password))
5007            .json(&request)
5008            .send()
5009            .await
5010            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
5011
5012        if !response.status().is_success() {
5013            return Err(AmpError::rpc(format!(
5014                "RPC request failed with status: {}",
5015                response.status()
5016            )));
5017        }
5018
5019        let rpc_response: RpcResponse<serde_json::Value> = response
5020            .json()
5021            .await
5022            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
5023
5024        if let Some(error) = rpc_response.error {
5025            return Err(AmpError::rpc(format!(
5026                "RPC error getting addresses by label: {} (code: {})",
5027                error.message, error.code
5028            )));
5029        }
5030
5031        if let Some(result) = rpc_response.result {
5032            // Result is an object with addresses as keys
5033            if let Some(obj) = result.as_object() {
5034                let addresses: Vec<String> = obj.keys().cloned().collect();
5035                tracing::info!(
5036                    "Successfully retrieved {} addresses for wallet: {}",
5037                    addresses.len(),
5038                    wallet_name
5039                );
5040                return Ok(addresses);
5041            }
5042        }
5043
5044        Ok(Vec::new())
5045    }
5046
5047    /// Lists addresses that have received transactions using listreceivedbyaddress RPC
5048    ///
5049    /// # Arguments
5050    /// * `wallet_name` - Name of the wallet to list addresses for
5051    /// * `min_conf` - Minimum number of confirmations (0 for unconfirmed)
5052    /// * `include_empty` - Whether to include addresses that haven't received payments
5053    ///
5054    /// # Errors
5055    /// Returns an error if the RPC call fails
5056    ///
5057    /// # Examples
5058    /// ```no_run
5059    /// # use amp_rs::ElementsRpc;
5060    /// # #[tokio::main]
5061    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
5062    /// let rpc = ElementsRpc::from_env()?;
5063    /// let addresses = rpc.list_received_by_address("my_wallet", 0, true).await?;
5064    /// for addr in addresses {
5065    ///     println!("Address: {:?}", addr);
5066    /// }
5067    /// # Ok(())
5068    /// # }
5069    /// ```
5070    pub async fn list_received_by_address(
5071        &self,
5072        wallet_name: &str,
5073        min_conf: u32,
5074        include_empty: bool,
5075    ) -> Result<Vec<ReceivedByAddress>, AmpError> {
5076        // First load the wallet to ensure it's available
5077        self.load_wallet(wallet_name).await?;
5078
5079        let params = serde_json::json!([min_conf, include_empty]);
5080
5081        // Create RPC request for listreceivedbyaddress
5082        let request = RpcRequest {
5083            jsonrpc: "1.0".to_string(),
5084            id: "amp-client".to_string(),
5085            method: "listreceivedbyaddress".to_string(),
5086            params,
5087        };
5088
5089        // Use the wallet-specific RPC endpoint
5090        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
5091
5092        let response = self
5093            .client
5094            .post(&wallet_url)
5095            .basic_auth(&self.username, Some(&self.password))
5096            .json(&request)
5097            .send()
5098            .await
5099            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
5100
5101        if !response.status().is_success() {
5102            return Err(AmpError::rpc(format!(
5103                "RPC request failed with status: {}",
5104                response.status()
5105            )));
5106        }
5107
5108        let rpc_response: RpcResponse<Vec<ReceivedByAddress>> = response
5109            .json()
5110            .await
5111            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
5112
5113        if let Some(error) = rpc_response.error {
5114            return Err(AmpError::rpc(format!(
5115                "RPC error listing received by address: {} (code: {})",
5116                error.message, error.code
5117            )));
5118        }
5119
5120        if let Some(result) = rpc_response.result {
5121            tracing::info!(
5122                "Successfully listed {} addresses for wallet: {}",
5123                result.len(),
5124                wallet_name
5125            );
5126            return Ok(result);
5127        }
5128
5129        Ok(Vec::new())
5130    }
5131}
5132
5133#[cfg(test)]
5134mod elements_rpc_tests {
5135    use super::*;
5136    use httpmock::prelude::*;
5137    use serial_test::serial;
5138    use std::collections::HashMap;
5139
5140    #[test]
5141    fn test_elements_rpc_new() {
5142        let rpc = ElementsRpc::new(
5143            "http://localhost:18884".to_string(),
5144            "user".to_string(),
5145            "pass".to_string(),
5146        );
5147
5148        assert_eq!(rpc.base_url, "http://localhost:18884");
5149        assert_eq!(rpc.username, "user");
5150        assert_eq!(rpc.password, "pass");
5151    }
5152
5153    #[test]
5154    #[serial]
5155    fn test_elements_rpc_from_env_missing_vars() {
5156        // Store original values to restore later
5157        let original_url = env::var("ELEMENTS_RPC_URL").ok();
5158        let original_user = env::var("ELEMENTS_RPC_USER").ok();
5159        let original_password = env::var("ELEMENTS_RPC_PASSWORD").ok();
5160
5161        // Clear environment variables to test error handling
5162        env::remove_var("ELEMENTS_RPC_URL");
5163        env::remove_var("ELEMENTS_RPC_USER");
5164        env::remove_var("ELEMENTS_RPC_PASSWORD");
5165
5166        let result = ElementsRpc::from_env();
5167        assert!(
5168            result.is_err(),
5169            "ElementsRpc::from_env() should fail when env vars are missing"
5170        );
5171
5172        match result.unwrap_err() {
5173            AmpError::Validation(msg) => {
5174                assert!(
5175                    msg.contains("ELEMENTS_RPC_URL"),
5176                    "Error message should mention missing ELEMENTS_RPC_URL"
5177                );
5178            }
5179            _ => panic!("Expected validation error"),
5180        }
5181
5182        // Restore original values or keep removed if they weren't set
5183        if let Some(val) = original_url {
5184            env::set_var("ELEMENTS_RPC_URL", val);
5185        }
5186        if let Some(val) = original_user {
5187            env::set_var("ELEMENTS_RPC_USER", val);
5188        }
5189        if let Some(val) = original_password {
5190            env::set_var("ELEMENTS_RPC_PASSWORD", val);
5191        }
5192    }
5193
5194    #[test]
5195    #[serial]
5196    fn test_elements_rpc_from_env_success() {
5197        // Store original values to restore later
5198        let original_url = env::var("ELEMENTS_RPC_URL").ok();
5199        let original_user = env::var("ELEMENTS_RPC_USER").ok();
5200        let original_password = env::var("ELEMENTS_RPC_PASSWORD").ok();
5201
5202        // Set test values
5203        env::set_var("ELEMENTS_RPC_URL", "http://localhost:18884");
5204        env::set_var("ELEMENTS_RPC_USER", "testuser");
5205        env::set_var("ELEMENTS_RPC_PASSWORD", "testpass");
5206
5207        let result = ElementsRpc::from_env();
5208        assert!(
5209            result.is_ok(),
5210            "ElementsRpc::from_env() should succeed when all env vars are set"
5211        );
5212
5213        let rpc = result.unwrap();
5214        assert_eq!(rpc.base_url, "http://localhost:18884");
5215        assert_eq!(rpc.username, "testuser");
5216        assert_eq!(rpc.password, "testpass");
5217
5218        // Restore original values or remove if they weren't set
5219        match original_url {
5220            Some(val) => env::set_var("ELEMENTS_RPC_URL", val),
5221            None => env::remove_var("ELEMENTS_RPC_URL"),
5222        }
5223        match original_user {
5224            Some(val) => env::set_var("ELEMENTS_RPC_USER", val),
5225            None => env::remove_var("ELEMENTS_RPC_USER"),
5226        }
5227        match original_password {
5228            Some(val) => env::set_var("ELEMENTS_RPC_PASSWORD", val),
5229            None => env::remove_var("ELEMENTS_RPC_PASSWORD"),
5230        }
5231    }
5232
5233    #[test]
5234    fn test_elements_rpc_method_signatures() {
5235        // Test that all new methods have correct signatures and can be called
5236        let rpc = ElementsRpc::new(
5237            "http://localhost:18884".to_string(),
5238            "user".to_string(),
5239            "pass".to_string(),
5240        );
5241
5242        // Test that methods exist and have correct signatures (compilation test)
5243        let _: std::pin::Pin<
5244            Box<dyn std::future::Future<Output = Result<Vec<Unspent>, AmpError>> + Send + '_>,
5245        > = Box::pin(rpc.list_unspent(Some("test_asset")));
5246
5247        let inputs = vec![TxInput {
5248            txid: "test_txid".to_string(),
5249            vout: 0,
5250            sequence: None,
5251        }];
5252        let outputs = std::collections::HashMap::new();
5253        let assets = std::collections::HashMap::new();
5254
5255        let _: std::pin::Pin<
5256            Box<dyn std::future::Future<Output = Result<String, AmpError>> + Send + '_>,
5257        > = Box::pin(rpc.create_raw_transaction(inputs, outputs, assets));
5258
5259        let _: std::pin::Pin<
5260            Box<dyn std::future::Future<Output = Result<String, AmpError>> + Send + '_>,
5261        > = Box::pin(rpc.send_raw_transaction("test_hex"));
5262
5263        let _: std::pin::Pin<
5264            Box<dyn std::future::Future<Output = Result<TransactionDetail, AmpError>> + Send + '_>,
5265        > = Box::pin(rpc.get_transaction("test_txid"));
5266    }
5267
5268    // Mock RPC response tests for UTXO and transaction operations
5269
5270    #[tokio::test]
5271    async fn test_get_network_info_success() {
5272        let server = MockServer::start();
5273
5274        let mock_response = serde_json::json!({
5275            "jsonrpc": "1.0",
5276            "id": "amp-client",
5277            "result": {
5278                "version": 220000,
5279                "subversion": "/Liquid:22.0.0/",
5280                "protocolversion": 70016,
5281                "localservices": "0000000000000409",
5282                "localrelay": true,
5283                "timeoffset": 0,
5284                "networkactive": true,
5285                "connections": 8,
5286                "networks": [],
5287                "relayfee": 0.00001000,
5288                "incrementalfee": 0.00001000,
5289                "localaddresses": [],
5290                "warnings": ""
5291            }
5292        });
5293
5294        let mock = server.mock(|when, then| {
5295            when.method(POST)
5296                .path("/")
5297                .header("authorization", "Basic dXNlcjpwYXNz") // base64 of "user:pass"
5298                .json_body(serde_json::json!({
5299                    "jsonrpc": "1.0",
5300                    "id": "amp-client",
5301                    "method": "getnetworkinfo",
5302                    "params": []
5303                }));
5304            then.status(200)
5305                .header("content-type", "application/json")
5306                .json_body(mock_response);
5307        });
5308
5309        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5310        let result = rpc.get_network_info().await;
5311
5312        assert!(result.is_ok());
5313        let network_info = result.unwrap();
5314        assert_eq!(network_info.version, 220000);
5315        assert_eq!(network_info.subversion, "/Liquid:22.0.0/");
5316        assert_eq!(network_info.connections, 8);
5317
5318        mock.assert();
5319    }
5320
5321    #[tokio::test]
5322    async fn test_get_blockchain_info_success() {
5323        let server = MockServer::start();
5324
5325        let mock_response = serde_json::json!({
5326            "jsonrpc": "1.0",
5327            "id": "amp-client",
5328            "result": {
5329                "chain": "liquidregtest",
5330                "blocks": 12345,
5331                "headers": 12345,
5332                "bestblockhash": "abc123def456789",
5333                "difficulty": 4.656542373906925e-10,
5334                "mediantime": 1640995200,
5335                "verificationprogress": 1.0,
5336                "initialblockdownload": false,
5337                "chainwork": "0000000000000000000000000000000000000000000000000000000000003039",
5338                "size_on_disk": 1234567,
5339                "pruned": false,
5340                "softforks": {},
5341                "warnings": ""
5342            }
5343        });
5344
5345        let mock = server.mock(|when, then| {
5346            when.method(POST)
5347                .path("/")
5348                .header("authorization", "Basic dXNlcjpwYXNz")
5349                .json_body(serde_json::json!({
5350                    "jsonrpc": "1.0",
5351                    "id": "amp-client",
5352                    "method": "getblockchaininfo",
5353                    "params": []
5354                }));
5355            then.status(200)
5356                .header("content-type", "application/json")
5357                .json_body(mock_response);
5358        });
5359
5360        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5361        let result = rpc.get_blockchain_info().await;
5362
5363        assert!(result.is_ok());
5364        let blockchain_info = result.unwrap();
5365        assert_eq!(blockchain_info.chain, "liquidregtest");
5366        assert_eq!(blockchain_info.blocks, 12345);
5367        assert_eq!(blockchain_info.bestblockhash, "abc123def456789");
5368
5369        mock.assert();
5370    }
5371
5372    #[tokio::test]
5373    async fn test_list_unspent_with_asset_filter() {
5374        let server = MockServer::start();
5375
5376        let mock_response = serde_json::json!({
5377            "jsonrpc": "1.0",
5378            "id": "amp-client",
5379            "result": [
5380                {
5381                    "txid": "abc123def456789",
5382                    "vout": 0,
5383                    "amount": 100.0,
5384                    "asset": "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d",
5385                    "address": "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq",
5386                    "spendable": true,
5387                    "confirmations": 6,
5388                    "scriptpubkey": "76a914abc123def456789abc123def456789abc123de88ac"
5389                },
5390                {
5391                    "txid": "def456abc123789",
5392                    "vout": 1,
5393                    "amount": 50.0,
5394                    "asset": "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d",
5395                    "address": "lq1qq3xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq",
5396                    "spendable": true,
5397                    "confirmations": 3
5398                }
5399            ]
5400        });
5401
5402        let asset_id = "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d";
5403
5404        let mock = server.mock(|when, then| {
5405            when.method(POST)
5406                .path("/")
5407                .header("authorization", "Basic dXNlcjpwYXNz")
5408                .json_body(serde_json::json!({
5409                    "jsonrpc": "1.0",
5410                    "id": "amp-client",
5411                    "method": "listunspent",
5412                    "params": [1, 9999999, [], true, {"asset": asset_id}]
5413                }));
5414            then.status(200)
5415                .header("content-type", "application/json")
5416                .json_body(mock_response);
5417        });
5418
5419        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5420        let result = rpc.list_unspent(Some(asset_id)).await;
5421
5422        assert!(result.is_ok());
5423        let utxos = result.unwrap();
5424        assert_eq!(utxos.len(), 2);
5425        assert_eq!(utxos[0].txid, "abc123def456789");
5426        assert_eq!(utxos[0].amount, 100.0);
5427        assert_eq!(utxos[0].asset, asset_id);
5428        assert_eq!(utxos[1].txid, "def456abc123789");
5429        assert_eq!(utxos[1].amount, 50.0);
5430
5431        mock.assert();
5432    }
5433
5434    #[tokio::test]
5435    async fn test_list_unspent_without_filter() {
5436        let server = MockServer::start();
5437
5438        let mock_response = serde_json::json!({
5439            "jsonrpc": "1.0",
5440            "id": "amp-client",
5441            "result": [
5442                {
5443                    "txid": "ghi789jkl012345",
5444                    "vout": 0,
5445                    "amount": 25.0,
5446                    "asset": "different_asset_id",
5447                    "address": "lq1qq4xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq",
5448                    "spendable": true,
5449                    "confirmations": 10
5450                }
5451            ]
5452        });
5453
5454        let mock = server.mock(|when, then| {
5455            when.method(POST)
5456                .path("/")
5457                .header("authorization", "Basic dXNlcjpwYXNz")
5458                .json_body(serde_json::json!({
5459                    "jsonrpc": "1.0",
5460                    "id": "amp-client",
5461                    "method": "listunspent",
5462                    "params": [1, 9999999, [], true]
5463                }));
5464            then.status(200)
5465                .header("content-type", "application/json")
5466                .json_body(mock_response);
5467        });
5468
5469        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5470        let result = rpc.list_unspent(None).await;
5471
5472        assert!(result.is_ok());
5473        let utxos = result.unwrap();
5474        assert_eq!(utxos.len(), 1);
5475        assert_eq!(utxos[0].txid, "ghi789jkl012345");
5476        assert_eq!(utxos[0].amount, 25.0);
5477
5478        mock.assert();
5479    }
5480
5481    #[tokio::test]
5482    async fn test_create_raw_transaction_success() {
5483        let server = MockServer::start();
5484
5485        let mock_response = serde_json::json!({
5486            "jsonrpc": "1.0",
5487            "id": "amp-client",
5488            "result": "0200000000010abc123def456789abc123def456789abc123def456789abc123def456789abc123def456789000000006b483045022100..."
5489        });
5490
5491        let mock = server.mock(|when, then| {
5492            when.method(POST)
5493                .path("/")
5494                .header("authorization", "Basic dXNlcjpwYXNz")
5495                .json_body(serde_json::json!({
5496                    "jsonrpc": "1.0",
5497                    "id": "amp-client",
5498                    "method": "createrawtransaction",
5499                    "params": [
5500                        [
5501                            {
5502                                "txid": "input_txid_123",
5503                                "vout": 0,
5504                                "sequence": 4294967295u32
5505                            }
5506                        ],
5507                        {
5508                            "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq": 100.0
5509                        },
5510                        0,
5511                        false,
5512                        {
5513                            "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq": "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d"
5514                        }
5515                    ]
5516                }));
5517            then.status(200)
5518                .header("content-type", "application/json")
5519                .json_body(mock_response);
5520        });
5521
5522        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5523
5524        let inputs = vec![TxInput {
5525            txid: "input_txid_123".to_string(),
5526            vout: 0,
5527            sequence: Some(0xffffffff),
5528        }];
5529
5530        let mut outputs = HashMap::new();
5531        outputs.insert(
5532            "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
5533            100.0,
5534        );
5535
5536        let mut assets = HashMap::new();
5537        assets.insert(
5538            "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
5539            "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d".to_string(),
5540        );
5541
5542        let result = rpc.create_raw_transaction(inputs, outputs, assets).await;
5543
5544        assert!(result.is_ok());
5545        let raw_tx = result.unwrap();
5546        assert!(raw_tx.starts_with("0200000000010abc123def456789"));
5547
5548        mock.assert();
5549    }
5550
5551    #[tokio::test]
5552    async fn test_send_raw_transaction_success() {
5553        let server = MockServer::start();
5554
5555        let mock_response = serde_json::json!({
5556            "jsonrpc": "1.0",
5557            "id": "amp-client",
5558            "result": "abc123def456789abc123def456789abc123def456789abc123def456789abc123de"
5559        });
5560
5561        let signed_tx_hex = "0200000000010abc123def456789abc123def456789abc123def456789abc123def456789abc123def456789000000006b483045022100...";
5562
5563        let mock = server.mock(|when, then| {
5564            when.method(POST)
5565                .path("/")
5566                .header("authorization", "Basic dXNlcjpwYXNz")
5567                .json_body(serde_json::json!({
5568                    "jsonrpc": "1.0",
5569                    "id": "amp-client",
5570                    "method": "sendrawtransaction",
5571                    "params": [signed_tx_hex]
5572                }));
5573            then.status(200)
5574                .header("content-type", "application/json")
5575                .json_body(mock_response);
5576        });
5577
5578        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5579        let result = rpc.send_raw_transaction(signed_tx_hex).await;
5580
5581        assert!(result.is_ok());
5582        let txid = result.unwrap();
5583        assert_eq!(
5584            txid,
5585            "abc123def456789abc123def456789abc123def456789abc123def456789abc123de"
5586        );
5587
5588        mock.assert();
5589    }
5590
5591    #[tokio::test]
5592    async fn test_get_transaction_success() {
5593        let server = MockServer::start();
5594
5595        let mock_response = serde_json::json!({
5596            "jsonrpc": "1.0",
5597            "id": "amp-client",
5598            "result": {
5599                "txid": "abc123def456789abc123def456789abc123def456789abc123def456789abc123de",
5600                "confirmations": 6,
5601                "blockheight": 12345,
5602                "hex": "0200000000010abc123def456789...",
5603                "blockhash": "def456abc123789def456abc123789def456abc123789def456abc123789def456ab",
5604                "blocktime": 1640995200,
5605                "time": 1640995200,
5606                "timereceived": 1640995180
5607            }
5608        });
5609
5610        let txid = "abc123def456789abc123def456789abc123def456789abc123def456789abc123de";
5611
5612        let mock = server.mock(|when, then| {
5613            when.method(POST)
5614                .path("/")
5615                .header("authorization", "Basic dXNlcjpwYXNz")
5616                .json_body(serde_json::json!({
5617                    "jsonrpc": "1.0",
5618                    "id": "amp-client",
5619                    "method": "gettransaction",
5620                    "params": [txid, true]
5621                }));
5622            then.status(200)
5623                .header("content-type", "application/json")
5624                .json_body(mock_response);
5625        });
5626
5627        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5628        let result = rpc.get_transaction(txid).await;
5629
5630        assert!(result.is_ok());
5631        let tx_detail = result.unwrap();
5632        assert_eq!(tx_detail.txid, txid);
5633        assert_eq!(tx_detail.confirmations, 6);
5634        assert_eq!(tx_detail.blockheight, Some(12345));
5635        assert_eq!(tx_detail.blocktime, Some(1640995200));
5636
5637        mock.assert();
5638    }
5639
5640    // Error handling tests
5641
5642    #[tokio::test]
5643    async fn test_rpc_call_network_failure() {
5644        // Use an invalid URL to simulate network failure
5645        let rpc = ElementsRpc::new(
5646            "http://invalid-host:99999".to_string(),
5647            "user".to_string(),
5648            "pass".to_string(),
5649        );
5650
5651        let result = rpc.get_network_info().await;
5652        assert!(result.is_err());
5653
5654        match result.unwrap_err() {
5655            AmpError::Rpc(msg) => {
5656                assert!(msg.contains("Failed to send RPC request"));
5657            }
5658            _ => panic!("Expected RPC error for network failure"),
5659        }
5660    }
5661
5662    #[tokio::test]
5663    async fn test_rpc_call_http_error_status() {
5664        let server = MockServer::start();
5665
5666        let mock = server.mock(|when, then| {
5667            when.method(POST).path("/");
5668            then.status(500)
5669                .header("content-type", "application/json")
5670                .body("Internal Server Error");
5671        });
5672
5673        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5674        let result = rpc.get_network_info().await;
5675
5676        assert!(result.is_err());
5677        match result.unwrap_err() {
5678            AmpError::Rpc(msg) => {
5679                assert!(msg.contains("RPC request failed with status: 500"));
5680            }
5681            _ => panic!("Expected RPC error for HTTP error status"),
5682        }
5683
5684        mock.assert();
5685    }
5686
5687    #[tokio::test]
5688    async fn test_rpc_call_invalid_json_response() {
5689        let server = MockServer::start();
5690
5691        let mock = server.mock(|when, then| {
5692            when.method(POST).path("/");
5693            then.status(200)
5694                .header("content-type", "application/json")
5695                .body("invalid json response");
5696        });
5697
5698        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5699        let result = rpc.get_network_info().await;
5700
5701        assert!(result.is_err());
5702        match result.unwrap_err() {
5703            AmpError::Rpc(msg) => {
5704                assert!(msg.contains("Failed to parse RPC response"));
5705            }
5706            _ => panic!("Expected RPC error for invalid JSON"),
5707        }
5708
5709        mock.assert();
5710    }
5711
5712    #[tokio::test]
5713    async fn test_rpc_call_error_response() {
5714        let server = MockServer::start();
5715
5716        let mock_response = serde_json::json!({
5717            "jsonrpc": "1.0",
5718            "id": "amp-client",
5719            "result": null,
5720            "error": {
5721                "code": -32601,
5722                "message": "Method not found"
5723            }
5724        });
5725
5726        let mock = server.mock(|when, then| {
5727            when.method(POST).path("/");
5728            then.status(200)
5729                .header("content-type", "application/json")
5730                .json_body(mock_response);
5731        });
5732
5733        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5734        let result = rpc.get_network_info().await;
5735
5736        assert!(result.is_err());
5737        match result.unwrap_err() {
5738            AmpError::Rpc(msg) => {
5739                assert!(msg.contains("RPC error -32601: Method not found"));
5740            }
5741            _ => panic!("Expected RPC error for error response"),
5742        }
5743
5744        mock.assert();
5745    }
5746
5747    #[tokio::test]
5748    async fn test_rpc_call_missing_result() {
5749        let server = MockServer::start();
5750
5751        let mock_response = serde_json::json!({
5752            "jsonrpc": "1.0",
5753            "id": "amp-client",
5754            "result": null,
5755            "error": null
5756        });
5757
5758        let mock = server.mock(|when, then| {
5759            when.method(POST).path("/");
5760            then.status(200)
5761                .header("content-type", "application/json")
5762                .json_body(mock_response);
5763        });
5764
5765        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5766        let result = rpc.get_network_info().await;
5767
5768        assert!(result.is_err());
5769        match result.unwrap_err() {
5770            AmpError::Rpc(msg) => {
5771                assert!(msg.contains("RPC response missing result field"));
5772            }
5773            _ => panic!("Expected RPC error for missing result"),
5774        }
5775
5776        mock.assert();
5777    }
5778
5779    // Authentication tests
5780
5781    #[tokio::test]
5782    async fn test_rpc_authentication_headers() {
5783        let server = MockServer::start();
5784
5785        let mock_response = serde_json::json!({
5786            "jsonrpc": "1.0",
5787            "id": "amp-client",
5788            "result": {
5789                "version": 220000,
5790                "subversion": "/Liquid:22.0.0/",
5791                "protocolversion": 70016,
5792                "localservices": "0000000000000409",
5793                "localrelay": true,
5794                "timeoffset": 0,
5795                "networkactive": true,
5796                "connections": 8,
5797                "networks": [],
5798                "relayfee": 0.00001000,
5799                "incrementalfee": 0.00001000,
5800                "localaddresses": [],
5801                "warnings": ""
5802            }
5803        });
5804
5805        // Test with custom username and password
5806        let mock = server.mock(|when, then| {
5807            when.method(POST)
5808                .path("/")
5809                .header("authorization", "Basic dGVzdHVzZXI6dGVzdHBhc3M=") // base64 of "testuser:testpass"
5810                .json_body(serde_json::json!({
5811                    "jsonrpc": "1.0",
5812                    "id": "amp-client",
5813                    "method": "getnetworkinfo",
5814                    "params": []
5815                }));
5816            then.status(200)
5817                .header("content-type", "application/json")
5818                .json_body(mock_response);
5819        });
5820
5821        let rpc = ElementsRpc::new(
5822            server.url("/"),
5823            "testuser".to_string(),
5824            "testpass".to_string(),
5825        );
5826        let result = rpc.get_network_info().await;
5827
5828        assert!(result.is_ok());
5829        mock.assert();
5830    }
5831
5832    // Wallet passphrase tests
5833
5834    #[tokio::test]
5835    async fn test_wallet_passphrase_success() {
5836        let server = MockServer::start();
5837
5838        let mock_response = serde_json::json!({
5839            "jsonrpc": "1.0",
5840            "id": "amp-client",
5841            "result": null
5842        });
5843
5844        let mock = server.mock(|when, then| {
5845            when.method(POST)
5846                .path("/")
5847                .header("authorization", "Basic dXNlcjpwYXNz")
5848                .json_body(serde_json::json!({
5849                    "jsonrpc": "1.0",
5850                    "id": "amp-client",
5851                    "method": "walletpassphrase",
5852                    "params": ["my_passphrase", 300]
5853                }));
5854            then.status(200)
5855                .header("content-type", "application/json")
5856                .json_body(mock_response);
5857        });
5858
5859        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5860        let result = rpc.wallet_passphrase("my_passphrase", 300).await;
5861
5862        assert!(result.is_ok());
5863        mock.assert();
5864    }
5865
5866    // Connection validation tests
5867
5868    #[tokio::test]
5869    async fn test_validate_connection_success() {
5870        let server = MockServer::start();
5871
5872        let mock_response = serde_json::json!({
5873            "jsonrpc": "1.0",
5874            "id": "amp-client",
5875            "result": {
5876                "version": 220000,
5877                "subversion": "/Liquid:22.0.0/",
5878                "protocolversion": 70016,
5879                "localservices": "0000000000000409",
5880                "localrelay": true,
5881                "timeoffset": 0,
5882                "networkactive": true,
5883                "connections": 8,
5884                "networks": [],
5885                "relayfee": 0.00001000,
5886                "incrementalfee": 0.00001000,
5887                "localaddresses": [],
5888                "warnings": ""
5889            }
5890        });
5891
5892        let mock = server.mock(|when, then| {
5893            when.method(POST).path("/");
5894            then.status(200)
5895                .header("content-type", "application/json")
5896                .json_body(mock_response);
5897        });
5898
5899        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5900        let result = rpc.validate_connection().await;
5901
5902        assert!(result.is_ok());
5903        mock.assert();
5904    }
5905
5906    #[tokio::test]
5907    async fn test_get_node_status_success() {
5908        let server = MockServer::start();
5909
5910        let network_mock_response = serde_json::json!({
5911            "jsonrpc": "1.0",
5912            "id": "amp-client",
5913            "result": {
5914                "version": 220000,
5915                "subversion": "/Liquid:22.0.0/",
5916                "protocolversion": 70016,
5917                "localservices": "0000000000000409",
5918                "localrelay": true,
5919                "timeoffset": 0,
5920                "networkactive": true,
5921                "connections": 8,
5922                "networks": [],
5923                "relayfee": 0.00001000,
5924                "incrementalfee": 0.00001000,
5925                "localaddresses": [],
5926                "warnings": ""
5927            }
5928        });
5929
5930        let blockchain_mock_response = serde_json::json!({
5931            "jsonrpc": "1.0",
5932            "id": "amp-client",
5933            "result": {
5934                "chain": "liquidregtest",
5935                "blocks": 12345,
5936                "headers": 12345,
5937                "bestblockhash": "abc123def456789",
5938                "difficulty": 4.656542373906925e-10,
5939                "mediantime": 1640995200,
5940                "verificationprogress": 1.0,
5941                "initialblockdownload": false,
5942                "chainwork": "0000000000000000000000000000000000000000000000000000000000003039",
5943                "size_on_disk": 1234567,
5944                "pruned": false,
5945                "softforks": {},
5946                "warnings": ""
5947            }
5948        });
5949
5950        let network_mock = server.mock(|when, then| {
5951            when.method(POST).path("/").json_body(serde_json::json!({
5952                "jsonrpc": "1.0",
5953                "id": "amp-client",
5954                "method": "getnetworkinfo",
5955                "params": []
5956            }));
5957            then.status(200)
5958                .header("content-type", "application/json")
5959                .json_body(network_mock_response);
5960        });
5961
5962        let blockchain_mock = server.mock(|when, then| {
5963            when.method(POST).path("/").json_body(serde_json::json!({
5964                "jsonrpc": "1.0",
5965                "id": "amp-client",
5966                "method": "getblockchaininfo",
5967                "params": []
5968            }));
5969            then.status(200)
5970                .header("content-type", "application/json")
5971                .json_body(blockchain_mock_response);
5972        });
5973
5974        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5975        let result = rpc.get_node_status().await;
5976
5977        assert!(result.is_ok());
5978        let (network_info, blockchain_info) = result.unwrap();
5979        assert_eq!(network_info.version, 220000);
5980        assert_eq!(blockchain_info.blocks, 12345);
5981
5982        network_mock.assert();
5983        blockchain_mock.assert();
5984    }
5985
5986    // Tests for UTXO selection and transaction building logic
5987
5988    #[tokio::test]
5989    async fn test_build_distribution_transaction_zero_amount() {
5990        let rpc = ElementsRpc::new(
5991            "http://localhost:18884".to_string(),
5992            "user".to_string(),
5993            "pass".to_string(),
5994        );
5995
5996        let address_amounts = HashMap::new(); // Empty distribution
5997
5998        let result = rpc
5999            .build_distribution_transaction(
6000                "test_wallet",
6001                "asset_id",
6002                address_amounts,
6003                "change_address",
6004                1.0,
6005            )
6006            .await;
6007
6008        assert!(result.is_err());
6009        match result.unwrap_err() {
6010            AmpError::Validation(msg) => {
6011                assert!(msg.contains("Total distribution amount must be greater than zero"));
6012            }
6013            _ => panic!("Expected validation error for zero distribution amount"),
6014        }
6015    }
6016
6017    #[tokio::test]
6018    async fn test_sign_transaction_validation() {
6019        let rpc = ElementsRpc::new(
6020            "http://localhost:18884".to_string(),
6021            "user".to_string(),
6022            "pass".to_string(),
6023        );
6024
6025        // Mock signer for testing
6026        struct MockSigner {
6027            should_fail: bool,
6028            return_value: String,
6029        }
6030
6031        #[async_trait::async_trait]
6032        impl crate::signer::Signer for MockSigner {
6033            async fn sign_transaction(
6034                &self,
6035                _unsigned_tx: &str,
6036            ) -> Result<String, crate::signer::SignerError> {
6037                if self.should_fail {
6038                    Err(crate::signer::SignerError::Lwk(
6039                        "Mock signing failure".to_string(),
6040                    ))
6041                } else {
6042                    // Return a longer hex string to simulate signed transaction (20+ bytes when decoded)
6043                    Ok(format!(
6044                        "{}deadbeefcafebabe1234567890abcdef",
6045                        self.return_value
6046                    ))
6047                }
6048            }
6049
6050            fn as_any(&self) -> &dyn std::any::Any {
6051                self
6052            }
6053        }
6054
6055        // Test empty transaction hex
6056        let mock_signer = MockSigner {
6057            should_fail: false,
6058            return_value: "".to_string(),
6059        };
6060        let result = rpc.sign_transaction("", &mock_signer).await;
6061        assert!(result.is_err());
6062        assert!(result.unwrap_err().to_string().contains("cannot be empty"));
6063
6064        // Test odd length hex
6065        let result = rpc.sign_transaction("abc", &mock_signer).await;
6066        assert!(result.is_err());
6067        assert!(result.unwrap_err().to_string().contains("even length"));
6068
6069        // Test invalid hex characters
6070        let result = rpc.sign_transaction("abcg", &mock_signer).await;
6071        assert!(result.is_err());
6072        assert!(result
6073            .unwrap_err()
6074            .to_string()
6075            .contains("invalid hex characters"));
6076
6077        // Test signer failure
6078        let mock_signer = MockSigner {
6079            should_fail: true,
6080            return_value: "".to_string(),
6081        };
6082        let result = rpc.sign_transaction("abcd", &mock_signer).await;
6083        assert!(result.is_err());
6084        assert!(result
6085            .unwrap_err()
6086            .to_string()
6087            .contains("Mock signing failure"));
6088
6089        // Test successful signing
6090        let mock_signer = MockSigner {
6091            should_fail: false,
6092            return_value: "abcd".to_string(),
6093        };
6094        let result = rpc.sign_transaction("abcd", &mock_signer).await;
6095        if result.is_err() {
6096            println!("Error: {}", result.as_ref().unwrap_err());
6097        }
6098        assert!(result.is_ok());
6099        assert_eq!(result.unwrap(), "abcddeadbeefcafebabe1234567890abcdef");
6100    }
6101
6102    #[tokio::test]
6103    async fn test_sign_transaction_validation_edge_cases() {
6104        let rpc = ElementsRpc::new(
6105            "http://localhost:18884".to_string(),
6106            "user".to_string(),
6107            "pass".to_string(),
6108        );
6109
6110        // Mock signer that returns invalid responses
6111        struct BadMockSigner {
6112            return_empty: bool,
6113            return_odd_length: bool,
6114            return_invalid_hex: bool,
6115            return_shorter: bool,
6116        }
6117
6118        #[async_trait::async_trait]
6119        impl crate::signer::Signer for BadMockSigner {
6120            async fn sign_transaction(
6121                &self,
6122                unsigned_tx: &str,
6123            ) -> Result<String, crate::signer::SignerError> {
6124                if self.return_empty {
6125                    Ok("".to_string())
6126                } else if self.return_odd_length {
6127                    Ok("abc".to_string())
6128                } else if self.return_invalid_hex {
6129                    Ok("abcg".to_string())
6130                } else if self.return_shorter {
6131                    Ok("ab".to_string()) // Shorter than input "abcd"
6132                } else {
6133                    Ok(format!("{}deadbeef", unsigned_tx))
6134                }
6135            }
6136
6137            fn as_any(&self) -> &dyn std::any::Any {
6138                self
6139            }
6140        }
6141
6142        // Test signer returning empty string
6143        let bad_signer = BadMockSigner {
6144            return_empty: true,
6145            return_odd_length: false,
6146            return_invalid_hex: false,
6147            return_shorter: false,
6148        };
6149        let result = rpc.sign_transaction("abcd", &bad_signer).await;
6150        assert!(result.is_err());
6151        assert!(result.unwrap_err().to_string().contains("cannot be empty"));
6152
6153        // Test signer returning odd length hex
6154        let bad_signer = BadMockSigner {
6155            return_empty: false,
6156            return_odd_length: true,
6157            return_invalid_hex: false,
6158            return_shorter: false,
6159        };
6160        let result = rpc.sign_transaction("abcd", &bad_signer).await;
6161        assert!(result.is_err());
6162        assert!(result.unwrap_err().to_string().contains("even length"));
6163
6164        // Test signer returning invalid hex
6165        let bad_signer = BadMockSigner {
6166            return_empty: false,
6167            return_odd_length: false,
6168            return_invalid_hex: true,
6169            return_shorter: false,
6170        };
6171        let result = rpc.sign_transaction("abcd", &bad_signer).await;
6172        assert!(result.is_err());
6173        assert!(result
6174            .unwrap_err()
6175            .to_string()
6176            .contains("invalid hex characters"));
6177
6178        // Test signer returning shorter transaction (invalid)
6179        let bad_signer = BadMockSigner {
6180            return_empty: false,
6181            return_odd_length: false,
6182            return_invalid_hex: false,
6183            return_shorter: true,
6184        };
6185        let result = rpc.sign_transaction("abcd", &bad_signer).await;
6186        assert!(result.is_err());
6187        assert!(result
6188            .unwrap_err()
6189            .to_string()
6190            .contains("shorter than unsigned transaction"));
6191    }
6192
6193    #[tokio::test]
6194    async fn test_sign_transaction_minimum_size_validation() {
6195        let rpc = ElementsRpc::new(
6196            "http://localhost:18884".to_string(),
6197            "user".to_string(),
6198            "pass".to_string(),
6199        );
6200
6201        // Mock signer that returns very small transactions
6202        struct TinyMockSigner;
6203
6204        #[async_trait::async_trait]
6205        impl crate::signer::Signer for TinyMockSigner {
6206            async fn sign_transaction(
6207                &self,
6208                _unsigned_tx: &str,
6209            ) -> Result<String, crate::signer::SignerError> {
6210                Ok("abcd".to_string()) // Only 2 bytes when decoded
6211            }
6212
6213            fn as_any(&self) -> &dyn std::any::Any {
6214                self
6215            }
6216        }
6217
6218        let tiny_signer = TinyMockSigner;
6219        let result = rpc.sign_transaction("abcd", &tiny_signer).await;
6220        assert!(result.is_err());
6221        let error_msg = result.unwrap_err().to_string();
6222        assert!(error_msg.contains("minimum size"));
6223        assert!(error_msg.contains("minimum is 10 bytes"));
6224    }
6225
6226    #[tokio::test]
6227    async fn test_sign_transaction_success_case() {
6228        let rpc = ElementsRpc::new(
6229            "http://localhost:18884".to_string(),
6230            "user".to_string(),
6231            "pass".to_string(),
6232        );
6233
6234        // Mock signer that returns a valid signed transaction
6235        struct GoodMockSigner;
6236
6237        #[async_trait::async_trait]
6238        impl crate::signer::Signer for GoodMockSigner {
6239            async fn sign_transaction(
6240                &self,
6241                unsigned_tx: &str,
6242            ) -> Result<String, crate::signer::SignerError> {
6243                // Return a longer valid hex string (20+ bytes when decoded)
6244                Ok(format!("{}deadbeefcafebabe1234567890abcdef", unsigned_tx))
6245            }
6246
6247            fn as_any(&self) -> &dyn std::any::Any {
6248                self
6249            }
6250        }
6251
6252        let good_signer = GoodMockSigner;
6253
6254        // Test with a reasonable sized unsigned transaction
6255        let unsigned_tx = "0200000000010123456789abcdef"; // 14 bytes when decoded
6256        let result = rpc.sign_transaction(unsigned_tx, &good_signer).await;
6257
6258        assert!(result.is_ok());
6259        let signed_tx = result.unwrap();
6260        assert!(signed_tx.starts_with(unsigned_tx));
6261        assert!(signed_tx.len() > unsigned_tx.len());
6262        assert!(signed_tx.contains("deadbeefcafebabe"));
6263    }
6264
6265    #[tokio::test]
6266    async fn test_sign_and_broadcast_transaction_mock() {
6267        // Create a mock server for testing the broadcast part
6268        let server = MockServer::start();
6269
6270        // Mock the RPC response for sendrawtransaction
6271        let mock = server.mock(|when, then| {
6272            when.method(POST).path("/").json_body(serde_json::json!({
6273                "jsonrpc": "1.0",
6274                "id": "amp-client",
6275                "method": "sendrawtransaction",
6276                "params": ["0200000000010123456789abcdefdeadbeefcafebabe1234567890abcdef"]
6277            }));
6278            then.status(200).json_body(serde_json::json!({
6279                "jsonrpc": "1.0",
6280                "id": "amp-client",
6281                "result": "abc123def456789",
6282                "error": null
6283            }));
6284        });
6285
6286        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
6287
6288        // Mock signer for testing
6289        struct TestMockSigner;
6290
6291        #[async_trait::async_trait]
6292        impl crate::signer::Signer for TestMockSigner {
6293            async fn sign_transaction(
6294                &self,
6295                unsigned_tx: &str,
6296            ) -> Result<String, crate::signer::SignerError> {
6297                Ok(format!("{}deadbeefcafebabe1234567890abcdef", unsigned_tx))
6298            }
6299
6300            fn as_any(&self) -> &dyn std::any::Any {
6301                self
6302            }
6303        }
6304
6305        let signer = TestMockSigner;
6306        let unsigned_tx = "0200000000010123456789abcdef";
6307
6308        let result = rpc
6309            .sign_and_broadcast_transaction(unsigned_tx, &signer)
6310            .await;
6311
6312        assert!(result.is_ok());
6313        assert_eq!(result.unwrap(), "abc123def456789");
6314
6315        // Verify the mock was called
6316        mock.assert();
6317    }
6318
6319    #[tokio::test]
6320    async fn test_sign_and_broadcast_transaction_signing_failure() {
6321        let rpc = ElementsRpc::new(
6322            "http://localhost:18884".to_string(),
6323            "user".to_string(),
6324            "pass".to_string(),
6325        );
6326
6327        // Mock signer that fails
6328        struct FailingSigner;
6329
6330        #[async_trait::async_trait]
6331        impl crate::signer::Signer for FailingSigner {
6332            async fn sign_transaction(
6333                &self,
6334                _unsigned_tx: &str,
6335            ) -> Result<String, crate::signer::SignerError> {
6336                Err(crate::signer::SignerError::Lwk(
6337                    "Signing failed".to_string(),
6338                ))
6339            }
6340
6341            fn as_any(&self) -> &dyn std::any::Any {
6342                self
6343            }
6344        }
6345
6346        let failing_signer = FailingSigner;
6347        let result = rpc
6348            .sign_and_broadcast_transaction("abcd", &failing_signer)
6349            .await;
6350
6351        assert!(result.is_err());
6352        let error_msg = result.unwrap_err().to_string();
6353        // The error should be a Signer error containing the original failure message
6354        assert!(error_msg.contains("Signer error"));
6355        assert!(error_msg.contains("Signing failed"));
6356    }
6357
6358    #[tokio::test]
6359    async fn test_sign_and_broadcast_transaction_broadcast_failure() {
6360        // Create a mock server that returns an error for broadcast
6361        let server = MockServer::start();
6362
6363        let mock = server.mock(|when, then| {
6364            when.method(POST).path("/");
6365            then.status(200).json_body(serde_json::json!({
6366                "jsonrpc": "1.0",
6367                "id": "amp-client",
6368                "result": null,
6369                "error": {
6370                    "code": -26,
6371                    "message": "Transaction rejected"
6372                }
6373            }));
6374        });
6375
6376        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
6377
6378        // Mock signer that succeeds
6379        struct WorkingSigner;
6380
6381        #[async_trait::async_trait]
6382        impl crate::signer::Signer for WorkingSigner {
6383            async fn sign_transaction(
6384                &self,
6385                unsigned_tx: &str,
6386            ) -> Result<String, crate::signer::SignerError> {
6387                Ok(format!("{}deadbeefcafebabe1234567890abcdef", unsigned_tx))
6388            }
6389
6390            fn as_any(&self) -> &dyn std::any::Any {
6391                self
6392            }
6393        }
6394
6395        let working_signer = WorkingSigner;
6396        let unsigned_tx = "0200000000010123456789abcdef";
6397
6398        let result = rpc
6399            .sign_and_broadcast_transaction(unsigned_tx, &working_signer)
6400            .await;
6401
6402        assert!(result.is_err());
6403        let error_msg = result.unwrap_err().to_string();
6404        assert!(error_msg.contains("Failed during transaction broadcast phase"));
6405        assert!(error_msg.contains("Transaction rejected"));
6406
6407        mock.assert();
6408    }
6409
6410    #[tokio::test]
6411    async fn test_wait_for_confirmations_success() {
6412        let server = MockServer::start();
6413
6414        let txid = "abc123def456789abc123def456789abc123def456789abc123def456789abc123de";
6415
6416        // First call returns 1 confirmation (not enough)
6417        let _mock_response_1 = serde_json::json!({
6418            "jsonrpc": "1.0",
6419            "id": "amp-client",
6420            "result": {
6421                "txid": txid,
6422                "confirmations": 1,
6423                "blockheight": 12345,
6424                "hex": "0200000000010abc123def456789...",
6425                "blockhash": "def456abc123789def456abc123789def456abc123789def456abc123789def456ab",
6426                "blocktime": 1640995200,
6427                "time": 1640995200,
6428                "timereceived": 1640995180
6429            }
6430        });
6431
6432        // Second call returns 2 confirmations (sufficient)
6433        let mock_response_2 = serde_json::json!({
6434            "jsonrpc": "1.0",
6435            "id": "amp-client",
6436            "result": {
6437                "txid": txid,
6438                "confirmations": 2,
6439                "blockheight": 12345,
6440                "hex": "0200000000010abc123def456789...",
6441                "blockhash": "def456abc123789def456abc123789def456abc123789def456abc123789def456ab",
6442                "blocktime": 1640995200,
6443                "time": 1640995200,
6444                "timereceived": 1640995180
6445            }
6446        });
6447
6448        // Create a mock that returns 2 confirmations immediately (simpler test)
6449        let mock = server.mock(|when, then| {
6450            when.method(POST)
6451                .path("/")
6452                .header("authorization", "Basic dXNlcjpwYXNz")
6453                .json_body(serde_json::json!({
6454                    "jsonrpc": "1.0",
6455                    "id": "amp-client",
6456                    "method": "gettransaction",
6457                    "params": [txid, true]
6458                }));
6459            then.status(200)
6460                .header("content-type", "application/json")
6461                .json_body(mock_response_2); // Return sufficient confirmations immediately
6462        });
6463
6464        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
6465
6466        // Use fast polling (1 second) for testing
6467        let result = rpc
6468            .wait_for_confirmations_with_interval(txid, Some(2), Some(1), Some(1))
6469            .await;
6470
6471        assert!(result.is_ok());
6472        let tx_detail = result.unwrap();
6473        assert_eq!(tx_detail.confirmations, 2);
6474        assert_eq!(tx_detail.txid, txid);
6475
6476        // Mock should have been called once
6477        mock.assert();
6478    }
6479
6480    #[tokio::test]
6481    async fn test_wait_for_confirmations_timeout() {
6482        let server = MockServer::start();
6483
6484        let txid = "abc123def456789abc123def456789abc123def456789abc123def456789abc123de";
6485
6486        // Always return insufficient confirmations
6487        let mock_response = serde_json::json!({
6488            "jsonrpc": "1.0",
6489            "id": "amp-client",
6490            "result": {
6491                "txid": txid,
6492                "confirmations": 1,
6493                "blockheight": 12345,
6494                "hex": "0200000000010abc123def456789...",
6495                "blockhash": null,
6496                "blocktime": null,
6497                "time": null,
6498                "timereceived": null
6499            }
6500        });
6501
6502        let _mock = server.mock(|when, then| {
6503            when.method(POST)
6504                .path("/")
6505                .header("authorization", "Basic dXNlcjpwYXNz")
6506                .json_body(serde_json::json!({
6507                    "jsonrpc": "1.0",
6508                    "id": "amp-client",
6509                    "method": "gettransaction",
6510                    "params": [txid, true]
6511                }));
6512            then.status(200)
6513                .header("content-type", "application/json")
6514                .json_body(mock_response);
6515        });
6516
6517        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
6518
6519        // Use a very short timeout for testing (0 = 3 seconds) and fast polling (1 second)
6520        let result = rpc
6521            .wait_for_confirmations_with_interval(txid, Some(2), Some(0), Some(1))
6522            .await;
6523
6524        assert!(result.is_err());
6525        match result.unwrap_err() {
6526            AmpError::Timeout(msg) => {
6527                assert!(msg.contains("Timeout waiting for confirmations"));
6528                assert!(msg.contains(txid));
6529                assert!(msg.contains("retry confirmation"));
6530            }
6531            _ => panic!("Expected timeout error"),
6532        }
6533
6534        // Mock will be called multiple times during the timeout period
6535        // We don't assert on the exact number since it depends on timing
6536    }
6537
6538    #[tokio::test]
6539    async fn test_wait_for_confirmations_immediate_success() {
6540        let server = MockServer::start();
6541
6542        let txid = "abc123def456789abc123def456789abc123def456789abc123def456789abc123de";
6543
6544        // Transaction already has sufficient confirmations
6545        let mock_response = serde_json::json!({
6546            "jsonrpc": "1.0",
6547            "id": "amp-client",
6548            "result": {
6549                "txid": txid,
6550                "confirmations": 5,
6551                "blockheight": 12345,
6552                "hex": "0200000000010abc123def456789...",
6553                "blockhash": "def456abc123789def456abc123789def456abc123789def456abc123789def456ab",
6554                "blocktime": 1640995200,
6555                "time": 1640995200,
6556                "timereceived": 1640995180
6557            }
6558        });
6559
6560        let mock = server.mock(|when, then| {
6561            when.method(POST)
6562                .path("/")
6563                .header("authorization", "Basic dXNlcjpwYXNz")
6564                .json_body(serde_json::json!({
6565                    "jsonrpc": "1.0",
6566                    "id": "amp-client",
6567                    "method": "gettransaction",
6568                    "params": [txid, true]
6569                }));
6570            then.status(200)
6571                .header("content-type", "application/json")
6572                .json_body(mock_response);
6573        });
6574
6575        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
6576
6577        let result = rpc.wait_for_confirmations(txid, Some(2), Some(10)).await;
6578
6579        assert!(result.is_ok());
6580        let tx_detail = result.unwrap();
6581        assert_eq!(tx_detail.confirmations, 5);
6582        assert_eq!(tx_detail.txid, txid);
6583
6584        // Should only need one call since confirmations are already sufficient
6585        mock.assert();
6586    }
6587}
6588
6589/// Configuration for retry behavior in API requests
6590#[derive(Debug, Clone)]
6591pub struct RetryConfig {
6592    /// Maximum number of retry attempts
6593    pub max_attempts: u32,
6594    /// Base delay in milliseconds for exponential backoff
6595    pub base_delay_ms: u64,
6596    /// Maximum delay in milliseconds to cap exponential backoff
6597    pub max_delay_ms: u64,
6598    /// Request timeout in seconds
6599    pub timeout_seconds: u64,
6600}
6601
6602impl Default for RetryConfig {
6603    fn default() -> Self {
6604        Self {
6605            max_attempts: 3,
6606            base_delay_ms: 1000,
6607            max_delay_ms: 30_000,
6608            timeout_seconds: 10,
6609        }
6610    }
6611}
6612
6613impl RetryConfig {
6614    /// Creates a `RetryConfig` from environment variables with default fallbacks
6615    ///
6616    /// Environment variables:
6617    /// - `API_RETRY_MAX_ATTEMPTS`: Maximum retry attempts (default: 3)
6618    /// - `API_RETRY_BASE_DELAY_MS`: Base delay in milliseconds (default: 1000)
6619    /// - `API_RETRY_MAX_DELAY_MS`: Maximum delay in milliseconds (default: 30000)
6620    /// - `API_REQUEST_TIMEOUT_SECONDS`: Request timeout in seconds (default: 10)
6621    ///
6622    /// # Errors
6623    ///
6624    /// Returns an error if any environment variable contains an invalid value
6625    pub fn from_env() -> Result<Self, Error> {
6626        let max_attempts = match env::var("API_RETRY_MAX_ATTEMPTS") {
6627            Ok(val) => val.parse::<u32>().map_err(|e| {
6628                Error::InvalidRetryConfig(format!("Invalid API_RETRY_MAX_ATTEMPTS: {e}"))
6629            })?,
6630            Err(_) => 3,
6631        };
6632
6633        let base_delay_ms = match env::var("API_RETRY_BASE_DELAY_MS") {
6634            Ok(val) => val.parse::<u64>().map_err(|e| {
6635                Error::InvalidRetryConfig(format!("Invalid API_RETRY_BASE_DELAY_MS: {e}"))
6636            })?,
6637            Err(_) => 1000,
6638        };
6639
6640        let max_delay_ms = match env::var("API_RETRY_MAX_DELAY_MS") {
6641            Ok(val) => val.parse::<u64>().map_err(|e| {
6642                Error::InvalidRetryConfig(format!("Invalid API_RETRY_MAX_DELAY_MS: {e}"))
6643            })?,
6644            Err(_) => 30_000,
6645        };
6646
6647        let timeout_seconds = match env::var("API_REQUEST_TIMEOUT_SECONDS") {
6648            Ok(val) => val.parse::<u64>().map_err(|e| {
6649                Error::InvalidRetryConfig(format!("Invalid API_REQUEST_TIMEOUT_SECONDS: {e}"))
6650            })?,
6651            Err(_) => 10,
6652        };
6653
6654        // Validate configuration
6655        if max_attempts == 0 {
6656            return Err(Error::InvalidRetryConfig(
6657                "max_attempts must be greater than 0".to_string(),
6658            ));
6659        }
6660        if base_delay_ms == 0 {
6661            return Err(Error::InvalidRetryConfig(
6662                "base_delay_ms must be greater than 0".to_string(),
6663            ));
6664        }
6665        if max_delay_ms < base_delay_ms {
6666            return Err(Error::InvalidRetryConfig(
6667                "max_delay_ms must be greater than or equal to base_delay_ms".to_string(),
6668            ));
6669        }
6670        if timeout_seconds == 0 {
6671            return Err(Error::InvalidRetryConfig(
6672                "timeout_seconds must be greater than 0".to_string(),
6673            ));
6674        }
6675
6676        Ok(Self {
6677            max_attempts,
6678            base_delay_ms,
6679            max_delay_ms,
6680            timeout_seconds,
6681        })
6682    }
6683
6684    /// Creates a `RetryConfig` optimized for test environments
6685    ///
6686    /// Uses reduced values for faster test execution:
6687    /// - 2 retry attempts
6688    /// - 500ms base delay
6689    /// - 5000ms max delay
6690    /// - 5 second timeout
6691    #[must_use]
6692    pub const fn for_tests() -> Self {
6693        Self {
6694            max_attempts: 2,
6695            base_delay_ms: 500,
6696            max_delay_ms: 5000,
6697            timeout_seconds: 5,
6698        }
6699    }
6700
6701    /// Sets a custom timeout value
6702    #[must_use]
6703    pub const fn with_timeout(mut self, timeout_seconds: u64) -> Self {
6704        self.timeout_seconds = timeout_seconds;
6705        self
6706    }
6707
6708    /// Sets custom max attempts
6709    #[must_use]
6710    pub const fn with_max_attempts(mut self, max_attempts: u32) -> Self {
6711        self.max_attempts = max_attempts;
6712        self
6713    }
6714
6715    /// Sets custom base delay
6716    #[must_use]
6717    pub const fn with_base_delay_ms(mut self, base_delay_ms: u64) -> Self {
6718        self.base_delay_ms = base_delay_ms;
6719        self
6720    }
6721
6722    /// Sets custom max delay
6723    #[must_use]
6724    pub const fn with_max_delay_ms(mut self, max_delay_ms: u64) -> Self {
6725        self.max_delay_ms = max_delay_ms;
6726        self
6727    }
6728}
6729
6730/// HTTP client with sophisticated retry logic and exponential backoff
6731#[derive(Debug, Clone)]
6732pub struct RetryClient {
6733    client: Client,
6734    config: RetryConfig,
6735}
6736
6737impl RetryClient {
6738    /// Creates a new `RetryClient` with the given configuration
6739    #[must_use]
6740    pub fn new(config: RetryConfig) -> Self {
6741        Self {
6742            client: Client::new(),
6743            config,
6744        }
6745    }
6746
6747    /// Creates a new `RetryClient` with default configuration
6748    #[must_use]
6749    pub fn with_default_config() -> Self {
6750        Self::new(RetryConfig::default())
6751    }
6752
6753    /// Creates a new `RetryClient` with test-optimized configuration
6754    #[must_use]
6755    pub fn for_tests() -> Self {
6756        Self::new(RetryConfig::for_tests())
6757    }
6758
6759    /// Executes an HTTP request with retry logic and exponential backoff
6760    ///
6761    /// # Arguments
6762    /// * `request_builder` - A function that creates the request builder
6763    ///
6764    /// # Returns
6765    /// The response if successful, or an error after all retries are exhausted
6766    ///
6767    /// # Errors
6768    /// Returns `TokenError::Timeout` if the request times out
6769    /// Returns `TokenError::RateLimited` if rate limited and retries are exhausted
6770    /// Returns `TokenError::ObtainFailed` if all retry attempts fail
6771    #[allow(clippy::cognitive_complexity)]
6772    pub async fn execute_with_retry<F>(
6773        &self,
6774        request_builder: F,
6775    ) -> Result<reqwest::Response, TokenError>
6776    where
6777        F: Fn() -> reqwest::RequestBuilder + Send + Sync,
6778    {
6779        let mut last_error = String::new();
6780        let mut attempt = 0;
6781
6782        while attempt < self.config.max_attempts {
6783            attempt += 1;
6784
6785            // Create the request with timeout
6786            let request =
6787                request_builder().timeout(StdDuration::from_secs(self.config.timeout_seconds));
6788
6789            // Execute the request
6790            match request.send().await {
6791                Ok(response) => {
6792                    let status = response.status();
6793
6794                    // Handle rate limiting (429 Too Many Requests)
6795                    if status == reqwest::StatusCode::TOO_MANY_REQUESTS {
6796                        let retry_after = Self::extract_retry_after(&response).unwrap_or(60);
6797
6798                        tracing::warn!(
6799                            "Rate limited (429) on attempt {}/{}. Retry after {} seconds",
6800                            attempt,
6801                            self.config.max_attempts,
6802                            retry_after
6803                        );
6804
6805                        // If this is our last attempt, return the rate limit error
6806                        if attempt >= self.config.max_attempts {
6807                            return Err(TokenError::rate_limited(retry_after));
6808                        }
6809
6810                        // Wait for the rate limit period (or our max delay, whichever is smaller)
6811                        let delay_ms = std::cmp::min(retry_after * 1000, self.config.max_delay_ms);
6812                        sleep(StdDuration::from_millis(delay_ms)).await;
6813                        continue;
6814                    }
6815
6816                    // Handle other client errors (4xx) - these are generally not retryable
6817                    if status.is_client_error() && status != reqwest::StatusCode::TOO_MANY_REQUESTS
6818                    {
6819                        last_error = format!("Client error: {status}");
6820                        tracing::error!("Non-retryable client error: {}", status);
6821                        break;
6822                    }
6823
6824                    // Handle server errors (5xx) - these are retryable
6825                    if status.is_server_error() {
6826                        last_error = format!("Server error: {status}");
6827                        tracing::warn!(
6828                            "Server error {} on attempt {}/{}",
6829                            status,
6830                            attempt,
6831                            self.config.max_attempts
6832                        );
6833
6834                        if attempt < self.config.max_attempts {
6835                            let delay = self.calculate_backoff_delay(attempt);
6836                            sleep(delay).await;
6837                            continue;
6838                        }
6839                        break;
6840                    }
6841
6842                    // Success case
6843                    return Ok(response);
6844                }
6845                Err(e) => {
6846                    last_error = e.to_string();
6847
6848                    // Check if this is a timeout error
6849                    if e.is_timeout() {
6850                        tracing::warn!(
6851                            "Request timeout on attempt {}/{}",
6852                            attempt,
6853                            self.config.max_attempts
6854                        );
6855
6856                        if attempt >= self.config.max_attempts {
6857                            return Err(TokenError::timeout(self.config.timeout_seconds));
6858                        }
6859                    } else {
6860                        tracing::warn!(
6861                            "Request failed on attempt {}/{}: {}",
6862                            attempt,
6863                            self.config.max_attempts,
6864                            e
6865                        );
6866                    }
6867
6868                    // If we have more attempts, wait and retry
6869                    if attempt < self.config.max_attempts {
6870                        let delay = self.calculate_backoff_delay(attempt);
6871                        sleep(delay).await;
6872                    }
6873                }
6874            }
6875        }
6876
6877        // All retries exhausted
6878        Err(TokenError::obtain_failed(attempt, last_error))
6879    }
6880
6881    /// Calculates the delay for exponential backoff with jitter
6882    ///
6883    /// Uses the formula: `min(base_delay * 2^(attempt-1) + jitter, max_delay)`
6884    /// where jitter is a random value between 0 and `base_delay/2`
6885    pub fn calculate_backoff_delay(&self, attempt: u32) -> StdDuration {
6886        use rand::Rng;
6887
6888        let base_delay = self.config.base_delay_ms;
6889        let max_delay = self.config.max_delay_ms;
6890
6891        // Calculate exponential backoff: base_delay * 2^(attempt-1)
6892        let exponential_delay = base_delay * 2_u64.pow(attempt.saturating_sub(1));
6893
6894        // Add jitter (random value between 0 and base_delay/2)
6895        let jitter = rand::thread_rng().gen_range(0..=base_delay / 2);
6896        let total_delay = exponential_delay + jitter;
6897
6898        // Cap at max_delay
6899        let final_delay = std::cmp::min(total_delay, max_delay);
6900
6901        tracing::debug!(
6902            "Calculated backoff delay for attempt {}: {}ms (exponential: {}ms, jitter: {}ms, capped at: {}ms)",
6903            attempt,
6904            final_delay,
6905            exponential_delay,
6906            jitter,
6907            max_delay
6908        );
6909
6910        StdDuration::from_millis(final_delay)
6911    }
6912
6913    /// Extracts the Retry-After header value from a 429 response
6914    ///
6915    /// Returns the number of seconds to wait, or None if the header is not present
6916    /// or cannot be parsed
6917    fn extract_retry_after(response: &reqwest::Response) -> Option<u64> {
6918        response
6919            .headers()
6920            .get("retry-after")
6921            .and_then(|value| value.to_str().ok())
6922            .and_then(|s| s.parse::<u64>().ok())
6923    }
6924
6925    /// Gets the underlying reqwest client
6926    #[must_use]
6927    pub const fn client(&self) -> &Client {
6928        &self.client
6929    }
6930
6931    /// Gets the retry configuration
6932    #[must_use]
6933    pub const fn config(&self) -> &RetryConfig {
6934        &self.config
6935    }
6936}
6937
6938/// Singleton instance of the `TokenManager` for shared token storage across all `ApiClient` instances
6939static GLOBAL_TOKEN_MANAGER: OnceCell<Arc<TokenManager>> = OnceCell::const_new();
6940
6941/// Core token manager with proactive refresh and secure storage
6942#[derive(Debug)]
6943pub struct TokenManager {
6944    pub token_data: Arc<Mutex<Option<TokenData>>>,
6945    pub retry_client: RetryClient,
6946    base_url: Url,
6947    /// Semaphore to ensure only one token operation (obtain/refresh) happens at a time
6948    /// This prevents race conditions where multiple threads try to refresh/obtain simultaneously
6949    token_operation_semaphore: Arc<Semaphore>,
6950}
6951
6952impl TokenManager {
6953    /// Gets the global singleton instance of `TokenManager`
6954    ///
6955    /// This ensures all `ApiClient` instances share the same token storage,
6956    /// preventing multiple token acquisition attempts in concurrent tests.
6957    ///
6958    /// # Errors
6959    /// Returns an error if the `TokenManager` cannot be initialized
6960    pub async fn get_global_instance() -> Result<Arc<Self>, Error> {
6961        let manager = GLOBAL_TOKEN_MANAGER
6962            .get_or_try_init(|| async {
6963                let config = RetryConfig::from_env()?;
6964                let base_url = get_amp_api_base_url()?;
6965                let manager = Self::with_config_and_base_url(config, base_url).await?;
6966                Ok::<Arc<Self>, Error>(Arc::new(manager))
6967            })
6968            .await?;
6969
6970        Ok(manager.clone())
6971    }
6972
6973    /// Creates a new `TokenManager` with default configuration
6974    ///
6975    /// # Errors
6976    /// Returns an error if the base URL cannot be obtained from environment variables
6977    pub async fn new() -> Result<Self, Error> {
6978        let config = RetryConfig::from_env()?;
6979        Self::with_config(config).await
6980    }
6981
6982    /// Creates a new `TokenManager` with the specified retry configuration
6983    ///
6984    /// # Errors
6985    /// Returns an error if the base URL cannot be obtained from environment variables
6986    pub async fn with_config(config: RetryConfig) -> Result<Self, Error> {
6987        let base_url = get_amp_api_base_url()?;
6988        Self::with_config_and_base_url(config, base_url).await
6989    }
6990
6991    /// Creates a new `TokenManager` with the specified configuration and base URL (for testing)
6992    ///
6993    /// # Errors
6994    /// This method is infallible but returns Result for API consistency
6995    pub async fn with_config_and_base_url(
6996        config: RetryConfig,
6997        base_url: Url,
6998    ) -> Result<Self, Error> {
6999        let manager = Self {
7000            token_data: Arc::new(Mutex::new(None)),
7001            retry_client: RetryClient::new(config),
7002            base_url,
7003            token_operation_semaphore: Arc::new(Semaphore::new(1)),
7004        };
7005
7006        // Load token from disk if persistence is enabled
7007        if Self::should_persist_tokens() {
7008            if let Ok(Some(token_data)) = manager.load_token_from_disk().await {
7009                *manager.token_data.lock().await = Some(token_data);
7010                tracing::info!("Token loaded from disk during initialization");
7011            }
7012        }
7013
7014        Ok(manager)
7015    }
7016
7017    /// Creates a new `TokenManager` with a pre-set mock token (for testing)
7018    ///
7019    /// # Errors
7020    /// This method is infallible but returns Result for API consistency
7021    pub fn with_mock_token(
7022        config: RetryConfig,
7023        base_url: Url,
7024        mock_token: String,
7025    ) -> Result<Self, Error> {
7026        let expires_at = Utc::now() + Duration::hours(24); // Mock token valid for 24 hours
7027        let token_data = TokenData::new(mock_token, expires_at);
7028
7029        let manager = Self {
7030            token_data: Arc::new(Mutex::new(Some(token_data))),
7031            retry_client: RetryClient::new(config),
7032            base_url,
7033            token_operation_semaphore: Arc::new(Semaphore::new(1)),
7034        };
7035
7036        Ok(manager)
7037    }
7038
7039    /// Gets a valid authentication token with proactive refresh logic
7040    ///
7041    /// This method implements thread-safe token management logic:
7042    /// 1. Check if a valid token exists and is not expiring soon (within 5 minutes)
7043    /// 2. If token needs refresh/obtain, acquire semaphore to prevent concurrent operations
7044    /// 3. Double-check token state after acquiring semaphore (another thread may have updated it)
7045    /// 4. Perform atomic token update operations
7046    /// 5. Return the valid token
7047    ///
7048    /// # Thread Safety
7049    /// This method is fully thread-safe and prevents race conditions by:
7050    /// - Using a semaphore to ensure only one token operation at a time
7051    /// - Double-checking token state after acquiring the semaphore
7052    /// - Performing atomic token updates within the critical section
7053    ///
7054    /// # Errors
7055    /// Returns a `TokenError` if token acquisition or refresh fails after all retries
7056    pub async fn get_token(&self) -> Result<String, Error> {
7057        // Fast path: check if we have a valid token without acquiring semaphore
7058        if let Some(token) = self.check_existing_token().await? {
7059            return Ok(token);
7060        }
7061
7062        // Slow path: token needs refresh/obtain, acquire semaphore for thread safety
7063        let _permit = self.acquire_token_semaphore().await?;
7064
7065        // Double-check token state after acquiring semaphore - another thread may have updated it
7066        if let Some(token) = self.check_existing_token().await? {
7067            tracing::debug!("Token was updated by another thread, using existing valid token");
7068            return Ok(token);
7069        }
7070
7071        // At this point, we need to refresh or obtain a new token
7072        self.handle_token_refresh_or_obtain().await
7073    }
7074
7075    /// Checks if we have a valid existing token that doesn't expire soon
7076    async fn check_existing_token(&self) -> Result<Option<String>, Error> {
7077        let token_guard = self.token_data.lock().await;
7078        if let Some(ref token_data) = *token_guard {
7079            if !token_data.expires_soon(Duration::minutes(5)) {
7080                tracing::debug!("Using existing valid token");
7081                let token = token_data.token.expose_secret().clone();
7082                drop(token_guard);
7083                return Ok(Some(token));
7084            }
7085        }
7086        drop(token_guard);
7087        Ok(None)
7088    }
7089
7090    /// Acquires the token operation semaphore for thread-safe operations
7091    async fn acquire_token_semaphore(&self) -> Result<tokio::sync::SemaphorePermit<'_>, Error> {
7092        let permit = self
7093            .token_operation_semaphore
7094            .acquire()
7095            .await
7096            .map_err(|e| {
7097                Error::Token(TokenError::storage(format!(
7098                    "Failed to acquire token operation semaphore: {e}"
7099                )))
7100            })?;
7101
7102        tracing::debug!("Acquired token operation semaphore for thread-safe token management");
7103        Ok(permit)
7104    }
7105
7106    /// Handles the token refresh or obtain logic
7107    async fn handle_token_refresh_or_obtain(&self) -> Result<String, Error> {
7108        let needs_refresh = self.determine_token_operation().await;
7109
7110        if needs_refresh {
7111            match self.refresh_token_internal().await {
7112                Ok(token) => {
7113                    tracing::info!("Token refreshed successfully");
7114                    return Ok(token);
7115                }
7116                Err(e) => {
7117                    tracing::warn!("Token refresh failed, falling back to obtain: {e}");
7118                    // Fall through to obtain new token
7119                }
7120            }
7121        }
7122
7123        // Either we needed to obtain from the start, or refresh failed
7124        self.obtain_token_internal().await
7125    }
7126
7127    /// Determines whether we need to refresh or obtain a new token
7128    async fn determine_token_operation(&self) -> bool {
7129        let token_guard = self.token_data.lock().await;
7130        token_guard.as_ref().map_or_else(
7131            || {
7132                tracing::info!("No token exists, will obtain new token");
7133                false
7134            },
7135            |token_data| {
7136                if token_data.is_expired() {
7137                    tracing::info!("Token is expired, will obtain new token");
7138                    false
7139                } else {
7140                    tracing::info!("Token expires soon, will attempt refresh");
7141                    true
7142                }
7143            },
7144        )
7145    }
7146
7147    /// Obtains a new authentication token using environment credentials with retry logic
7148    ///
7149    /// This method:
7150    /// 1. Reads credentials from environment variables
7151    /// 2. Makes a token request with retry logic
7152    /// 3. Stores the new token with 24-hour expiry
7153    /// 4. Returns the token string
7154    ///
7155    /// # Thread Safety
7156    /// This method acquires the token operation semaphore to ensure thread-safe operation.
7157    /// For internal use within already-synchronized contexts, use `obtain_token_internal()`.
7158    ///
7159    /// # Errors
7160    /// Returns an error if:
7161    /// - Environment variables are missing
7162    /// - All retry attempts fail
7163    /// - Response parsing fails
7164    pub async fn obtain_token(&self) -> Result<String, Error> {
7165        let _permit = self
7166            .token_operation_semaphore
7167            .acquire()
7168            .await
7169            .map_err(|e| {
7170                Error::Token(TokenError::storage(format!(
7171                    "Failed to acquire token operation semaphore: {e}"
7172                )))
7173            })?;
7174
7175        self.obtain_token_internal().await
7176    }
7177
7178    /// Internal method to obtain a new authentication token without acquiring semaphore
7179    ///
7180    /// This method should only be called from contexts where the token operation semaphore
7181    /// has already been acquired (e.g., from within `get_token()`).
7182    ///
7183    /// # Errors
7184    /// Returns an error if:
7185    /// - Environment variables are missing
7186    /// - All retry attempts fail
7187    /// - Response parsing fails
7188    async fn obtain_token_internal(&self) -> Result<String, Error> {
7189        tracing::debug!("Obtaining new authentication token");
7190
7191        let request_payload = Self::get_credentials_from_env()?;
7192        let url = self.build_obtain_token_url();
7193        let response = self.execute_token_request(&url, &request_payload).await?;
7194        let token_response = self.parse_token_response(response).await?;
7195
7196        self.store_token_data(&token_response.token).await;
7197
7198        tracing::info!("New authentication token obtained successfully");
7199        Ok(token_response.token)
7200    }
7201
7202    /// Gets credentials from environment variables
7203    fn get_credentials_from_env() -> Result<TokenRequest, Error> {
7204        let username = env::var("AMP_USERNAME")
7205            .map_err(|_| Error::MissingEnvVar("AMP_USERNAME".to_string()))?;
7206        let password = env::var("AMP_PASSWORD")
7207            .map_err(|_| Error::MissingEnvVar("AMP_PASSWORD".to_string()))?;
7208
7209        Ok(TokenRequest { username, password })
7210    }
7211
7212    /// Builds the URL for token obtain endpoint
7213    fn build_obtain_token_url(&self) -> Url {
7214        let mut url = self.base_url.clone();
7215        url.path_segments_mut()
7216            .unwrap()
7217            .push("user")
7218            .push("obtain_token");
7219        url
7220    }
7221
7222    /// Executes the token request with retry logic
7223    async fn execute_token_request(
7224        &self,
7225        url: &Url,
7226        request_payload: &TokenRequest,
7227    ) -> Result<reqwest::Response, Error> {
7228        let response = self
7229            .retry_client
7230            .execute_with_retry(|| {
7231                self.retry_client
7232                    .client()
7233                    .post(url.clone())
7234                    .json(request_payload)
7235            })
7236            .await
7237            .map_err(Error::Token)?;
7238
7239        if !response.status().is_success() {
7240            let status = response.status();
7241            let error_text = response
7242                .text()
7243                .await
7244                .unwrap_or_else(|_| "Unknown error".to_string());
7245            return Err(Error::TokenRequestFailed { status, error_text });
7246        }
7247
7248        Ok(response)
7249    }
7250
7251    /// Parses the token response from the API
7252    async fn parse_token_response(
7253        &self,
7254        response: reqwest::Response,
7255    ) -> Result<TokenResponse, Error> {
7256        response
7257            .json()
7258            .await
7259            .map_err(|e| Error::ResponseParsingFailed(e.to_string()))
7260    }
7261
7262    /// Stores the token data with 24-hour expiry and optional disk persistence
7263    async fn store_token_data(&self, token: &str) {
7264        let expires_at = Utc::now() + Duration::days(1);
7265        let token_data = TokenData::new(token.to_string(), expires_at);
7266
7267        // Atomic token update - hold the lock for the minimal time needed
7268        *self.token_data.lock().await = Some(token_data.clone());
7269        tracing::debug!("Token data updated atomically in storage");
7270
7271        // Save to disk if persistence is enabled
7272        if Self::should_persist_tokens() {
7273            if let Err(e) = self.save_token_to_disk(&token_data).await {
7274                tracing::warn!("Failed to save token to disk: {e}");
7275            }
7276        }
7277    }
7278
7279    /// Refreshes the current authentication token with fallback to obtain on failure
7280    ///
7281    /// This method:
7282    /// 1. Uses the existing token to request a refresh
7283    /// 2. Updates the stored token data on success
7284    /// 3. Falls back to obtaining a new token if refresh fails
7285    ///
7286    /// # Thread Safety
7287    /// This method acquires the token operation semaphore to ensure thread-safe operation.
7288    /// For internal use within already-synchronized contexts, use `refresh_token_internal()`.
7289    ///
7290    /// # Errors
7291    /// Returns an error if both refresh and obtain operations fail
7292    pub async fn refresh_token(&self) -> Result<String, Error> {
7293        let _permit = self
7294            .token_operation_semaphore
7295            .acquire()
7296            .await
7297            .map_err(|e| {
7298                Error::Token(TokenError::storage(format!(
7299                    "Failed to acquire token operation semaphore: {e}"
7300                )))
7301            })?;
7302
7303        self.refresh_token_internal().await
7304    }
7305
7306    /// Internal method to refresh the current authentication token without acquiring semaphore
7307    ///
7308    /// This method should only be called from contexts where the token operation semaphore
7309    /// has already been acquired (e.g., from within `get_token()`).
7310    ///
7311    /// # Errors
7312    /// Returns an error if both refresh and obtain operations fail
7313    #[allow(clippy::cognitive_complexity)]
7314    async fn refresh_token_internal(&self) -> Result<String, Error> {
7315        tracing::debug!("Refreshing authentication token");
7316
7317        let Some(current_token) = self.get_current_token_for_refresh().await else {
7318            tracing::warn!("No token available for refresh, obtaining new token");
7319            return self.obtain_token_internal().await;
7320        };
7321
7322        let url = self.build_refresh_token_url();
7323        let response = self.execute_refresh_request(&url, &current_token).await;
7324
7325        match response {
7326            Ok(resp) => self.handle_refresh_response(resp).await,
7327            Err(e) => {
7328                tracing::warn!("Token refresh request failed: {e}, falling back to obtain");
7329                self.obtain_token_internal().await
7330            }
7331        }
7332    }
7333
7334    /// Gets the current token for refresh operations
7335    async fn get_current_token_for_refresh(&self) -> Option<String> {
7336        let token_guard = self.token_data.lock().await;
7337        token_guard
7338            .as_ref()
7339            .map(|token_data| token_data.token.expose_secret().clone())
7340    }
7341
7342    /// Builds the URL for token refresh endpoint
7343    fn build_refresh_token_url(&self) -> Url {
7344        let mut url = self.base_url.clone();
7345        url.path_segments_mut()
7346            .unwrap()
7347            .push("user")
7348            .push("refresh_token");
7349        url
7350    }
7351
7352    /// Executes the refresh request with retry logic
7353    async fn execute_refresh_request(
7354        &self,
7355        url: &Url,
7356        current_token: &str,
7357    ) -> Result<reqwest::Response, TokenError> {
7358        self.retry_client
7359            .execute_with_retry(|| {
7360                self.retry_client
7361                    .client()
7362                    .post(url.clone())
7363                    .header(AUTHORIZATION, format!("token {current_token}"))
7364            })
7365            .await
7366    }
7367
7368    /// Handles the refresh response, either storing the new token or falling back to obtain
7369    async fn handle_refresh_response(&self, resp: reqwest::Response) -> Result<String, Error> {
7370        if !resp.status().is_success() {
7371            let status = resp.status();
7372            let error_text = resp
7373                .text()
7374                .await
7375                .unwrap_or_else(|_| "Unknown error".to_string());
7376
7377            tracing::warn!("Token refresh failed with status {status}: {error_text}");
7378            return self.obtain_token_internal().await;
7379        }
7380
7381        let token_response: TokenResponse = resp
7382            .json()
7383            .await
7384            .map_err(|e| Error::ResponseParsingFailed(e.to_string()))?;
7385
7386        self.store_token_data(&token_response.token).await;
7387        tracing::info!("Authentication token refreshed successfully");
7388        Ok(token_response.token)
7389    }
7390
7391    /// Gets current token information for debugging and monitoring
7392    ///
7393    /// Returns detailed information about the current token including:
7394    /// - Expiry time and remaining duration
7395    /// - Token age since acquisition
7396    /// - Expiry status flags
7397    ///
7398    /// # Returns
7399    /// `Some(TokenInfo)` if a token exists, `None` if no token is stored
7400    ///
7401    /// # Errors
7402    /// Returns an error if token information retrieval fails
7403    pub async fn get_token_info(&self) -> Result<Option<TokenInfo>, Error> {
7404        tracing::debug!("Retrieving token information for debugging");
7405
7406        let token_info = self.token_data.lock().await.as_ref().map(TokenInfo::from);
7407
7408        match &token_info {
7409            Some(info) => {
7410                tracing::debug!(
7411                    "Token info retrieved - expires_at: {}, age: {:?}, expires_in: {:?}, is_expired: {}, expires_soon: {}",
7412                    info.expires_at,
7413                    info.age,
7414                    info.expires_in,
7415                    info.is_expired,
7416                    info.expires_soon
7417                );
7418            }
7419            None => {
7420                tracing::debug!("No token information available - no token stored");
7421            }
7422        }
7423
7424        Ok(token_info)
7425    }
7426
7427    /// Clears the stored token (useful for testing scenarios)
7428    ///
7429    /// This method removes the current token from storage, forcing the next
7430    /// `get_token()` call to obtain a fresh token.
7431    ///
7432    /// # Errors
7433    /// Returns an error if token clearing fails
7434    pub async fn clear_token(&self) -> Result<(), Error> {
7435        tracing::debug!("Clearing stored token from memory and disk");
7436
7437        let had_token = self.clear_token_from_memory().await;
7438        self.clear_token_from_disk_if_enabled().await;
7439        Self::log_token_clear_result(had_token);
7440
7441        Ok(())
7442    }
7443
7444    /// Clears the token from memory and returns whether a token was present
7445    async fn clear_token_from_memory(&self) -> bool {
7446        let mut token_guard = self.token_data.lock().await;
7447        let had_token = token_guard.is_some();
7448        *token_guard = None;
7449        drop(token_guard);
7450        had_token
7451    }
7452
7453    /// Clears the token from disk if persistence is enabled
7454    async fn clear_token_from_disk_if_enabled(&self) {
7455        if Self::should_persist_tokens() {
7456            if let Err(e) = self.remove_token_from_disk().await {
7457                tracing::warn!("Failed to remove token from disk: {e}");
7458            }
7459        }
7460    }
7461
7462    /// Logs the result of the token clearing operation
7463    fn log_token_clear_result(had_token: bool) {
7464        if had_token {
7465            tracing::info!("Token successfully cleared from memory and disk - next get_token() will obtain fresh token");
7466        } else {
7467            tracing::debug!("No token was stored to clear");
7468        }
7469    }
7470
7471    /// Forces a token refresh regardless of current token status
7472    ///
7473    /// This method bypasses the normal proactive refresh logic and immediately
7474    /// attempts to refresh the current token. If no token exists or refresh fails,
7475    /// it falls back to obtaining a new token.
7476    ///
7477    /// # Thread Safety
7478    /// This method is fully thread-safe and uses the same semaphore-based synchronization
7479    /// as other token operations to prevent race conditions.
7480    ///
7481    /// # Errors
7482    /// Returns an error if both refresh and obtain operations fail
7483    pub async fn force_refresh(&self) -> Result<String, Error> {
7484        tracing::info!("Forcing token refresh - bypassing normal proactive refresh logic");
7485
7486        let _permit = self.acquire_token_semaphore().await?;
7487        self.log_token_status_for_refresh().await;
7488        self.execute_forced_refresh().await
7489    }
7490
7491    /// Logs the current token status for forced refresh operation
7492    async fn log_token_status_for_refresh(&self) {
7493        let has_token = {
7494            let token_guard = self.token_data.lock().await;
7495            token_guard.is_some()
7496        };
7497
7498        if has_token {
7499            tracing::debug!("Existing token found, attempting forced refresh");
7500        } else {
7501            tracing::debug!("No existing token found, will obtain new token");
7502        }
7503    }
7504
7505    /// Executes the forced refresh operation
7506    async fn execute_forced_refresh(&self) -> Result<String, Error> {
7507        match self.refresh_token_internal().await {
7508            Ok(token) => {
7509                tracing::info!("Forced token refresh completed successfully");
7510                Ok(token)
7511            }
7512            Err(e) => {
7513                tracing::error!("Forced token refresh failed: {e}");
7514                Err(e)
7515            }
7516        }
7517    }
7518
7519    /// Determines if token persistence is enabled based on environment variables
7520    ///
7521    /// Token persistence is enabled when:
7522    /// - `AMP_TESTS=live` (for live API testing)
7523    /// - `AMP_TOKEN_PERSISTENCE=true` is set
7524    /// - NOT in mock test environments (to prevent test pollution)
7525    fn should_persist_tokens() -> bool {
7526        // Use the new environment detection logic
7527        let environment = TokenEnvironment::detect();
7528
7529        // Never persist tokens in mock environments to prevent test pollution
7530        if environment.is_mock() {
7531            tracing::debug!("Token persistence disabled - mock environment detected");
7532            return false;
7533        }
7534
7535        // Check if explicitly enabled
7536        if env::var("AMP_TOKEN_PERSISTENCE").unwrap_or_default() == "true" {
7537            tracing::debug!("Token persistence enabled - AMP_TOKEN_PERSISTENCE=true");
7538            return true;
7539        }
7540
7541        // Use environment-based persistence setting
7542        let should_persist = environment.should_persist_tokens();
7543        tracing::debug!(
7544            "Token persistence setting from environment: {}",
7545            should_persist
7546        );
7547        should_persist
7548    }
7549
7550    /// Loads token data from disk if it exists and is valid
7551    async fn load_token_from_disk(&self) -> Result<Option<TokenData>, Error> {
7552        let token_file = "token.json";
7553
7554        if !self.token_file_exists(token_file).await {
7555            return Ok(None);
7556        }
7557
7558        let content = self.read_token_file(token_file).await?;
7559        self.parse_and_validate_token(token_file, &content).await
7560    }
7561
7562    /// Checks if the token file exists on disk
7563    async fn token_file_exists(&self, token_file: &str) -> bool {
7564        tokio::fs::try_exists(token_file).await.map_or_else(
7565            |_| {
7566                tracing::debug!("Error checking token file existence: {}", token_file);
7567                false
7568            },
7569            |exists| {
7570                if !exists {
7571                    tracing::debug!("Token file does not exist: {}", token_file);
7572                }
7573                exists
7574            },
7575        )
7576    }
7577
7578    /// Reads the token file content from disk
7579    async fn read_token_file(&self, token_file: &str) -> Result<String, Error> {
7580        use tokio::fs;
7581
7582        match fs::read_to_string(token_file).await {
7583            Ok(content) => Ok(content),
7584            Err(e) => {
7585                tracing::warn!("Failed to read token file: {e}");
7586                Err(Error::Token(TokenError::storage(format!(
7587                    "Failed to read token file: {e}"
7588                ))))
7589            }
7590        }
7591    }
7592
7593    /// Parses token content and validates expiration
7594    async fn parse_and_validate_token(
7595        &self,
7596        token_file: &str,
7597        content: &str,
7598    ) -> Result<Option<TokenData>, Error> {
7599        match serde_json::from_str::<TokenData>(content) {
7600            Ok(token_data) => self.handle_parsed_token(token_file, token_data).await,
7601            Err(e) => self.handle_parse_error(token_file, e).await,
7602        }
7603    }
7604
7605    /// Handles successfully parsed token data, checking expiration
7606    async fn handle_parsed_token(
7607        &self,
7608        token_file: &str,
7609        token_data: TokenData,
7610    ) -> Result<Option<TokenData>, Error> {
7611        if token_data.is_expired() {
7612            tracing::info!("Token loaded from disk is expired, removing file");
7613            let _ = tokio::fs::remove_file(token_file).await;
7614            Ok(None)
7615        } else {
7616            tracing::info!("Valid token loaded from disk");
7617            Ok(Some(token_data))
7618        }
7619    }
7620
7621    /// Handles token parsing errors by cleaning up the invalid file
7622    async fn handle_parse_error(
7623        &self,
7624        token_file: &str,
7625        e: serde_json::Error,
7626    ) -> Result<Option<TokenData>, Error> {
7627        tracing::warn!("Failed to parse token file, removing: {e}");
7628        let _ = tokio::fs::remove_file(token_file).await;
7629        Err(Error::Token(TokenError::serialization(format!(
7630            "Failed to parse token file: {e}"
7631        ))))
7632    }
7633
7634    /// Saves token data to disk
7635    async fn save_token_to_disk(&self, token_data: &TokenData) -> Result<(), Error> {
7636        use tokio::fs;
7637
7638        let token_file = "token.json";
7639
7640        match serde_json::to_string_pretty(token_data) {
7641            Ok(json) => match fs::write(token_file, json).await {
7642                Ok(()) => {
7643                    tracing::debug!("Token saved to disk: {}", token_file);
7644                    Ok(())
7645                }
7646                Err(e) => {
7647                    tracing::error!("Failed to write token file: {e}");
7648                    Err(Error::Token(TokenError::storage(format!(
7649                        "Failed to write token file: {e}"
7650                    ))))
7651                }
7652            },
7653            Err(e) => {
7654                tracing::error!("Failed to serialize token data: {e}");
7655                Err(Error::Token(TokenError::serialization(format!(
7656                    "Failed to serialize token data: {e}"
7657                ))))
7658            }
7659        }
7660    }
7661
7662    /// Removes the token file from disk
7663    async fn remove_token_from_disk(&self) -> Result<(), Error> {
7664        use tokio::fs;
7665
7666        let token_file = "token.json";
7667
7668        match fs::remove_file(token_file).await {
7669            Ok(()) => {
7670                tracing::debug!("Token file removed from disk: {}", token_file);
7671                Ok(())
7672            }
7673            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
7674                tracing::debug!("Token file does not exist, nothing to remove");
7675                Ok(())
7676            }
7677            Err(e) => {
7678                tracing::warn!("Failed to remove token file: {e}");
7679                Err(Error::Token(TokenError::storage(format!(
7680                    "Failed to remove token file: {e}"
7681                ))))
7682            }
7683        }
7684    }
7685
7686    /// Forces cleanup of token persistence files (useful for testing)
7687    /// This method removes token files regardless of persistence settings
7688    ///
7689    /// # Errors
7690    /// Returns an error if:
7691    /// - File system permissions prevent deletion of the token file
7692    /// - I/O errors occur during file deletion operations
7693    /// - The token file is locked by another process
7694    pub async fn force_cleanup_token_files() -> Result<(), Error> {
7695        use tokio::fs;
7696
7697        let token_file = "token.json";
7698
7699        match fs::remove_file(token_file).await {
7700            Ok(()) => {
7701                tracing::debug!("Token file forcefully removed: {}", token_file);
7702                Ok(())
7703            }
7704            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
7705                tracing::debug!("No token file to clean up");
7706                Ok(())
7707            }
7708            Err(e) => {
7709                tracing::warn!("Failed to force cleanup token file: {e}");
7710                Err(Error::Token(TokenError::storage(format!(
7711                    "Failed to force cleanup token file: {e}"
7712                ))))
7713            }
7714        }
7715    }
7716
7717    /// Resets the global `TokenManager` singleton (useful for testing)
7718    ///
7719    /// This method clears the global singleton instance, forcing the next
7720    /// call to `get_global_instance()` to create a fresh `TokenManager`.
7721    /// Primarily intended for test scenarios where a clean state is needed.
7722    ///
7723    /// # Errors
7724    /// Returns an error if:
7725    /// - Token clearing operations fail during the reset process
7726    /// - File system errors occur when clearing persistent token data
7727    /// - The global instance is in an invalid state that prevents cleanup
7728    pub async fn reset_global_instance() -> Result<(), Error> {
7729        // Clear any existing token from the current global instance
7730        if let Some(manager) = GLOBAL_TOKEN_MANAGER.get() {
7731            let _ = manager.clear_token().await;
7732        }
7733
7734        // Reset the OnceCell to allow a new instance to be created
7735        // Note: OnceCell doesn't have a reset method, so we can't actually reset it
7736        // The best we can do is clear the token from the existing instance
7737        tracing::debug!("Global TokenManager instance token cleared for testing");
7738        Ok(())
7739    }
7740}
7741
7742#[derive(Debug)]
7743pub struct ApiClient {
7744    client: Client,
7745    base_url: Url,
7746    token_strategy: Box<dyn TokenStrategy>,
7747}
7748
7749#[allow(clippy::missing_errors_doc, clippy::missing_panics_doc)]
7750impl ApiClient {
7751    /// Creates a new API client with the base URL from environment variables.
7752    ///
7753    /// Automatically selects the appropriate token strategy based on environment detection:
7754    /// - Mock strategy for mock environments (no persistence, isolated tokens)
7755    /// - Live strategy for live environments (full token management with persistence)
7756    ///
7757    /// # Errors
7758    ///
7759    /// Returns an error if:
7760    /// - The `AMP_API_BASE_URL` environment variable contains an invalid URL
7761    /// - Token strategy initialization fails
7762    ///
7763    /// # Examples
7764    /// ```no_run
7765    /// # use amp_rs::ApiClient;
7766    /// # #[tokio::main]
7767    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
7768    /// // Create a new client - automatically detects environment
7769    /// let client = ApiClient::new().await?;
7770    ///
7771    /// // Client is ready to use
7772    /// let assets = client.get_assets().await?;
7773    /// println!("Found {} assets", assets.len());
7774    /// # Ok(())
7775    /// # }
7776    /// ```
7777    pub async fn new() -> Result<Self, Error> {
7778        let base_url = get_amp_api_base_url()?;
7779        let client = Client::new();
7780
7781        // Automatic strategy selection based on environment
7782        let token_strategy = TokenEnvironment::create_auto_strategy(None).await?;
7783
7784        tracing::info!(
7785            "Created ApiClient with {} strategy for base URL: {}",
7786            token_strategy.strategy_type(),
7787            base_url
7788        );
7789
7790        Ok(Self {
7791            client,
7792            base_url,
7793            token_strategy,
7794        })
7795    }
7796
7797    /// Creates a new API client with the specified base URL.
7798    ///
7799    /// Automatically selects the appropriate token strategy based on environment detection.
7800    ///
7801    /// # Errors
7802    ///
7803    /// Returns an error if token strategy initialization fails.
7804    ///
7805    /// # Examples
7806    /// ```no_run
7807    /// # use amp_rs::ApiClient;
7808    /// # use reqwest::Url;
7809    /// # #[tokio::main]
7810    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
7811    /// let base_url = Url::parse("https://amp-test.blockstream.com/api")?;
7812    /// let client = ApiClient::with_base_url(base_url).await?;
7813    ///
7814    /// // Client is ready to use with the specified URL
7815    /// let assets = client.get_assets().await?;
7816    /// # Ok(())
7817    /// # }
7818    /// ```
7819    pub async fn with_base_url(base_url: Url) -> Result<Self, Error> {
7820        let client = Client::new();
7821
7822        // Automatic strategy selection based on environment
7823        let token_strategy = TokenEnvironment::create_auto_strategy(None).await?;
7824
7825        tracing::info!(
7826            "Created ApiClient with {} strategy for base URL: {}",
7827            token_strategy.strategy_type(),
7828            base_url
7829        );
7830
7831        Ok(Self {
7832            client,
7833            base_url,
7834            token_strategy,
7835        })
7836    }
7837
7838    /// Creates a new API client with a custom token strategy (useful for testing).
7839    ///
7840    /// # Errors
7841    ///
7842    /// Returns an error if the base URL cannot be obtained from environment variables.
7843    pub fn with_token_strategy(token_strategy: Box<dyn TokenStrategy>) -> Result<Self, Error> {
7844        let base_url = get_amp_api_base_url()?;
7845
7846        tracing::info!(
7847            "Created ApiClient with explicit {} strategy for base URL: {}",
7848            token_strategy.strategy_type(),
7849            base_url
7850        );
7851
7852        Ok(Self {
7853            client: Client::new(),
7854            base_url,
7855            token_strategy,
7856        })
7857    }
7858
7859    /// Creates a new API client with a custom token manager (useful for testing).
7860    ///
7861    /// # Errors
7862    ///
7863    /// Returns an error if the base URL cannot be obtained from environment variables.
7864    pub fn with_token_manager(token_manager: Arc<TokenManager>) -> Result<Self, Error> {
7865        let base_url = get_amp_api_base_url()?;
7866        let token_strategy: Box<dyn TokenStrategy> =
7867            Box::new(LiveTokenStrategy::with_token_manager(token_manager));
7868
7869        tracing::info!(
7870            "Created ApiClient with custom token manager for base URL: {}",
7871            base_url
7872        );
7873
7874        Ok(Self {
7875            client: Client::new(),
7876            base_url,
7877            token_strategy,
7878        })
7879    }
7880
7881    /// Creates a new API client for testing with a mock token strategy that always returns a fixed token.
7882    /// This bypasses all token acquisition and management logic and uses complete isolation.
7883    ///
7884    /// # Errors
7885    ///
7886    /// This method is infallible but returns Result for API consistency.
7887    ///
7888    /// # Examples
7889    /// ```
7890    /// # use amp_rs::ApiClient;
7891    /// # use reqwest::Url;
7892    /// # #[tokio::main]
7893    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
7894    /// let base_url = Url::parse("http://localhost:8080/api")?;
7895    /// let client = ApiClient::with_mock_token(base_url, "test_token".to_string())?;
7896    ///
7897    /// // Client will always use "test_token" for authentication
7898    /// let token = client.get_token().await?;
7899    /// assert_eq!(token, "test_token");
7900    /// # Ok(())
7901    /// # }
7902    /// ```
7903    pub fn with_mock_token(base_url: Url, mock_token: String) -> Result<Self, Error> {
7904        let client = Client::new();
7905        let token_strategy: Box<dyn TokenStrategy> = Box::new(MockTokenStrategy::new(mock_token));
7906
7907        tracing::info!(
7908            "Created ApiClient with explicit mock token strategy for base URL: {}",
7909            base_url
7910        );
7911
7912        Ok(Self {
7913            client,
7914            base_url,
7915            token_strategy,
7916        })
7917    }
7918
7919    /// Obtains a new authentication token from the AMP API.
7920    ///
7921    /// **Note**: This method is deprecated in favor of the automatic token management
7922    /// provided by `get_token()`. The `TokenManager` handles token acquisition internally
7923    /// with enhanced retry logic and error handling.
7924    ///
7925    /// # Errors
7926    ///
7927    /// Returns an error if:
7928    /// - The `AMP_USERNAME` or `AMP_PASSWORD` environment variables are not set
7929    /// - The HTTP request fails
7930    /// - The token request is rejected by the server
7931    /// - The response cannot be parsed
7932    #[deprecated(note = "Use get_token() instead - it provides automatic token management")]
7933    pub async fn obtain_amp_token(&self) -> Result<String, Error> {
7934        // Delegate to get_token for backward compatibility
7935        self.get_token().await
7936    }
7937
7938    /// Gets current token information for debugging and monitoring.
7939    ///
7940    /// Returns detailed information about the current token including:
7941    /// - Expiry time and remaining duration
7942    /// - Token age since acquisition
7943    /// - Expiry status flags
7944    ///
7945    /// Note: Mock strategies may return limited or no token information.
7946    ///
7947    /// # Returns
7948    /// `Some(TokenInfo)` if a token exists, `None` if no token is stored or strategy doesn't support info
7949    ///
7950    /// # Errors
7951    /// Returns an error if token information retrieval fails
7952    ///
7953    /// # Examples
7954    /// ```no_run
7955    /// # use amp_rs::ApiClient;
7956    /// # #[tokio::main]
7957    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
7958    /// let client = ApiClient::new().await?;
7959    ///
7960    /// if let Some(token_info) = client.get_token_info().await? {
7961    ///     println!("Token expires at: {}", token_info.expires_at);
7962    ///     println!("Token is expired: {}", token_info.is_expired);
7963    /// } else {
7964    ///     println!("No token stored or mock strategy in use");
7965    /// }
7966    /// # Ok(())
7967    /// # }
7968    /// ```
7969    pub async fn get_token_info(&self) -> Result<Option<TokenInfo>, Error> {
7970        // Only live strategies support detailed token information
7971        if let Some(live_strategy) = self
7972            .token_strategy
7973            .as_any()
7974            .downcast_ref::<LiveTokenStrategy>()
7975        {
7976            live_strategy.get_token_info().await
7977        } else {
7978            // Mock strategies don't provide detailed token information
7979            tracing::debug!(
7980                "Token info not available for {} strategy",
7981                self.token_strategy.strategy_type()
7982            );
7983            Ok(None)
7984        }
7985    }
7986
7987    /// Clears the stored token (useful for testing scenarios).
7988    ///
7989    /// This method removes the current token from storage, forcing the next
7990    /// `get_token()` call to obtain a fresh token.
7991    ///
7992    /// # Errors
7993    /// Returns an error if token clearing fails
7994    ///
7995    /// # Examples
7996    /// ```no_run
7997    /// # use amp_rs::ApiClient;
7998    /// # #[tokio::main]
7999    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
8000    /// let client = ApiClient::new().await?;
8001    ///
8002    /// // Clear any existing token
8003    /// client.clear_token().await?;
8004    ///
8005    /// // Next get_token() call will obtain a fresh token
8006    /// let token = client.get_token().await?;
8007    /// # Ok(())
8008    /// # }
8009    /// ```
8010    pub async fn clear_token(&self) -> Result<(), Error> {
8011        self.token_strategy.clear_token().await
8012    }
8013
8014    /// Forces a token refresh regardless of current token status.
8015    ///
8016    /// This method bypasses the normal proactive refresh logic and immediately
8017    /// attempts to refresh the current token. If no token exists or refresh fails,
8018    /// it falls back to obtaining a new token.
8019    ///
8020    /// # Errors
8021    /// Returns an error if both refresh and obtain operations fail
8022    pub async fn force_refresh(&self) -> Result<String, Error> {
8023        // Clear current token and get a fresh one
8024        self.token_strategy.clear_token().await?;
8025        self.token_strategy.get_token().await
8026    }
8027
8028    /// Resets the global `TokenManager` singleton (useful for testing).
8029    ///
8030    /// This method clears the token from the global `TokenManager` instance.
8031    /// Primarily intended for test scenarios where a clean token state is needed.
8032    ///
8033    /// # Errors
8034    /// Returns an error if the reset operation fails
8035    pub async fn reset_global_token_manager() -> Result<(), Error> {
8036        TokenManager::reset_global_instance().await
8037    }
8038
8039    /// Gets a valid authentication token with automatic token management.
8040    ///
8041    /// This method uses the integrated `TokenManager` to handle:
8042    /// - Proactive token refresh (5 minutes before expiry)
8043    /// - Automatic fallback from refresh to obtain on failure
8044    /// - Retry logic with exponential backoff
8045    /// - Thread-safe token storage
8046    ///
8047    /// # Errors
8048    ///
8049    /// Returns an error if token acquisition or refresh fails after all retries.
8050    ///
8051    /// # Examples
8052    /// ```no_run
8053    /// # use amp_rs::ApiClient;
8054    /// # #[tokio::main]
8055    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
8056    /// let client = ApiClient::new().await?;
8057    ///
8058    /// // Get a valid token - automatically handles refresh if needed
8059    /// let token = client.get_token().await?;
8060    /// println!("Got token: {}", &token[..10]); // Print first 10 chars
8061    /// # Ok(())
8062    /// # }
8063    /// ```
8064    pub async fn get_token(&self) -> Result<String, Error> {
8065        self.token_strategy.get_token().await
8066    }
8067
8068    /// Returns the type of token strategy currently in use
8069    ///
8070    /// This is useful for debugging and testing to verify the correct strategy is selected.
8071    ///
8072    /// # Returns
8073    /// A string indicating the strategy type: "mock" or "live"
8074    #[must_use]
8075    pub fn get_strategy_type(&self) -> &'static str {
8076        self.token_strategy.strategy_type()
8077    }
8078
8079    /// Returns whether the current strategy persists tokens
8080    ///
8081    /// This is useful for understanding the token management behavior.
8082    ///
8083    /// # Returns
8084    /// `true` if tokens are persisted to disk, `false` for in-memory only
8085    #[must_use]
8086    pub fn should_persist_tokens(&self) -> bool {
8087        self.token_strategy.should_persist()
8088    }
8089
8090    /// Force cleanup of token files (for test cleanup)
8091    ///
8092    /// This is a static method that can be used to cleanup token files
8093    /// without needing an `ApiClient` instance. Useful for test teardown.
8094    ///
8095    /// # Errors
8096    /// Returns an error if token file cleanup fails
8097    pub async fn force_cleanup_token_files() -> Result<(), Error> {
8098        // Only cleanup if we're not in a live test environment
8099        let environment = TokenEnvironment::detect();
8100        if !environment.is_live() || environment.is_mock() {
8101            TokenManager::force_cleanup_token_files().await?;
8102            tracing::debug!("Token files cleaned up for non-live environment");
8103        } else {
8104            tracing::debug!("Skipping token file cleanup in live environment");
8105        }
8106        Ok(())
8107    }
8108
8109    async fn request_raw(
8110        &self,
8111        method: Method,
8112        path: &[&str],
8113        body: Option<impl serde::Serialize>,
8114    ) -> Result<reqwest::Response, Error> {
8115        let debug_logging = std::env::var("AMP_DEBUG").is_ok();
8116
8117        if debug_logging {
8118            eprintln!("🌐 HTTP Request: {} /{}", method, path.join("/"));
8119        }
8120
8121        let token = self.get_token().await?;
8122        let mut url = self.base_url.clone();
8123        url.path_segments_mut().unwrap().extend(path);
8124
8125        if debug_logging {
8126            eprintln!("🔗 Full URL: {url}");
8127        }
8128
8129        // Retry logic for network issues
8130        let max_retries = 3;
8131        let mut last_error = None;
8132
8133        for attempt in 1..=max_retries {
8134            if debug_logging && attempt > 1 {
8135                eprintln!("🔄 Retry attempt {attempt} of {max_retries}");
8136            }
8137
8138            let mut request_builder = self
8139                .client
8140                .request(method.clone(), url.clone())
8141                .header(AUTHORIZATION, format!("token {token}"))
8142                .timeout(std::time::Duration::from_secs(60)); // Increase timeout to 60 seconds
8143
8144            if let Some(ref body) = body {
8145                if debug_logging && attempt == 1 {
8146                    if let Ok(json_body) = serde_json::to_string_pretty(&body) {
8147                        eprintln!(
8148                            "📤 Request body ({} bytes):\n{}",
8149                            json_body.len(),
8150                            json_body
8151                        );
8152                    } else {
8153                        eprintln!("📤 Request body: [serialization failed]");
8154                    }
8155                }
8156                request_builder = request_builder.json(&body);
8157            } else if debug_logging && attempt == 1 {
8158                eprintln!("📤 Request body: [empty]");
8159            }
8160
8161            if debug_logging {
8162                eprintln!("🚀 Sending HTTP request (attempt {attempt})...");
8163            }
8164
8165            match request_builder.send().await {
8166                Ok(response) => {
8167                    let status = response.status();
8168
8169                    if debug_logging {
8170                        eprintln!("📥 Response status: {status}");
8171                    }
8172
8173                    if !status.is_success() {
8174                        let error_text = response
8175                            .text()
8176                            .await
8177                            .unwrap_or_else(|_| "Unknown error".to_string());
8178
8179                        if debug_logging {
8180                            eprintln!("❌ Error response body: {error_text}");
8181                        }
8182
8183                        return Err(Error::RequestFailed(format!(
8184                            "Request to {path:?} failed with status {status}: {error_text}"
8185                        )));
8186                    }
8187
8188                    if debug_logging {
8189                        eprintln!("✅ HTTP request successful");
8190                    }
8191
8192                    return Ok(response);
8193                }
8194                Err(e) => {
8195                    if debug_logging {
8196                        eprintln!("❌ HTTP request failed (attempt {attempt}): {e:?}");
8197                        eprintln!("   Error kind: {:?}", e.is_timeout());
8198                        eprintln!("   Is connect error: {}", e.is_connect());
8199                        eprintln!("   Is request error: {}", e.is_request());
8200                    }
8201
8202                    last_error = Some(e);
8203
8204                    // Only retry on network/connection errors, not on client errors
8205                    if attempt < max_retries {
8206                        #[allow(clippy::cast_sign_loss)] // attempt is always positive (1-3)
8207                        let delay = std::time::Duration::from_millis((attempt as u64) * 1000);
8208                        if debug_logging {
8209                            eprintln!("⏳ Waiting {}ms before retry...", delay.as_millis());
8210                        }
8211                        tokio::time::sleep(delay).await;
8212                    }
8213                }
8214            }
8215        }
8216
8217        // If we get here, all retries failed
8218        if debug_logging {
8219            eprintln!("❌ All {max_retries} retry attempts failed");
8220        }
8221
8222        Err(Error::Reqwest(last_error.unwrap()))
8223    }
8224
8225    async fn request_json<T: DeserializeOwned>(
8226        &self,
8227        method: Method,
8228        path: &[&str],
8229        body: Option<impl serde::Serialize>,
8230    ) -> Result<T, Error> {
8231        // Capture request context for better error messages
8232        let method_str = method.to_string();
8233        let mut url = self.base_url.clone();
8234        url.path_segments_mut().unwrap().extend(path);
8235        let endpoint = url.to_string();
8236        let expected_type = std::any::type_name::<T>().to_string();
8237
8238        let response = self.request_raw(method, path, body).await?;
8239
8240        // Try to deserialize, capturing raw response on failure
8241        match response.text().await {
8242            Ok(raw_response) => serde_json::from_str(&raw_response).map_err(|e| {
8243                Error::ResponseDeserializationFailed {
8244                    method: method_str,
8245                    endpoint,
8246                    expected_type,
8247                    serde_error: e.to_string(),
8248                    raw_response,
8249                }
8250            }),
8251            Err(e) => Err(Error::ResponseParsingFailed(format!(
8252                "Failed to read response body: {e}"
8253            ))),
8254        }
8255    }
8256
8257    async fn request_empty(
8258        &self,
8259        method: Method,
8260        path: &[&str],
8261        body: Option<impl serde::Serialize>,
8262    ) -> Result<(), Error> {
8263        self.request_raw(method, path, body).await?;
8264        Ok(())
8265    }
8266
8267    /// Gets the API changelog.
8268    ///
8269    /// # Errors
8270    ///
8271    /// Returns an error if:
8272    /// - Authentication fails
8273    /// - The HTTP request fails
8274    /// - The server returns an error status
8275    /// - The response cannot be parsed as JSON
8276    pub async fn get_changelog(&self) -> Result<serde_json::Value, Error> {
8277        self.request_json(Method::GET, &["changelog"], None::<&()>)
8278            .await
8279    }
8280
8281    /// Changes the user's password.
8282    ///
8283    /// # Errors
8284    ///
8285    /// Returns an error if:
8286    /// - Authentication fails
8287    /// - The HTTP request fails
8288    /// - The server rejects the password change
8289    /// - The response cannot be parsed
8290    pub async fn user_change_password(
8291        &self,
8292        password: Secret<String>,
8293    ) -> Result<ChangePasswordResponse, Error> {
8294        let request = ChangePasswordRequest {
8295            password: Secret::new(Password(password.expose_secret().clone())),
8296        };
8297        self.request_json(Method::POST, &["user", "change_password"], Some(request))
8298            .await
8299    }
8300
8301    /// Gets a list of all assets.
8302    ///
8303    /// # Errors
8304    ///
8305    /// Returns an error if:
8306    /// - Authentication fails
8307    /// - The HTTP request fails
8308    /// - The server returns an error status
8309    /// - The response cannot be parsed
8310    ///
8311    /// # Examples
8312    /// ```no_run
8313    /// # use amp_rs::ApiClient;
8314    /// # #[tokio::main]
8315    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
8316    /// let client = ApiClient::new().await?;
8317    ///
8318    /// let assets = client.get_assets().await?;
8319    /// for asset in assets {
8320    ///     println!("Asset: {} ({})", asset.name, asset.ticker.unwrap_or_default());
8321    /// }
8322    /// # Ok(())
8323    /// # }
8324    /// ```
8325    pub async fn get_assets(&self) -> Result<Vec<Asset>, Error> {
8326        self.request_json(Method::GET, &["assets"], None::<&()>)
8327            .await
8328    }
8329
8330    /// Gets a specific asset by UUID.
8331    ///
8332    /// # Errors
8333    ///
8334    /// Returns an error if:
8335    /// - Authentication fails
8336    /// - The HTTP request fails
8337    /// - The asset does not exist
8338    /// - The response cannot be parsed
8339    ///
8340    /// # Examples
8341    /// ```no_run
8342    /// # use amp_rs::ApiClient;
8343    /// # #[tokio::main]
8344    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
8345    /// let client = ApiClient::new().await?;
8346    ///
8347    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
8348    /// let asset = client.get_asset(asset_uuid).await?;
8349    ///
8350    /// println!("Asset: {} ({})", asset.name, asset.ticker.unwrap_or_default());
8351    /// println!("Registered: {}, Locked: {}", asset.is_registered, asset.is_locked);
8352    /// # Ok(())
8353    /// # }
8354    /// ```
8355    pub async fn get_asset(&self, asset_uuid: &str) -> Result<Asset, Error> {
8356        self.request_json(Method::GET, &["assets", asset_uuid], None::<&()>)
8357            .await
8358    }
8359
8360    /// Issues a new asset.
8361    ///
8362    /// # Errors
8363    ///
8364    /// Returns an error if:
8365    /// - Authentication fails
8366    /// - The HTTP request fails
8367    /// - The issuance request is invalid
8368    /// - The response cannot be parsed
8369    ///
8370    /// # Examples
8371    /// ```no_run
8372    /// # use amp_rs::{ApiClient, model::IssuanceRequest};
8373    /// # #[tokio::main]
8374    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
8375    /// let client = ApiClient::new().await?;
8376    ///
8377    /// let issuance_request = IssuanceRequest {
8378    ///     name: "My Token".to_string(),
8379    ///     amount: 1000000,
8380    ///     destination_address: "vjU2i2EM2viGEzSywpStMPkTX9U9QSDsLSN63kJJYVpxKJZuxaph8v5r5Jf11aqnfBVdjSbrvcJ2pw26".to_string(),
8381    ///     domain: "example.com".to_string(),
8382    ///     ticker: "MYTKN".to_string(),
8383    ///     pubkey: "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798".to_string(),
8384    ///     precision: Some(8),
8385    ///     is_confidential: Some(true),
8386    ///     is_reissuable: Some(false),
8387    ///     reissuance_amount: None,
8388    ///     reissuance_address: None,
8389    ///     transfer_restricted: Some(false),
8390    /// };
8391    ///
8392    /// let response = client.issue_asset(&issuance_request).await?;
8393    /// println!("Issued asset with UUID: {}", response.asset_uuid);
8394    /// # Ok(())
8395    /// # }
8396    /// ```
8397    pub async fn issue_asset(
8398        &self,
8399        issuance_request: &IssuanceRequest,
8400    ) -> Result<IssuanceResponse, Error> {
8401        self.request_json(Method::POST, &["assets", "issue"], Some(issuance_request))
8402            .await
8403    }
8404
8405    /// Edits an existing asset.
8406    ///
8407    /// # Errors
8408    ///
8409    /// Returns an error if:
8410    /// - Authentication fails
8411    /// - The HTTP request fails
8412    /// - The asset does not exist
8413    /// - The edit request is invalid
8414    /// - The response cannot be parsed
8415    pub async fn edit_asset(
8416        &self,
8417        asset_uuid: &str,
8418        edit_asset_request: &EditAssetRequest,
8419    ) -> Result<Asset, Error> {
8420        self.request_json(
8421            Method::PUT,
8422            &["assets", asset_uuid, "edit"],
8423            Some(edit_asset_request),
8424        )
8425        .await
8426    }
8427
8428    /// Registers an asset with the Blockstream Asset Registry.
8429    ///
8430    /// This method publishes an asset to the public registry, making it discoverable
8431    /// and verifiable by other users and applications. The asset must already exist
8432    /// in the AMP system before it can be registered.
8433    ///
8434    /// # Arguments
8435    ///
8436    /// * `asset_uuid` - The unique identifier of the asset to register
8437    ///
8438    /// # Returns
8439    ///
8440    /// Returns a `RegisterAssetResponse` containing:
8441    /// - `success`: Boolean indicating whether the registration was successful
8442    /// - `message`: Optional status message from the API
8443    /// - `asset_id`: The registered asset identifier (hex string)
8444    ///
8445    /// # Errors
8446    ///
8447    /// Returns an error if:
8448    /// - The asset does not exist or cannot be found (404)
8449    /// - Authentication fails or token is invalid (401)
8450    /// - The asset is already registered (returns success with appropriate message)
8451    /// - Network connectivity issues occur
8452    /// - The server returns an error status (5xx)
8453    /// - The response cannot be parsed
8454    ///
8455    /// # Examples
8456    ///
8457    /// ```no_run
8458    /// # use amp_rs::ApiClient;
8459    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
8460    /// let client = ApiClient::from_env().await?;
8461    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
8462    ///
8463    /// let response = client.register_asset(asset_uuid).await?;
8464    /// if response.success {
8465    ///     println!("Asset registered successfully!");
8466    ///     println!("Asset ID: {}", response.asset_id);
8467    ///     if let Some(message) = response.message {
8468    ///         println!("Message: {}", message);
8469    ///     }
8470    /// }
8471    /// # Ok(())
8472    /// # }
8473    /// ```
8474    pub async fn register_asset(&self, asset_uuid: &str) -> Result<RegisterAssetResponse, Error> {
8475        // Make HTTP request directly to handle both success and error responses
8476        let token = self.get_token().await?;
8477        let mut url = self.base_url.clone();
8478        url.path_segments_mut()
8479            .unwrap()
8480            .extend(&["assets", asset_uuid, "register"]);
8481
8482        let response = self
8483            .client
8484            .request(Method::GET, url)
8485            .header(AUTHORIZATION, format!("token {token}"))
8486            .timeout(std::time::Duration::from_secs(60))
8487            .send()
8488            .await
8489            .map_err(|e| Error::RequestFailed(format!("HTTP request failed: {e}")))?;
8490
8491        let status = response.status();
8492        let response_text = response.text().await.map_err(|e| {
8493            Error::ResponseParsingFailed(format!("Failed to read response body: {e}"))
8494        })?;
8495
8496        // Handle HTTP 200 - success case
8497        if status == reqwest::StatusCode::OK {
8498            // Try to parse as Asset (full registration response)
8499            if let Ok(asset) = serde_json::from_str::<Asset>(&response_text) {
8500                return Ok(RegisterAssetResponse {
8501                    success: true,
8502                    message: Some("Asset registered successfully".to_string()),
8503                    asset_data: Some(asset),
8504                });
8505            }
8506
8507            // If parsing as Asset fails, return success with raw message
8508            return Ok(RegisterAssetResponse {
8509                success: true,
8510                message: Some(response_text),
8511                asset_data: None,
8512            });
8513        }
8514
8515        // Handle error responses
8516        // Try to parse error response as JSON
8517        if let Ok(error_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
8518            // Check for "already registered" error
8519            if let Some(error_msg) = error_json.get("Error").and_then(|e| e.as_str()) {
8520                let error_msg_lower = error_msg.to_lowercase();
8521                if error_msg_lower.contains("already registered") {
8522                    return Ok(RegisterAssetResponse {
8523                        success: true,
8524                        message: Some("Asset is already registered".to_string()),
8525                        asset_data: None,
8526                    });
8527                }
8528
8529                // Other errors - return as error
8530                return Err(Error::RequestFailed(format!(
8531                    "Request to [\"assets\", \"{asset_uuid}\", \"register\"] failed with status {status}: {error_msg}"
8532                )));
8533            }
8534        }
8535
8536        // Fallback error for non-JSON or unexpected responses
8537        Err(Error::RequestFailed(format!(
8538            "Request to [\"assets\", \"{asset_uuid}\", \"register\"] failed with status {status}: {response_text}"
8539        )))
8540    }
8541
8542    /// # Errors
8543    /// Returns an error if:
8544    /// - The asset does not exist or cannot be found
8545    /// - Authentication fails or token is invalid
8546    /// - Network connectivity issues occur
8547    /// - The server returns an error status
8548    pub async fn delete_asset(&self, asset_uuid: &str) -> Result<(), Error> {
8549        self.request_empty(
8550            Method::DELETE,
8551            &["assets", asset_uuid, "delete"],
8552            None::<&()>,
8553        )
8554        .await
8555    }
8556
8557    /// # Errors
8558    /// Returns an error if:
8559    /// - The transaction ID is invalid or not found
8560    /// - Authentication fails or token is invalid
8561    /// - Network connectivity issues occur
8562    /// - The server returns an error status
8563    /// - The response cannot be parsed
8564    pub async fn get_broadcast_status(&self, txid: &str) -> Result<BroadcastResponse, Error> {
8565        self.request_json(Method::GET, &["tx", "broadcast", txid], None::<&()>)
8566            .await
8567    }
8568
8569    /// # Errors
8570    /// Returns an error if:
8571    /// - The transaction hex is invalid or malformed
8572    /// - The transaction is rejected by the network
8573    /// - Authentication fails or token is invalid
8574    /// - Network connectivity issues occur
8575    /// - The server returns an error status
8576    /// - The response cannot be parsed
8577    pub async fn broadcast_transaction(&self, tx_hex: &str) -> Result<BroadcastResponse, Error> {
8578        self.request_json(Method::POST, &["tx", "broadcast"], Some(tx_hex))
8579            .await
8580    }
8581
8582    /// # Errors
8583    /// Returns an error if:
8584    /// - The asset UUID is invalid or not found
8585    /// - The user lacks authorization to register the asset
8586    /// - The asset is already registered
8587    /// - Authentication fails or token is invalid
8588    /// - Network connectivity issues occur
8589    /// - The server returns an error status
8590    /// - The response cannot be parsed
8591    pub async fn register_asset_authorized(&self, asset_uuid: &str) -> Result<Asset, Error> {
8592        self.request_json(
8593            Method::GET,
8594            &["assets", asset_uuid, "register-authorized"],
8595            None::<&()>,
8596        )
8597        .await
8598    }
8599
8600    /// # Errors
8601    /// Returns an error if:
8602    /// - The asset UUID is invalid or not found
8603    /// - The asset is already locked
8604    /// - The user lacks permission to lock the asset
8605    /// - Authentication fails or token is invalid
8606    /// - Network connectivity issues occur
8607    /// - The server returns an error status
8608    /// - The response cannot be parsed
8609    pub async fn lock_asset(&self, asset_uuid: &str) -> Result<Asset, Error> {
8610        self.request_json(Method::PUT, &["assets", asset_uuid, "lock"], None::<&()>)
8611            .await
8612    }
8613
8614    /// # Errors
8615    /// Returns an error if:
8616    /// - The asset UUID is invalid or not found
8617    /// - The asset is not currently locked
8618    /// - The user lacks permission to unlock the asset
8619    /// - Authentication fails or token is invalid
8620    /// - Network connectivity issues occur
8621    /// - The server returns an error status
8622    /// - The response cannot be parsed
8623    pub async fn unlock_asset(&self, asset_uuid: &str) -> Result<Asset, Error> {
8624        self.request_json(Method::PUT, &["assets", asset_uuid, "unlock"], None::<&()>)
8625            .await
8626    }
8627
8628    /// # Errors
8629    /// Returns an error if:
8630    /// - The asset UUID is invalid or not found
8631    /// - The activity parameters are invalid
8632    /// - Authentication fails or token is invalid
8633    /// - Network connectivity issues occur
8634    /// - The server returns an error status
8635    /// - The response cannot be parsed
8636    pub async fn get_asset_activities(
8637        &self,
8638        asset_uuid: &str,
8639        params: &AssetActivityParams,
8640    ) -> Result<Vec<Activity>, Error> {
8641        self.request_json(
8642            Method::GET,
8643            &["assets", asset_uuid, "activities"],
8644            Some(params),
8645        )
8646        .await
8647    }
8648
8649    /// # Errors
8650    /// Returns an error if:
8651    /// - The asset UUID is invalid or not found
8652    /// - The specified height is invalid or out of range
8653    /// - Authentication fails or token is invalid
8654    /// - Network connectivity issues occur
8655    /// - The server returns an error status
8656    /// - The response cannot be parsed
8657    pub async fn get_asset_ownerships(
8658        &self,
8659        asset_uuid: &str,
8660        height: Option<i64>,
8661    ) -> Result<Vec<Ownership>, Error> {
8662        let mut path = vec!["assets", asset_uuid, "ownerships"];
8663        let height_str;
8664        if let Some(h) = height {
8665            height_str = h.to_string();
8666            path.push(&height_str);
8667        }
8668        self.request_json(Method::GET, &path, None::<&()>).await
8669    }
8670
8671    /// # Errors
8672    /// Returns an error if:
8673    /// - The asset UUID is invalid or not found
8674    /// - Authentication fails or token is invalid
8675    /// - Network connectivity issues occur
8676    /// - The server returns an error status
8677    /// - The response cannot be parsed
8678    pub async fn get_asset_balance(&self, asset_uuid: &str) -> Result<Balance, Error> {
8679        self.request_json(Method::GET, &["assets", asset_uuid, "balance"], None::<&()>)
8680            .await
8681    }
8682
8683    /// # Errors
8684    /// Returns an error if:
8685    /// - The asset UUID is invalid or not found
8686    /// - Authentication fails or token is invalid
8687    /// - Network connectivity issues occur
8688    /// - The server returns an error status
8689    /// - The response cannot be parsed
8690    pub async fn get_asset_summary(&self, asset_uuid: &str) -> Result<AssetSummary, Error> {
8691        self.request_json(Method::GET, &["assets", asset_uuid, "summary"], None::<&()>)
8692            .await
8693    }
8694
8695    /// # Errors
8696    /// Returns an error if:
8697    /// - The asset UUID is invalid or not found
8698    /// - Authentication fails or token is invalid
8699    /// - Network connectivity issues occur
8700    /// - The server returns an error status
8701    /// - The response cannot be parsed
8702    pub async fn get_asset_utxos(&self, asset_uuid: &str) -> Result<Vec<Utxo>, Error> {
8703        self.request_json(Method::GET, &["assets", asset_uuid, "utxos"], None::<&()>)
8704            .await
8705    }
8706
8707    /// Gets the memo for a specific asset.
8708    ///
8709    /// # Arguments
8710    /// * `asset_uuid` - The UUID of the asset to retrieve the memo for
8711    ///
8712    /// # Returns
8713    /// The memo string associated with the asset
8714    ///
8715    /// # Errors
8716    /// Returns an error if:
8717    /// - Authentication fails
8718    /// - The HTTP request fails
8719    /// - The server returns an error status
8720    /// - The asset does not exist
8721    /// - The response cannot be parsed
8722    pub async fn get_asset_memo(&self, asset_uuid: &str) -> Result<String, Error> {
8723        self.request_json(Method::GET, &["assets", asset_uuid, "memo"], None::<&()>)
8724            .await
8725    }
8726
8727    /// Sets a memo for the specified asset.
8728    ///
8729    /// # Arguments
8730    /// * `asset_uuid` - The UUID of the asset to set the memo for
8731    /// * `memo` - The memo string to associate with the asset
8732    ///
8733    /// # Returns
8734    /// Returns `Ok(())` on success.
8735    ///
8736    /// # Errors
8737    /// Returns an error if:
8738    /// - Authentication fails
8739    /// - The HTTP request fails
8740    /// - The server returns an error status
8741    /// - The asset does not exist
8742    /// - The memo cannot be set due to validation errors
8743    ///
8744    /// # Example
8745    /// ```rust
8746    /// # use amp_rs::ApiClient;
8747    /// # async fn example(client: &ApiClient) -> Result<(), Box<dyn std::error::Error>> {
8748    /// client.set_asset_memo("asset-uuid-123", "This is a memo for the asset").await?;
8749    /// # Ok(())
8750    /// # }
8751    /// ```
8752    pub async fn set_asset_memo(&self, asset_uuid: &str, memo: &str) -> Result<(), Error> {
8753        let token = self.get_token().await?;
8754        let mut url = self.base_url.clone();
8755        url.path_segments_mut()
8756            .unwrap()
8757            .extend(&["assets", asset_uuid, "memo", "set"]);
8758
8759        let response = self
8760            .client
8761            .request(Method::POST, url)
8762            .header(AUTHORIZATION, format!("token {token}"))
8763            .header("content-type", "application/json")
8764            .body(format!("\"{}\"", memo.replace('"', "\\\"")))
8765            .send()
8766            .await?;
8767
8768        if !response.status().is_success() {
8769            let status = response.status();
8770            let error_text = response
8771                .text()
8772                .await
8773                .unwrap_or_else(|_| "Unknown error".to_string());
8774            return Err(Error::RequestFailed(format!(
8775                "Request to [\"assets\", \"{asset_uuid}\", \"memo\", \"set\"] failed with status {status}: {error_text}"
8776            )));
8777        }
8778
8779        Ok(())
8780    }
8781
8782    /// Blacklists specific UTXOs for an asset to prevent them from being used in transactions.
8783    ///
8784    /// This method adds the specified UTXOs to the asset's blacklist, preventing them from being
8785    /// used in future transactions. This is typically used for security purposes when UTXOs are
8786    /// suspected to be compromised or need to be temporarily disabled.
8787    ///
8788    /// # Arguments
8789    /// * `asset_uuid` - The UUID of the asset to blacklist UTXOs for
8790    /// * `utxos` - A slice of `Outpoint` structs representing the UTXOs to blacklist
8791    ///
8792    /// # Returns
8793    /// Returns a vector of `Utxo` structs representing the blacklisted UTXOs with their updated status.
8794    ///
8795    /// # Errors
8796    /// Returns an error if:
8797    /// - Authentication fails or insufficient permissions
8798    /// - The asset UUID is invalid or does not exist
8799    /// - One or more UTXOs are invalid or already blacklisted
8800    /// - The HTTP request fails
8801    /// - The server returns an error status
8802    /// - The response cannot be parsed
8803    ///
8804    /// # Examples
8805    /// ```no_run
8806    /// # use amp_rs::{ApiClient, model::Outpoint};
8807    /// # #[tokio::main]
8808    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
8809    /// let client = ApiClient::new().await?;
8810    ///
8811    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
8812    /// let utxos = vec![
8813    ///     Outpoint {
8814    ///         txid: "abc123...".to_string(),
8815    ///         vout: 0,
8816    ///     },
8817    ///     Outpoint {
8818    ///         txid: "def456...".to_string(),
8819    ///         vout: 1,
8820    ///     },
8821    /// ];
8822    ///
8823    /// let blacklisted_utxos = client.blacklist_asset_utxos(asset_uuid, &utxos).await?;
8824    /// println!("Blacklisted {} UTXOs", blacklisted_utxos.len());
8825    /// # Ok(())
8826    /// # }
8827    /// ```
8828    ///
8829    /// # Related Methods
8830    /// - [`whitelist_asset_utxos`](Self::whitelist_asset_utxos) - Remove UTXOs from blacklist
8831    /// - [`get_asset`](Self::get_asset) - Get asset information including UTXO status
8832    pub async fn blacklist_asset_utxos(
8833        &self,
8834        asset_uuid: &str,
8835        utxos: &[Outpoint],
8836    ) -> Result<Vec<Utxo>, Error> {
8837        self.request_json(
8838            Method::POST,
8839            &["assets", asset_uuid, "utxos", "blacklist"],
8840            Some(utxos),
8841        )
8842        .await
8843    }
8844
8845    /// Removes UTXOs from the asset's blacklist, allowing them to be used in transactions again.
8846    ///
8847    /// This method removes the specified UTXOs from the asset's blacklist, restoring their ability
8848    /// to be used in transactions. This is the reverse operation of blacklisting UTXOs.
8849    ///
8850    /// # Arguments
8851    /// * `asset_uuid` - The UUID of the asset to whitelist UTXOs for
8852    /// * `utxos` - A slice of `Outpoint` structs representing the UTXOs to remove from blacklist
8853    ///
8854    /// # Returns
8855    /// Returns a vector of `Utxo` structs representing the whitelisted UTXOs with their updated status.
8856    ///
8857    /// # Errors
8858    /// Returns an error if:
8859    /// - Authentication fails or insufficient permissions
8860    /// - The asset UUID is invalid or does not exist
8861    /// - One or more UTXOs are invalid or not currently blacklisted
8862    /// - The HTTP request fails
8863    /// - The server returns an error status
8864    /// - The response cannot be parsed
8865    ///
8866    /// # Examples
8867    /// ```no_run
8868    /// # use amp_rs::{ApiClient, model::Outpoint};
8869    /// # #[tokio::main]
8870    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
8871    /// let client = ApiClient::new().await?;
8872    ///
8873    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
8874    /// let utxos = vec![
8875    ///     Outpoint {
8876    ///         txid: "abc123...".to_string(),
8877    ///         vout: 0,
8878    ///     },
8879    /// ];
8880    ///
8881    /// let whitelisted_utxos = client.whitelist_asset_utxos(asset_uuid, &utxos).await?;
8882    /// println!("Whitelisted {} UTXOs", whitelisted_utxos.len());
8883    /// # Ok(())
8884    /// # }
8885    /// ```
8886    ///
8887    /// # Related Methods
8888    /// - [`blacklist_asset_utxos`](Self::blacklist_asset_utxos) - Add UTXOs to blacklist
8889    /// - [`get_asset`](Self::get_asset) - Get asset information including UTXO status
8890    pub async fn whitelist_asset_utxos(
8891        &self,
8892        asset_uuid: &str,
8893        utxos: &[Outpoint],
8894    ) -> Result<Vec<Utxo>, Error> {
8895        self.request_json(
8896            Method::POST,
8897            &["assets", asset_uuid, "utxos", "whitelist"],
8898            Some(utxos),
8899        )
8900        .await
8901    }
8902
8903    /// Gets the treasury addresses for a specific asset
8904    ///
8905    /// # Arguments
8906    /// * `asset_uuid` - The UUID of the asset to get treasury addresses for
8907    ///
8908    /// # Returns
8909    /// A vector of treasury addresses as strings
8910    ///
8911    /// # Errors
8912    /// Returns an error if:
8913    /// - The asset does not exist
8914    /// - The request fails
8915    /// - The response cannot be parsed
8916    pub async fn get_asset_treasury_addresses(
8917        &self,
8918        asset_uuid: &str,
8919    ) -> Result<Vec<String>, Error> {
8920        self.request_json(
8921            Method::GET,
8922            &["assets", asset_uuid, "treasury-addresses"],
8923            None::<&()>,
8924        )
8925        .await
8926    }
8927
8928    /// Adds treasury addresses to a specific asset
8929    ///
8930    /// # Arguments
8931    /// * `asset_uuid` - The UUID of the asset to add treasury addresses to
8932    /// * `addresses` - A slice of address strings to add as treasury addresses
8933    ///
8934    /// # Returns
8935    /// Returns `Ok(())` on success
8936    ///
8937    /// # Errors
8938    /// Returns an error if:
8939    /// - The asset does not exist
8940    /// - The addresses are invalid
8941    /// - The request fails
8942    /// - Insufficient permissions
8943    pub async fn add_asset_treasury_addresses(
8944        &self,
8945        asset_uuid: &str,
8946        addresses: &[String],
8947    ) -> Result<(), Error> {
8948        self.request_empty(
8949            Method::POST,
8950            &["assets", asset_uuid, "treasury-addresses", "add"],
8951            Some(addresses),
8952        )
8953        .await
8954    }
8955
8956    /// Removes treasury addresses from a specific asset.
8957    ///
8958    /// This method removes the specified addresses from the asset's treasury address list.
8959    /// Treasury addresses are special addresses that can be used for asset management operations
8960    /// such as reissuance and burning.
8961    ///
8962    /// # Arguments
8963    /// * `asset_uuid` - The UUID of the asset to remove treasury addresses from
8964    /// * `addresses` - A slice of address strings to remove from the treasury addresses
8965    ///
8966    /// # Returns
8967    /// Returns `Ok(())` on successful removal.
8968    ///
8969    /// # Errors
8970    /// Returns an error if:
8971    /// - Authentication fails or insufficient permissions
8972    /// - The asset UUID is invalid or does not exist
8973    /// - One or more addresses are invalid or not currently treasury addresses
8974    /// - The HTTP request fails
8975    /// - The server returns an error status
8976    /// - Attempting to remove the last treasury address (if not allowed)
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 asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
8986    /// let addresses = vec![
8987    ///     "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh".to_string(),
8988    ///     "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4".to_string(),
8989    /// ];
8990    ///
8991    /// client.delete_asset_treasury_addresses(asset_uuid, &addresses).await?;
8992    /// println!("Removed {} treasury addresses", addresses.len());
8993    /// # Ok(())
8994    /// # }
8995    /// ```
8996    ///
8997    /// # Related Methods
8998    /// - [`add_asset_treasury_addresses`](Self::add_asset_treasury_addresses) - Add treasury addresses
8999    /// - [`get_asset_treasury_addresses`](Self::get_asset_treasury_addresses) - Get current treasury addresses
9000    /// - [`reissue_asset`](Self::reissue_asset) - Reissue assets using treasury addresses
9001    pub async fn delete_asset_treasury_addresses(
9002        &self,
9003        asset_uuid: &str,
9004        addresses: &[String],
9005    ) -> Result<(), Error> {
9006        self.request_empty(
9007            Method::DELETE,
9008            &["assets", asset_uuid, "treasury-addresses", "delete"],
9009            Some(addresses),
9010        )
9011        .await
9012    }
9013
9014    /// Gets a list of all registered users.
9015    ///
9016    /// # Errors
9017    /// Returns an error if:
9018    /// - Authentication fails
9019    /// - The HTTP request fails
9020    /// - The server returns an error status
9021    /// - The response cannot be parsed
9022    ///
9023    /// # Examples
9024    /// ```no_run
9025    /// # use amp_rs::ApiClient;
9026    /// # #[tokio::main]
9027    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9028    /// let client = ApiClient::new().await?;
9029    ///
9030    /// let users = client.get_registered_users().await?;
9031    /// for user in users {
9032    ///     println!("User: {} (ID: {})", user.name, user.id);
9033    /// }
9034    /// # Ok(())
9035    /// # }
9036    /// ```
9037    pub async fn get_registered_users(
9038        &self,
9039    ) -> Result<Vec<crate::model::RegisteredUserResponse>, Error> {
9040        self.request_json(Method::GET, &["registered_users"], None::<&()>)
9041            .await
9042    }
9043
9044    /// Gets a specific registered user by ID.
9045    ///
9046    /// # Arguments
9047    /// * `user_id` - The ID of the registered user to retrieve
9048    ///
9049    /// # Errors
9050    /// Returns an error if:
9051    /// - Authentication fails
9052    /// - The HTTP request fails
9053    /// - The user ID does not exist
9054    /// - The response cannot be parsed
9055    ///
9056    /// # Examples
9057    /// ```no_run
9058    /// # use amp_rs::ApiClient;
9059    /// # #[tokio::main]
9060    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9061    /// let client = ApiClient::new().await?;
9062    ///
9063    /// let user = client.get_registered_user(1).await?;
9064    /// println!("User: {} (ID: {})", user.name, user.id);
9065    /// # Ok(())
9066    /// # }
9067    /// ```
9068    pub async fn get_registered_user(
9069        &self,
9070        user_id: i64,
9071    ) -> Result<crate::model::RegisteredUserResponse, Error> {
9072        self.request_json(
9073            Method::GET,
9074            &["registered_users", &user_id.to_string()],
9075            None::<&()>,
9076        )
9077        .await
9078    }
9079
9080    /// Creates a new registered user in the AMP system.
9081    ///
9082    /// This method creates a new registered user with the provided information. Registered users
9083    /// can be associated with GAIDs, assigned to categories, and receive asset assignments.
9084    ///
9085    /// # Arguments
9086    /// * `new_user` - A `RegisteredUserAdd` struct containing the user information to create
9087    ///
9088    /// # Returns
9089    /// Returns a `RegisteredUserResponse` containing the created user's information including
9090    /// the assigned user ID.
9091    ///
9092    /// # Errors
9093    /// Returns an error if:
9094    /// - Authentication fails or insufficient permissions
9095    /// - The user data is invalid (e.g., missing required fields, invalid email format)
9096    /// - A user with the same identifier already exists
9097    /// - The HTTP request fails
9098    /// - The server returns an error status
9099    /// - The response cannot be parsed
9100    ///
9101    /// # Examples
9102    /// ```no_run
9103    /// # use amp_rs::{ApiClient, model::RegisteredUserAdd};
9104    /// # #[tokio::main]
9105    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9106    /// let client = ApiClient::new().await?;
9107    ///
9108    /// let new_user = RegisteredUserAdd {
9109    ///     name: "John Doe".to_string(),
9110    ///     gaid: Some("GAbYScu6jkWUND2jo3L4KJxyvo55d".to_string()),
9111    ///     is_company: false,
9112    /// };
9113    ///
9114    /// let created_user = client.add_registered_user(&new_user).await?;
9115    /// println!("Created user: {} with ID {}", created_user.name, created_user.id);
9116    /// # Ok(())
9117    /// # }
9118    /// ```
9119    ///
9120    /// # Related Methods
9121    /// - [`get_registered_users`](Self::get_registered_users) - List all registered users
9122    /// - [`edit_registered_user`](Self::edit_registered_user) - Update user information
9123    /// - [`delete_registered_user`](Self::delete_registered_user) - Remove a user
9124    pub async fn add_registered_user(
9125        &self,
9126        new_user: &crate::model::RegisteredUserAdd,
9127    ) -> Result<crate::model::RegisteredUserResponse, Error> {
9128        self.request_json(Method::POST, &["registered_users", "add"], Some(new_user))
9129            .await
9130    }
9131
9132    /// Removes a registered user from the AMP system.
9133    ///
9134    /// This method permanently deletes a registered user and all associated data. This operation
9135    /// cannot be undone. Any GAIDs associated with the user will be disassociated, and any
9136    /// pending assignments may be affected.
9137    ///
9138    /// # Arguments
9139    /// * `user_id` - The ID of the registered user to delete
9140    ///
9141    /// # Returns
9142    /// Returns `Ok(())` on successful deletion.
9143    ///
9144    /// # Errors
9145    /// Returns an error if:
9146    /// - Authentication fails or insufficient permissions
9147    /// - The user ID is invalid or does not exist
9148    /// - The user has active assignments that prevent deletion
9149    /// - The HTTP request fails
9150    /// - The server returns an error status
9151    ///
9152    /// # Examples
9153    /// ```no_run
9154    /// # use amp_rs::ApiClient;
9155    /// # #[tokio::main]
9156    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9157    /// let client = ApiClient::new().await?;
9158    ///
9159    /// let user_id = 123;
9160    /// client.delete_registered_user(user_id).await?;
9161    /// println!("Successfully deleted user with ID {}", user_id);
9162    /// # Ok(())
9163    /// # }
9164    /// ```
9165    ///
9166    /// # Related Methods
9167    /// - [`get_registered_user`](Self::get_registered_user) - Get user information before deletion
9168    /// - [`add_registered_user`](Self::add_registered_user) - Create a new user
9169    /// - [`get_registered_user_summary`](Self::get_registered_user_summary) - Check user's assignments
9170    pub async fn delete_registered_user(&self, user_id: i64) -> Result<(), Error> {
9171        self.request_empty(
9172            Method::DELETE,
9173            &["registered_users", &user_id.to_string(), "delete"],
9174            None::<&()>,
9175        )
9176        .await
9177    }
9178
9179    /// Updates registered user information.
9180    ///
9181    /// This method allows you to modify the information of an existing registered user.
9182    /// Only the fields provided in the edit data will be updated; other fields remain unchanged.
9183    ///
9184    /// # Arguments
9185    /// * `registered_user_id` - The ID of the registered user to update
9186    /// * `edit_data` - A `RegisteredUserEdit` struct containing the fields to update
9187    ///
9188    /// # Returns
9189    /// Returns a `RegisteredUserResponse` containing the updated user information.
9190    ///
9191    /// # Errors
9192    /// Returns an error if:
9193    /// - Authentication fails or insufficient permissions
9194    /// - The user ID is invalid or does not exist
9195    /// - The edit data contains invalid values (e.g., invalid email format)
9196    /// - The HTTP request fails
9197    /// - The server returns an error status
9198    /// - The response cannot be parsed
9199    ///
9200    /// # Examples
9201    /// ```no_run
9202    /// # use amp_rs::{ApiClient, model::RegisteredUserEdit};
9203    /// # #[tokio::main]
9204    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9205    /// let client = ApiClient::new().await?;
9206    ///
9207    /// let user_id = 123;
9208    /// let edit_data = RegisteredUserEdit {
9209    ///     name: Some("Jane Doe".to_string()),
9210    /// };
9211    ///
9212    /// let updated_user = client.edit_registered_user(user_id, &edit_data).await?;
9213    /// println!("Updated user: {}", updated_user.name);
9214    /// # Ok(())
9215    /// # }
9216    /// ```
9217    ///
9218    /// # Related Methods
9219    /// - [`get_registered_user`](Self::get_registered_user) - Get current user information
9220    /// - [`add_registered_user`](Self::add_registered_user) - Create a new user
9221    /// - [`delete_registered_user`](Self::delete_registered_user) - Remove a user
9222    pub async fn edit_registered_user(
9223        &self,
9224        registered_user_id: i64,
9225        edit_data: &crate::model::RegisteredUserEdit,
9226    ) -> Result<crate::model::RegisteredUserResponse, Error> {
9227        self.request_json(
9228            Method::PUT,
9229            &["registered_users", &registered_user_id.to_string(), "edit"],
9230            Some(edit_data),
9231        )
9232        .await
9233    }
9234
9235    /// Gets comprehensive summary information for a registered user including assets and distributions.
9236    ///
9237    /// This method retrieves detailed summary information about a registered user, including
9238    /// their basic information, associated assets, assignment history, and distribution records.
9239    /// This provides a complete overview of the user's activity and holdings in the system.
9240    ///
9241    /// # Arguments
9242    /// * `registered_user_id` - The ID of the registered user to get summary for
9243    ///
9244    /// # Returns
9245    /// Returns a `RegisteredUserSummary` containing:
9246    /// - Basic user information (name, email, etc.)
9247    /// - List of associated GAIDs
9248    /// - Asset assignments and their status
9249    /// - Distribution history
9250    /// - Balance information
9251    /// - Activity timestamps
9252    ///
9253    /// # Errors
9254    /// Returns an error if:
9255    /// - Authentication fails or insufficient permissions
9256    /// - The user ID is invalid or does not exist
9257    /// - The HTTP request fails
9258    /// - The server returns an error status
9259    /// - The response cannot be parsed
9260    ///
9261    /// # Examples
9262    /// ```no_run
9263    /// # use amp_rs::ApiClient;
9264    /// # #[tokio::main]
9265    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9266    /// let client = ApiClient::new().await?;
9267    ///
9268    /// let user_id = 123;
9269    /// let summary = client.get_registered_user_summary(user_id).await?;
9270    ///
9271    /// println!("Asset UUID: {}", summary.asset_uuid);
9272    /// println!("Asset ID: {}", summary.asset_id);
9273    /// println!("Asset assignments: {}", summary.assignments.len());
9274    /// println!("Distributions received: {}", summary.distributions.len());
9275    /// # Ok(())
9276    /// # }
9277    /// ```
9278    ///
9279    /// # Related Methods
9280    /// - [`get_registered_user`](Self::get_registered_user) - Get basic user information
9281    /// - [`get_registered_user_gaids`](Self::get_registered_user_gaids) - Get only GAIDs
9282    /// - [`get_asset_assignments`](Self::get_asset_assignments) - Get assignments for specific asset
9283    pub async fn get_registered_user_summary(
9284        &self,
9285        registered_user_id: i64,
9286    ) -> Result<crate::model::RegisteredUserSummary, Error> {
9287        self.request_json(
9288            Method::GET,
9289            &[
9290                "registered_users",
9291                &registered_user_id.to_string(),
9292                "summary",
9293            ],
9294            None::<&()>,
9295        )
9296        .await
9297    }
9298
9299    /// Gets all GAIDs (Green Address IDs) associated with a registered user.
9300    ///
9301    /// This method retrieves a list of all GAIDs that are currently associated with the specified
9302    /// registered user. GAIDs are unique identifiers that can be used to receive assets and
9303    /// track ownership.
9304    ///
9305    /// # Arguments
9306    /// * `registered_user_id` - The ID of the registered user to get GAIDs for
9307    ///
9308    /// # Returns
9309    /// Returns a vector of GAID strings associated with the user.
9310    ///
9311    /// # Errors
9312    /// Returns an error if:
9313    /// - Authentication fails or insufficient permissions
9314    /// - The user ID is invalid or does not exist
9315    /// - The HTTP request fails
9316    /// - The server returns an error status
9317    /// - The response cannot be parsed
9318    ///
9319    /// # Examples
9320    /// ```no_run
9321    /// # use amp_rs::ApiClient;
9322    /// # #[tokio::main]
9323    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9324    /// let client = ApiClient::new().await?;
9325    ///
9326    /// let user_id = 123;
9327    /// let gaids = client.get_registered_user_gaids(user_id).await?;
9328    ///
9329    /// println!("User {} has {} associated GAIDs:", user_id, gaids.len());
9330    /// for gaid in gaids {
9331    ///     println!("  - {}", gaid);
9332    /// }
9333    /// # Ok(())
9334    /// # }
9335    /// ```
9336    ///
9337    /// # Related Methods
9338    /// - [`add_gaid_to_registered_user`](Self::add_gaid_to_registered_user) - Associate a GAID with user
9339    /// - [`set_default_gaid_for_registered_user`](Self::set_default_gaid_for_registered_user) - Set default GAID
9340    /// - [`get_gaid_registered_user`](Self::get_gaid_registered_user) - Find user by GAID
9341    /// - [`validate_gaid`](Self::validate_gaid) - Validate GAID format
9342    pub async fn get_registered_user_gaids(
9343        &self,
9344        registered_user_id: i64,
9345    ) -> Result<Vec<String>, Error> {
9346        self.request_json(
9347            Method::GET,
9348            &["registered_users", &registered_user_id.to_string(), "gaids"],
9349            None::<&()>,
9350        )
9351        .await
9352    }
9353
9354    /// Associates a GAID with a registered user.
9355    ///
9356    /// # Arguments
9357    /// * `registered_user_id` - The ID of the registered user
9358    /// * `gaid` - The GAID to associate with the user
9359    ///
9360    /// # Errors
9361    ///
9362    /// Returns an error if:
9363    /// - Authentication fails
9364    /// - The HTTP request fails
9365    /// - The server returns an error status
9366    /// - The registered user ID is invalid
9367    /// - The GAID is invalid or already associated
9368    pub async fn add_gaid_to_registered_user(
9369        &self,
9370        registered_user_id: i64,
9371        gaid: &str,
9372    ) -> Result<(), Error> {
9373        // Send GAID as a plain string, not wrapped in an object
9374        self.request_empty(
9375            Method::POST,
9376            &[
9377                "registered_users",
9378                &registered_user_id.to_string(),
9379                "gaids",
9380                "add",
9381            ],
9382            Some(gaid),
9383        )
9384        .await
9385    }
9386
9387    /// Sets an existing GAID as the default for a registered user.
9388    ///
9389    /// This method allows you to designate a specific GAID as the primary/default
9390    /// GAID for a registered user. The GAID must already be associated with the user.
9391    ///
9392    /// # Arguments
9393    /// * `registered_user_id` - The ID of the registered user
9394    /// * `gaid` - The GAID to set as default
9395    ///
9396    /// # Returns
9397    /// Returns `Ok(())` if the operation is successful.
9398    ///
9399    /// # Errors
9400    /// Returns an error if:
9401    /// - Authentication fails
9402    /// - The HTTP request fails
9403    /// - The server returns an error status
9404    /// - The registered user ID is invalid
9405    /// - The GAID is not associated with the user
9406    pub async fn set_default_gaid_for_registered_user(
9407        &self,
9408        registered_user_id: i64,
9409        gaid: &str,
9410    ) -> Result<(), Error> {
9411        // Send GAID as a plain string, not wrapped in an object
9412        self.request_empty(
9413            Method::POST,
9414            &[
9415                "registered_users",
9416                &registered_user_id.to_string(),
9417                "gaids",
9418                "set-default",
9419            ],
9420            Some(gaid),
9421        )
9422        .await
9423    }
9424
9425    /// Retrieves the registered user associated with a GAID
9426    ///
9427    /// # Arguments
9428    /// * `gaid` - The GAID to look up
9429    ///
9430    /// # Returns
9431    /// Returns the registered user data if the GAID is associated with a user
9432    ///
9433    /// # Errors
9434    /// This function will return an error if:
9435    /// - The GAID has no associated user
9436    /// - The GAID is invalid
9437    /// - Network or authentication errors occur
9438    pub async fn get_gaid_registered_user(
9439        &self,
9440        gaid: &str,
9441    ) -> Result<crate::model::RegisteredUserResponse, Error> {
9442        self.request_json(
9443            Method::GET,
9444            &["gaids", gaid, "registered_user"],
9445            None::<&()>,
9446        )
9447        .await
9448    }
9449
9450    /// Gets the balance information for a specific GAID.
9451    ///
9452    /// This method retrieves all asset balances associated with the given GAID,
9453    /// including confirmed balances and any lost outputs.
9454    ///
9455    /// # Arguments
9456    /// * `gaid` - The GAID to query balance for
9457    ///
9458    /// # Returns
9459    /// Returns a `Balance` struct containing confirmed balances and lost outputs
9460    ///
9461    /// # Errors
9462    /// Returns an error if:
9463    /// - The GAID is invalid
9464    /// - Network or authentication errors occur
9465    /// - The response cannot be parsed
9466    ///
9467    /// # Examples
9468    /// ```no_run
9469    /// # use amp_rs::ApiClient;
9470    /// # #[tokio::main]
9471    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9472    /// let client = ApiClient::new().await?;
9473    ///
9474    /// let gaid = "GAbYScu6jkWUND2jo3L4KJxyvo55d";
9475    /// let balance = client.get_gaid_balance(gaid).await?;
9476    ///
9477    /// println!("GAID {} has {} balance entries", gaid, balance.len());
9478    /// for entry in balance {
9479    ///     println!("Asset {}: {} units", entry.asset_id, entry.balance);
9480    /// }
9481    /// # Ok(())
9482    /// # }
9483    /// ```
9484    pub async fn get_gaid_balance(&self, gaid: &str) -> Result<Balance, Error> {
9485        self.request_json(Method::GET, &["gaids", gaid, "balance"], None::<&()>)
9486            .await
9487    }
9488
9489    /// Retrieves the specific asset balance for a GAID
9490    ///
9491    /// # Arguments
9492    /// * `gaid` - The GAID to query
9493    /// * `asset_uuid` - The UUID of the asset to query
9494    ///
9495    /// # Returns
9496    /// Returns the specific asset balance information
9497    ///
9498    /// # Errors
9499    /// Returns an error if:
9500    /// - The GAID is invalid
9501    /// - The asset UUID is invalid
9502    /// - Network or authentication errors occur
9503    /// - The response cannot be parsed
9504    pub async fn get_gaid_asset_balance(
9505        &self,
9506        gaid: &str,
9507        asset_uuid: &str,
9508    ) -> Result<Ownership, Error> {
9509        // Try to get the response as a GaidBalanceEntry first, then convert to Ownership
9510        let balance_entry: GaidBalanceEntry = self
9511            .request_json(
9512                Method::GET,
9513                &["gaids", gaid, "balance", asset_uuid],
9514                None::<&()>,
9515            )
9516            .await?;
9517
9518        // Convert GaidBalanceEntry to Ownership format
9519        Ok(Ownership {
9520            owner: gaid.to_string(),
9521            amount: balance_entry.balance,
9522            gaid: Some(gaid.to_string()),
9523        })
9524    }
9525
9526    /// Gets a list of all categories.
9527    ///
9528    /// # Returns
9529    /// Returns a vector of `CategoryResponse` objects
9530    ///
9531    /// # Errors
9532    /// Returns an error if:
9533    /// - Authentication fails
9534    /// - The HTTP request fails
9535    /// - The response cannot be parsed
9536    ///
9537    /// # Examples
9538    /// ```no_run
9539    /// # use amp_rs::ApiClient;
9540    /// # #[tokio::main]
9541    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9542    /// let client = ApiClient::new().await?;
9543    ///
9544    /// let categories = client.get_categories().await?;
9545    /// for category in categories {
9546    ///     println!("Category: {} (ID: {})", category.name, category.id);
9547    ///     if let Some(desc) = category.description {
9548    ///         println!("  Description: {}", desc);
9549    ///     }
9550    /// }
9551    /// # Ok(())
9552    /// # }
9553    /// ```
9554    pub async fn get_categories(&self) -> Result<Vec<CategoryResponse>, Error> {
9555        self.request_json(Method::GET, &["categories"], None::<&()>)
9556            .await
9557    }
9558
9559    /// Creates a new category for organizing users and assets.
9560    ///
9561    /// This method creates a new category that can be used to group registered users and assets
9562    /// for organizational purposes. Categories help manage permissions and provide logical
9563    /// groupings for assets and users.
9564    ///
9565    /// # Arguments
9566    /// * `new_category` - A `CategoryAdd` struct containing the category information to create
9567    ///
9568    /// # Returns
9569    /// Returns a `CategoryResponse` containing the created category information including
9570    /// the assigned category ID.
9571    ///
9572    /// # Errors
9573    /// Returns an error if:
9574    /// - Authentication fails or insufficient permissions
9575    /// - The category data is invalid (e.g., missing name, invalid characters)
9576    /// - A category with the same name already exists
9577    /// - The HTTP request fails
9578    /// - The server returns an error status
9579    /// - The response cannot be parsed
9580    ///
9581    /// # Examples
9582    /// ```no_run
9583    /// # use amp_rs::{ApiClient, model::CategoryAdd};
9584    /// # #[tokio::main]
9585    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9586    /// let client = ApiClient::new().await?;
9587    ///
9588    /// let new_category = CategoryAdd {
9589    ///     name: "Premium Users".to_string(),
9590    ///     description: Some("High-value users with special privileges".to_string()),
9591    /// };
9592    ///
9593    /// let created_category = client.add_category(&new_category).await?;
9594    /// println!("Created category: {} with ID {}", created_category.name, created_category.id);
9595    /// # Ok(())
9596    /// # }
9597    /// ```
9598    ///
9599    /// # Related Methods
9600    /// - [`get_categories`](Self::get_categories) - List all categories
9601    /// - [`edit_category`](Self::edit_category) - Update category information
9602    /// - [`delete_category`](Self::delete_category) - Remove a category
9603    /// - [`add_registered_user_to_category`](Self::add_registered_user_to_category) - Add users to category
9604    pub async fn add_category(
9605        &self,
9606        new_category: &CategoryAdd,
9607    ) -> Result<CategoryResponse, Error> {
9608        self.request_json(Method::POST, &["categories", "add"], Some(new_category))
9609            .await
9610    }
9611
9612    /// Gets a specific category by ID.
9613    ///
9614    /// This method retrieves detailed information about a specific category, including
9615    /// its name, description, and associated users and assets.
9616    ///
9617    /// # Arguments
9618    /// * `category_id` - The ID of the category to retrieve
9619    ///
9620    /// # Returns
9621    /// Returns a `CategoryResponse` containing the category information including:
9622    /// - Category ID, name, and description
9623    /// - List of associated registered users
9624    /// - List of associated assets
9625    /// - Creation and modification timestamps
9626    ///
9627    /// # Errors
9628    /// Returns an error if:
9629    /// - Authentication fails or insufficient permissions
9630    /// - The category ID is invalid or does not exist
9631    /// - The HTTP request fails
9632    /// - The server returns an error status
9633    /// - The response cannot be parsed
9634    ///
9635    /// # Examples
9636    /// ```no_run
9637    /// # use amp_rs::ApiClient;
9638    /// # #[tokio::main]
9639    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9640    /// let client = ApiClient::new().await?;
9641    ///
9642    /// let category_id = 1;
9643    /// let category = client.get_category(category_id).await?;
9644    ///
9645    /// println!("Category: {} (ID: {})", category.name, category.id);
9646    /// if let Some(desc) = category.description {
9647    ///     println!("Description: {}", desc);
9648    /// }
9649    /// println!("Users: {}, Assets: {}", category.registered_users.len(), category.assets.len());
9650    /// # Ok(())
9651    /// # }
9652    /// ```
9653    ///
9654    /// # Related Methods
9655    /// - [`get_categories`](Self::get_categories) - List all categories
9656    /// - [`add_category`](Self::add_category) - Create a new category
9657    /// - [`edit_category`](Self::edit_category) - Update category information
9658    /// - [`delete_category`](Self::delete_category) - Remove a category
9659    pub async fn get_category(&self, category_id: i64) -> Result<CategoryResponse, Error> {
9660        self.request_json(
9661            Method::GET,
9662            &["categories", &category_id.to_string()],
9663            None::<&()>,
9664        )
9665        .await
9666    }
9667
9668    /// Updates category information.
9669    ///
9670    /// This method allows you to modify the information of an existing category.
9671    /// Only the fields provided in the edit data will be updated; other fields remain unchanged.
9672    ///
9673    /// # Arguments
9674    /// * `category_id` - The ID of the category to update
9675    /// * `edit_category` - A `CategoryEdit` struct containing the fields to update
9676    ///
9677    /// # Returns
9678    /// Returns a `CategoryResponse` containing the updated category information.
9679    ///
9680    /// # Errors
9681    /// Returns an error if:
9682    /// - Authentication fails or insufficient permissions
9683    /// - The category ID is invalid or does not exist
9684    /// - The edit data contains invalid values (e.g., empty name, invalid characters)
9685    /// - A category with the new name already exists (if name is being changed)
9686    /// - The HTTP request fails
9687    /// - The server returns an error status
9688    /// - The response cannot be parsed
9689    ///
9690    /// # Examples
9691    /// ```no_run
9692    /// # use amp_rs::{ApiClient, model::CategoryEdit};
9693    /// # #[tokio::main]
9694    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9695    /// let client = ApiClient::new().await?;
9696    ///
9697    /// let category_id = 1;
9698    /// let edit_data = CategoryEdit {
9699    ///     name: Some("VIP Users".to_string()),
9700    ///     description: Some("Very important users with premium access".to_string()),
9701    /// };
9702    ///
9703    /// let updated_category = client.edit_category(category_id, &edit_data).await?;
9704    /// println!("Updated category: {}", updated_category.name);
9705    /// # Ok(())
9706    /// # }
9707    /// ```
9708    ///
9709    /// # Related Methods
9710    /// - [`get_category`](Self::get_category) - Get current category information
9711    /// - [`add_category`](Self::add_category) - Create a new category
9712    /// - [`delete_category`](Self::delete_category) - Remove a category
9713    pub async fn edit_category(
9714        &self,
9715        category_id: i64,
9716        edit_category: &CategoryEdit,
9717    ) -> Result<CategoryResponse, Error> {
9718        self.request_json(
9719            Method::PUT,
9720            &["categories", &category_id.to_string(), "edit"],
9721            Some(edit_category),
9722        )
9723        .await
9724    }
9725
9726    /// Removes a category from the system.
9727    ///
9728    /// This method permanently deletes a category. All users and assets associated with the
9729    /// category will be disassociated, but the users and assets themselves are not deleted.
9730    /// This operation cannot be undone.
9731    ///
9732    /// # Arguments
9733    /// * `category_id` - The ID of the category to delete
9734    ///
9735    /// # Returns
9736    /// Returns `Ok(())` on successful deletion.
9737    ///
9738    /// # Errors
9739    /// Returns an error if:
9740    /// - Authentication fails or insufficient permissions
9741    /// - The category ID is invalid or does not exist
9742    /// - The category is still in use and cannot be deleted (depending on system configuration)
9743    /// - The HTTP request fails
9744    /// - The server returns an error status
9745    ///
9746    /// # Examples
9747    /// ```no_run
9748    /// # use amp_rs::ApiClient;
9749    /// # #[tokio::main]
9750    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9751    /// let client = ApiClient::new().await?;
9752    ///
9753    /// let category_id = 1;
9754    /// client.delete_category(category_id).await?;
9755    /// println!("Successfully deleted category with ID {}", category_id);
9756    /// # Ok(())
9757    /// # }
9758    /// ```
9759    ///
9760    /// # Related Methods
9761    /// - [`get_category`](Self::get_category) - Get category information before deletion
9762    /// - [`add_category`](Self::add_category) - Create a new category
9763    /// - [`remove_registered_user_from_category`](Self::remove_registered_user_from_category) - Remove users first
9764    /// - [`remove_asset_from_category`](Self::remove_asset_from_category) - Remove assets first
9765    pub async fn delete_category(&self, category_id: i64) -> Result<(), Error> {
9766        self.request_empty(
9767            Method::DELETE,
9768            &["categories", &category_id.to_string(), "delete"],
9769            None::<&()>,
9770        )
9771        .await
9772    }
9773
9774    /// Associates a registered user with a category.
9775    ///
9776    /// This method adds a registered user to a category, allowing for organized grouping
9777    /// of users. Users can belong to multiple categories, and categories can contain
9778    /// multiple users.
9779    ///
9780    /// # Arguments
9781    /// * `category_id` - The ID of the category to add the user to
9782    /// * `user_id` - The ID of the registered user to add to the category
9783    ///
9784    /// # Returns
9785    /// Returns a `CategoryResponse` containing the updated category information including
9786    /// the newly added user.
9787    ///
9788    /// # Errors
9789    /// Returns an error if:
9790    /// - Authentication fails or insufficient permissions
9791    /// - The category ID is invalid or does not exist
9792    /// - The user ID is invalid or does not exist
9793    /// - The user is already associated with the category
9794    /// - The HTTP request fails
9795    /// - The server returns an error status
9796    /// - The response cannot be parsed
9797    ///
9798    /// # Examples
9799    /// ```no_run
9800    /// # use amp_rs::ApiClient;
9801    /// # #[tokio::main]
9802    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9803    /// let client = ApiClient::new().await?;
9804    ///
9805    /// let category_id = 1;
9806    /// let user_id = 123;
9807    ///
9808    /// let updated_category = client.add_registered_user_to_category(category_id, user_id).await?;
9809    /// println!("Added user {} to category '{}'", user_id, updated_category.name);
9810    /// println!("Category now has {} users", updated_category.registered_users.len());
9811    /// # Ok(())
9812    /// # }
9813    /// ```
9814    ///
9815    /// # Related Methods
9816    /// - [`remove_registered_user_from_category`](Self::remove_registered_user_from_category) - Remove user from category
9817    /// - [`get_category`](Self::get_category) - Get category information including users
9818    /// - [`get_registered_user`](Self::get_registered_user) - Get user information
9819    pub async fn add_registered_user_to_category(
9820        &self,
9821        category_id: i64,
9822        user_id: i64,
9823    ) -> Result<CategoryResponse, Error> {
9824        self.request_json(
9825            Method::PUT,
9826            &[
9827                "categories",
9828                &category_id.to_string(),
9829                "registered_users",
9830                &user_id.to_string(),
9831                "add",
9832            ],
9833            None::<&()>,
9834        )
9835        .await
9836    }
9837
9838    /// Removes a registered user from a category.
9839    ///
9840    /// This method disassociates a registered user from a category. The user remains in the
9841    /// system but is no longer part of the specified category. This does not affect the user's
9842    /// association with other categories.
9843    ///
9844    /// # Arguments
9845    /// * `category_id` - The ID of the category to remove the user from
9846    /// * `user_id` - The ID of the registered user to remove from the category
9847    ///
9848    /// # Returns
9849    /// Returns a `CategoryResponse` containing the updated category information without
9850    /// the removed user.
9851    ///
9852    /// # Errors
9853    /// Returns an error if:
9854    /// - Authentication fails or insufficient permissions
9855    /// - The category ID is invalid or does not exist
9856    /// - The user ID is invalid or does not exist
9857    /// - The user is not currently associated with the category
9858    /// - The HTTP request fails
9859    /// - The server returns an error status
9860    /// - The response cannot be parsed
9861    ///
9862    /// # Examples
9863    /// ```no_run
9864    /// # use amp_rs::ApiClient;
9865    /// # #[tokio::main]
9866    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9867    /// let client = ApiClient::new().await?;
9868    ///
9869    /// let category_id = 1;
9870    /// let user_id = 123;
9871    ///
9872    /// let updated_category = client.remove_registered_user_from_category(category_id, user_id).await?;
9873    /// println!("Removed user {} from category '{}'", user_id, updated_category.name);
9874    /// println!("Category now has {} users", updated_category.registered_users.len());
9875    /// # Ok(())
9876    /// # }
9877    /// ```
9878    ///
9879    /// # Related Methods
9880    /// - [`add_registered_user_to_category`](Self::add_registered_user_to_category) - Add user to category
9881    /// - [`get_category`](Self::get_category) - Get category information including users
9882    /// - [`get_registered_user`](Self::get_registered_user) - Get user information
9883    pub async fn remove_registered_user_from_category(
9884        &self,
9885        category_id: i64,
9886        user_id: i64,
9887    ) -> Result<CategoryResponse, Error> {
9888        self.request_json(
9889            Method::PUT,
9890            &[
9891                "categories",
9892                &category_id.to_string(),
9893                "registered_users",
9894                &user_id.to_string(),
9895                "remove",
9896            ],
9897            None::<&()>,
9898        )
9899        .await
9900    }
9901
9902    /// Associates an asset with a category.
9903    ///
9904    /// This method adds an asset to a category, allowing for organized grouping of assets.
9905    /// Assets can belong to multiple categories, and categories can contain multiple assets.
9906    /// This helps with asset management and permission organization.
9907    ///
9908    /// # Arguments
9909    /// * `category_id` - The ID of the category to add the asset to
9910    /// * `asset_uuid` - The UUID of the asset to add to the category
9911    ///
9912    /// # Returns
9913    /// Returns a `CategoryResponse` containing the updated category information including
9914    /// the newly added asset.
9915    ///
9916    /// # Errors
9917    /// Returns an error if:
9918    /// - Authentication fails or insufficient permissions
9919    /// - The category ID is invalid or does not exist
9920    /// - The asset UUID is invalid or does not exist
9921    /// - The asset is already associated with the category
9922    /// - The HTTP request fails
9923    /// - The server returns an error status
9924    /// - The response cannot be parsed
9925    ///
9926    /// # Examples
9927    /// ```no_run
9928    /// # use amp_rs::ApiClient;
9929    /// # #[tokio::main]
9930    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9931    /// let client = ApiClient::new().await?;
9932    ///
9933    /// let category_id = 1;
9934    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
9935    ///
9936    /// let updated_category = client.add_asset_to_category(category_id, asset_uuid).await?;
9937    /// println!("Added asset {} to category '{}'", asset_uuid, updated_category.name);
9938    /// println!("Category now has {} assets", updated_category.assets.len());
9939    /// # Ok(())
9940    /// # }
9941    /// ```
9942    ///
9943    /// # Related Methods
9944    /// - [`remove_asset_from_category`](Self::remove_asset_from_category) - Remove asset from category
9945    /// - [`get_category`](Self::get_category) - Get category information including assets
9946    /// - [`get_asset`](Self::get_asset) - Get asset information
9947    pub async fn add_asset_to_category(
9948        &self,
9949        category_id: i64,
9950        asset_uuid: &str,
9951    ) -> Result<CategoryResponse, Error> {
9952        self.request_json(
9953            Method::PUT,
9954            &[
9955                "categories",
9956                &category_id.to_string(),
9957                "assets",
9958                asset_uuid,
9959                "add",
9960            ],
9961            None::<&()>,
9962        )
9963        .await
9964    }
9965
9966    /// Removes an asset from a category.
9967    ///
9968    /// This method disassociates an asset from a category. The asset remains in the system
9969    /// but is no longer part of the specified category. This does not affect the asset's
9970    /// association with other categories.
9971    ///
9972    /// # Arguments
9973    /// * `category_id` - The ID of the category to remove the asset from
9974    /// * `asset_uuid` - The UUID of the asset to remove from the category
9975    ///
9976    /// # Returns
9977    /// Returns a `CategoryResponse` containing the updated category information without
9978    /// the removed asset.
9979    ///
9980    /// # Errors
9981    /// Returns an error if:
9982    /// - Authentication fails or insufficient permissions
9983    /// - The category ID is invalid or does not exist
9984    /// - The asset UUID is invalid or does not exist
9985    /// - The asset is not currently associated with the category
9986    /// - The HTTP request fails
9987    /// - The server returns an error status
9988    /// - The response cannot be parsed
9989    ///
9990    /// # Examples
9991    /// ```no_run
9992    /// # use amp_rs::ApiClient;
9993    /// # #[tokio::main]
9994    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9995    /// let client = ApiClient::new().await?;
9996    ///
9997    /// let category_id = 1;
9998    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
9999    ///
10000    /// let updated_category = client.remove_asset_from_category(category_id, asset_uuid).await?;
10001    /// println!("Removed asset {} from category '{}'", asset_uuid, updated_category.name);
10002    /// println!("Category now has {} assets", updated_category.assets.len());
10003    /// # Ok(())
10004    /// # }
10005    /// ```
10006    ///
10007    /// # Related Methods
10008    /// - [`add_asset_to_category`](Self::add_asset_to_category) - Add asset to category
10009    /// - [`get_category`](Self::get_category) - Get category information including assets
10010    /// - [`get_asset`](Self::get_asset) - Get asset information
10011    pub async fn remove_asset_from_category(
10012        &self,
10013        category_id: i64,
10014        asset_uuid: &str,
10015    ) -> Result<CategoryResponse, Error> {
10016        self.request_json(
10017            Method::PUT,
10018            &[
10019                "categories",
10020                &category_id.to_string(),
10021                "assets",
10022                asset_uuid,
10023                "remove",
10024            ],
10025            None::<&()>,
10026        )
10027        .await
10028    }
10029
10030    /// Validates a GAID (Green Address ID).
10031    ///
10032    /// # Arguments
10033    /// * `gaid` - The GAID string to validate
10034    ///
10035    /// # Returns
10036    /// Returns a `ValidateGaidResponse` indicating whether the GAID is valid
10037    ///
10038    /// # Errors
10039    /// Returns an error if:
10040    /// - Authentication fails
10041    /// - The HTTP request fails
10042    /// - The response cannot be parsed
10043    ///
10044    /// # Examples
10045    /// ```no_run
10046    /// # use amp_rs::ApiClient;
10047    /// # #[tokio::main]
10048    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
10049    /// let client = ApiClient::new().await?;
10050    ///
10051    /// let gaid = "GAbYScu6jkWUND2jo3L4KJxyvo55d";
10052    /// let validation = client.validate_gaid(gaid).await?;
10053    ///
10054    /// if validation.is_valid {
10055    ///     println!("GAID {} is valid", gaid);
10056    /// } else {
10057    ///     println!("GAID {} is invalid: {:?}", gaid, validation.error);
10058    /// }
10059    /// # Ok(())
10060    /// # }
10061    /// ```
10062    pub async fn validate_gaid(
10063        &self,
10064        gaid: &str,
10065    ) -> Result<crate::model::ValidateGaidResponse, Error> {
10066        self.request_json(Method::GET, &["gaids", gaid, "validate"], None::<&()>)
10067            .await
10068    }
10069
10070    /// Gets the address associated with a GAID.
10071    ///
10072    /// # Arguments
10073    /// * `gaid` - The GAID to get the address for
10074    ///
10075    /// # Returns
10076    /// Returns an `AddressGaidResponse` containing the address
10077    ///
10078    /// # Errors
10079    /// Returns an error if:
10080    /// - The GAID is invalid
10081    /// - Authentication fails
10082    /// - The HTTP request fails
10083    /// - The response cannot be parsed
10084    ///
10085    /// # Examples
10086    /// ```no_run
10087    /// # use amp_rs::ApiClient;
10088    /// # #[tokio::main]
10089    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
10090    /// let client = ApiClient::new().await?;
10091    ///
10092    /// let gaid = "GAbYScu6jkWUND2jo3L4KJxyvo55d";
10093    /// let address_response = client.get_gaid_address(gaid).await?;
10094    ///
10095    /// println!("Address for GAID {}: {}", gaid, address_response.address);
10096    /// # Ok(())
10097    /// # }
10098    /// ```
10099    pub async fn get_gaid_address(
10100        &self,
10101        gaid: &str,
10102    ) -> Result<crate::model::AddressGaidResponse, Error> {
10103        self.request_json(Method::GET, &["gaids", gaid, "address"], None::<&()>)
10104            .await
10105    }
10106
10107    /// Gets a list of all managers.
10108    ///
10109    /// # Returns
10110    /// Returns a vector of `Manager` objects
10111    ///
10112    /// # Errors
10113    /// Returns an error if:
10114    /// - Authentication fails
10115    /// - The HTTP request fails
10116    /// - The response cannot be parsed
10117    ///
10118    /// # Examples
10119    /// ```no_run
10120    /// # use amp_rs::ApiClient;
10121    /// # #[tokio::main]
10122    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
10123    /// let client = ApiClient::new().await?;
10124    ///
10125    /// let managers = client.get_managers().await?;
10126    /// for manager in managers {
10127    ///     println!("Manager: {} (ID: {})", manager.username, manager.id);
10128    /// }
10129    /// # Ok(())
10130    /// # }
10131    /// ```
10132    pub async fn get_managers(&self) -> Result<Vec<crate::model::Manager>, Error> {
10133        self.request_json(Method::GET, &["managers"], None::<&()>)
10134            .await
10135    }
10136
10137    /// Creates a new manager.
10138    ///
10139    /// # Arguments
10140    /// * `new_manager` - The manager creation request containing username and password
10141    ///
10142    /// # Returns
10143    /// Returns the created `Manager` object
10144    ///
10145    /// # Errors
10146    /// Returns an error if:
10147    /// - Authentication fails
10148    /// - The HTTP request fails
10149    /// - The manager creation request is invalid
10150    /// - The response cannot be parsed
10151    ///
10152    /// # Examples
10153    /// ```no_run
10154    /// # use amp_rs::{ApiClient, model::ManagerCreate};
10155    /// # #[tokio::main]
10156    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
10157    /// let client = ApiClient::new().await?;
10158    ///
10159    /// let new_manager = ManagerCreate {
10160    ///     username: "new_manager".to_string(),
10161    ///     password: "secure_password".to_string(),
10162    /// };
10163    ///
10164    /// let manager = client.create_manager(&new_manager).await?;
10165    /// println!("Created manager: {} (ID: {})", manager.username, manager.id);
10166    /// # Ok(())
10167    /// # }
10168    /// ```
10169    pub async fn create_manager(
10170        &self,
10171        new_manager: &crate::model::ManagerCreate,
10172    ) -> Result<crate::model::Manager, Error> {
10173        self.request_json(Method::POST, &["managers", "create"], Some(new_manager))
10174            .await
10175    }
10176
10177    /// Gets all assignments for a specific asset.
10178    ///
10179    /// # Arguments
10180    /// * `asset_uuid` - The UUID of the asset to get assignments for
10181    ///
10182    /// # Returns
10183    /// Returns a vector of `Assignment` objects
10184    ///
10185    /// # Errors
10186    /// Returns an error if:
10187    /// - Authentication fails
10188    /// - The HTTP request fails
10189    /// - The asset UUID is invalid
10190    /// - The response cannot be parsed
10191    ///
10192    /// # Examples
10193    /// ```no_run
10194    /// # use amp_rs::ApiClient;
10195    /// # #[tokio::main]
10196    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
10197    /// let client = ApiClient::new().await?;
10198    ///
10199    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
10200    /// let assignments = client.get_asset_assignments(asset_uuid).await?;
10201    ///
10202    /// for assignment in assignments {
10203    ///     println!("Assignment ID: {}, Amount: {}", assignment.id, assignment.amount);
10204    /// }
10205    /// # Ok(())
10206    /// # }
10207    /// ```
10208    pub async fn get_asset_assignments(&self, asset_uuid: &str) -> Result<Vec<Assignment>, Error> {
10209        self.request_json(
10210            Method::GET,
10211            &["assets", asset_uuid, "assignments"],
10212            None::<&()>,
10213        )
10214        .await
10215    }
10216
10217    /// Creates multiple asset assignments in batch.
10218    ///
10219    /// This method creates multiple asset assignments for the specified asset. Each assignment
10220    /// allocates a specific amount of the asset to a registered user. The assignments are
10221    /// created individually due to API limitations, but this method handles the batch processing
10222    /// automatically.
10223    ///
10224    /// # Arguments
10225    /// * `asset_uuid` - The UUID of the asset to create assignments for
10226    /// * `requests` - A slice of `CreateAssetAssignmentRequest` structs containing assignment details
10227    ///
10228    /// # Returns
10229    /// Returns a vector of `Assignment` structs representing the created assignments with their
10230    /// assigned IDs and status information.
10231    ///
10232    /// # Errors
10233    /// Returns an error if:
10234    /// - Authentication fails or insufficient permissions
10235    /// - The asset UUID is invalid or does not exist
10236    /// - Any assignment request contains invalid data (e.g., invalid user ID, negative amount)
10237    /// - Insufficient asset balance for the total requested assignments
10238    /// - Any individual assignment creation fails
10239    /// - The HTTP request fails
10240    /// - The server returns an error status
10241    /// - The response cannot be parsed
10242    ///
10243    /// # Examples
10244    /// ```no_run
10245    /// # use amp_rs::{ApiClient, model::CreateAssetAssignmentRequest};
10246    /// # #[tokio::main]
10247    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
10248    /// let client = ApiClient::new().await?;
10249    ///
10250    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
10251    /// let requests = vec![
10252    ///     CreateAssetAssignmentRequest {
10253    ///         registered_user: 123,
10254    ///         amount: 1000,
10255    ///         vesting_timestamp: None,
10256    ///         ready_for_distribution: false,
10257    ///     },
10258    ///     CreateAssetAssignmentRequest {
10259    ///         registered_user: 456,
10260    ///         amount: 500,
10261    ///         vesting_timestamp: None,
10262    ///         ready_for_distribution: true,
10263    ///     },
10264    /// ];
10265    ///
10266    /// let assignments = client.create_asset_assignments(asset_uuid, &requests).await?;
10267    /// println!("Created {} assignments", assignments.len());
10268    /// for assignment in assignments {
10269    ///     println!("Assignment {}: {} units to user {}",
10270    ///              assignment.id, assignment.amount, assignment.registered_user);
10271    /// }
10272    /// # Ok(())
10273    /// # }
10274    /// ```
10275    ///
10276    /// # Related Methods
10277    /// - [`get_asset_assignments`](Self::get_asset_assignments) - List all assignments for an asset
10278    /// - [`delete_asset_assignment`](Self::delete_asset_assignment) - Remove an assignment
10279    /// - [`edit_asset_assignment`](Self::edit_asset_assignment) - Update assignment details
10280    /// - [`set_assignment_ready_for_distribution`](Self::set_assignment_ready_for_distribution) - Mark for distribution
10281    pub async fn create_asset_assignments(
10282        &self,
10283        asset_uuid: &str,
10284        requests: &[CreateAssetAssignmentRequest],
10285    ) -> Result<Vec<Assignment>, Error> {
10286        use crate::model::CreateAssetAssignmentRequestWrapper;
10287
10288        // The API only supports maximum length 1 per request, so we need to break
10289        // multiple assignments into separate CreateAssetAssignmentRequestWrapper instances
10290        let mut all_assignments = Vec::new();
10291
10292        for request in requests {
10293            let wrapper = CreateAssetAssignmentRequestWrapper {
10294                assignments: vec![request.clone()],
10295            };
10296
10297            let assignments: Vec<Assignment> = self
10298                .request_json(
10299                    Method::POST,
10300                    &["assets", asset_uuid, "assignments", "create"],
10301                    Some(&wrapper),
10302                )
10303                .await?;
10304
10305            all_assignments.extend(assignments);
10306        }
10307
10308        Ok(all_assignments)
10309    }
10310
10311    /// Gets a specific asset assignment by asset UUID and assignment ID.
10312    ///
10313    /// This method sends a GET request to retrieve detailed information about a specific asset
10314    /// assignment. Asset assignments represent the allocation of assets to users or entities,
10315    /// including information such as the assigned amount, recipient details, and assignment status.
10316    ///
10317    /// # Arguments
10318    /// * `asset_uuid` - The UUID of the asset for which to retrieve the assignment
10319    /// * `assignment_id` - The ID of the specific assignment to retrieve
10320    ///
10321    /// # Returns
10322    /// Returns an `Assignment` struct containing the assignment details including:
10323    /// - Assignment ID and amount
10324    /// - Recipient information
10325    /// - Assignment status and metadata
10326    /// - Creation and modification timestamps
10327    ///
10328    /// # Errors
10329    /// Returns an error if:
10330    /// - Authentication fails
10331    /// - The HTTP request fails
10332    /// - The server returns an error status
10333    /// - The asset UUID is invalid or does not exist
10334    /// - The assignment ID is invalid or does not exist
10335    /// - The assignment is not accessible to the current user
10336    /// - The response cannot be parsed as a valid Assignment
10337    ///
10338    /// # Example
10339    /// ```no_run
10340    /// # use amp_rs::ApiClient;
10341    /// # #[tokio::main]
10342    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
10343    /// let client = ApiClient::new().await?;
10344    ///
10345    /// // Retrieve assignment with ID "123" for asset "550e8400-e29b-41d4-a716-446655440000"
10346    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
10347    /// let assignment_id = "123";
10348    ///
10349    /// let assignment = client.get_asset_assignment(asset_uuid, assignment_id).await?;
10350    ///
10351    /// println!("Assignment ID: {}", assignment.id);
10352    /// println!("Assigned amount: {}", assignment.amount);
10353    /// println!("Registered user: {}", assignment.registered_user);
10354    /// # Ok(())
10355    /// # }
10356    /// ```
10357    pub async fn get_asset_assignment(
10358        &self,
10359        asset_uuid: &str,
10360        assignment_id: &str,
10361    ) -> Result<Assignment, Error> {
10362        self.request_json(
10363            Method::GET,
10364            &["assets", asset_uuid, "assignments", assignment_id],
10365            None::<&()>,
10366        )
10367        .await
10368    }
10369
10370    /// Creates a distribution for an asset with the specified assignments.
10371    ///
10372    /// This method initiates the distribution creation process by sending assignment details
10373    /// to the AMP API. The API will return a distribution UUID and address mappings that
10374    /// can be used for subsequent transaction creation and confirmation steps.
10375    ///
10376    /// # Arguments
10377    /// * `asset_uuid` - The UUID of the asset to distribute
10378    /// * `assignments` - A vector of `AssetDistributionAssignment` structs containing user IDs, addresses, and amounts
10379    ///
10380    /// # Returns
10381    /// Returns a `DistributionResponse` containing:
10382    /// - `distribution_uuid` - Unique identifier for the created distribution
10383    /// - `map_address_amount` - Mapping of addresses to amounts to be distributed
10384    /// - `map_address_asset` - Mapping of addresses to asset IDs
10385    /// - `asset_id` - The asset ID for the distribution
10386    ///
10387    /// # Errors
10388    /// Returns an `AmpError` if:
10389    /// - Authentication fails or insufficient permissions
10390    /// - The asset UUID is invalid or does not exist
10391    /// - Assignment data is invalid (e.g., invalid user IDs, negative amounts, invalid addresses)
10392    /// - Insufficient asset balance for the requested distribution
10393    /// - The HTTP request fails
10394    /// - The server returns an error status
10395    /// - The response cannot be parsed
10396    ///
10397    /// # Examples
10398    /// ```no_run
10399    /// # use amp_rs::{ApiClient, model::AssetDistributionAssignment, AmpError};
10400    /// # #[tokio::main]
10401    /// # async fn main() -> Result<(), AmpError> {
10402    /// let client = ApiClient::new().await.map_err(AmpError::from)?;
10403    ///
10404    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
10405    /// let assignments = vec![
10406    ///     AssetDistributionAssignment {
10407    ///         user_id: "user123".to_string(),
10408    ///         address: "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
10409    ///         amount: 100.0,
10410    ///     },
10411    ///     AssetDistributionAssignment {
10412    ///         user_id: "user456".to_string(),
10413    ///         address: "lq1qq3xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
10414    ///         amount: 50.0,
10415    ///     },
10416    /// ];
10417    ///
10418    /// let distribution_response = client.create_distribution(asset_uuid, assignments).await?;
10419    /// println!("Created distribution: {}", distribution_response.distribution_uuid);
10420    /// println!("Asset ID: {}", distribution_response.asset_id);
10421    /// # Ok(())
10422    /// # }
10423    /// ```
10424    ///
10425    /// # Related Methods
10426    /// - [`get_asset_assignments`](Self::get_asset_assignments) - List assignments for an asset
10427    /// - [`create_asset_assignments`](Self::create_asset_assignments) - Create new assignments
10428    #[allow(clippy::cognitive_complexity, clippy::too_many_lines)]
10429    pub async fn create_distribution(
10430        &self,
10431        asset_uuid: &str,
10432        assignments: Vec<crate::model::AssetDistributionAssignment>,
10433    ) -> Result<crate::model::DistributionResponse, AmpError> {
10434        use crate::model::{CreateDistributionRequest, DistributionAssignmentRequest};
10435
10436        let create_span = tracing::debug_span!(
10437            "create_distribution",
10438            asset_uuid = %asset_uuid,
10439            assignment_count = assignments.len()
10440        );
10441        let _enter = create_span.enter();
10442
10443        tracing::debug!(
10444            "Creating distribution for asset {} with {} assignments",
10445            asset_uuid,
10446            assignments.len()
10447        );
10448
10449        // Validate inputs
10450        if asset_uuid.is_empty() {
10451            tracing::error!("Distribution creation failed: empty asset UUID");
10452            return Err(AmpError::validation("Asset UUID cannot be empty"));
10453        }
10454
10455        if assignments.is_empty() {
10456            tracing::error!("Distribution creation failed: empty assignments");
10457            return Err(AmpError::validation("Assignments cannot be empty"));
10458        }
10459
10460        // Convert AssetDistributionAssignment to DistributionAssignmentRequest
10461        // The API expects user_uuid field, but our input uses user_id
10462        tracing::trace!("Converting {} assignments to API format", assignments.len());
10463        let mut total_amount = 0.0;
10464        let api_assignments: Vec<DistributionAssignmentRequest> = assignments
10465            .into_iter()
10466            .enumerate()
10467            .map(
10468                #[allow(clippy::cognitive_complexity)]
10469                |(index, assignment)| {
10470                    tracing::trace!(
10471                        "Converting assignment {}: user_id={}, address={}, amount={}",
10472                        index,
10473                        assignment.user_id,
10474                        assignment.address,
10475                        assignment.amount
10476                    );
10477
10478                    // Validate assignment data
10479                    if assignment.user_id.is_empty() {
10480                        tracing::error!("Assignment {} has empty user_id", index);
10481                        return Err(AmpError::validation(format!(
10482                            "Assignment {index} has empty user_id"
10483                        )));
10484                    }
10485                    if assignment.address.is_empty() {
10486                        tracing::error!("Assignment {} has empty address", index);
10487                        return Err(AmpError::validation(format!(
10488                            "Assignment {index} has empty address"
10489                        )));
10490                    }
10491                    if assignment.amount <= 0.0 {
10492                        tracing::error!(
10493                            "Assignment {} has non-positive amount: {}",
10494                            index,
10495                            assignment.amount
10496                        );
10497                        return Err(AmpError::validation(format!(
10498                            "Assignment {} has non-positive amount: {}",
10499                            index, assignment.amount
10500                        )));
10501                    }
10502
10503                    total_amount += assignment.amount;
10504
10505                    Ok(DistributionAssignmentRequest {
10506                        user_uuid: assignment.user_id, // Map user_id to user_uuid for API
10507                        amount: assignment.amount,
10508                        address: assignment.address,
10509                    })
10510                },
10511            )
10512            .collect::<Result<Vec<_>, AmpError>>()?;
10513
10514        tracing::debug!(
10515            "Converted {} assignments successfully, total amount: {}",
10516            api_assignments.len(),
10517            total_amount
10518        );
10519
10520        let request = CreateDistributionRequest {
10521            assignments: api_assignments,
10522        };
10523
10524        tracing::debug!("Sending distribution creation request to AMP API");
10525        let api_call_start = std::time::Instant::now();
10526
10527        // Make the API call
10528        let response: crate::model::DistributionResponse = self
10529            .request_json(
10530                Method::GET,
10531                &["assets", asset_uuid, "distributions", "create"],
10532                Some(&request),
10533            )
10534            .await
10535            .map_err(
10536                #[allow(clippy::cognitive_complexity)]
10537                |e| {
10538                    let api_call_duration = api_call_start.elapsed();
10539                    let error_msg =
10540                        format!("Failed to create distribution after {api_call_duration:?}: {e}");
10541                    tracing::error!("{}", error_msg);
10542
10543                    // Check for specific API error patterns
10544                    let error_str = e.to_string();
10545                    if error_str.contains("404") || error_str.contains("not found") {
10546                        tracing::error!(
10547                            "Asset {} not found - verify asset UUID is correct",
10548                            asset_uuid
10549                        );
10550                    } else if error_str.contains("400") || error_str.contains("bad request") {
10551                        tracing::error!("Bad request - check assignment data format and values");
10552                    } else if error_str.contains("401") || error_str.contains("unauthorized") {
10553                        tracing::error!("Unauthorized - check API credentials and token validity");
10554                    } else if error_str.contains("403") || error_str.contains("forbidden") {
10555                        tracing::error!("Forbidden - check permissions for asset distribution");
10556                    } else if error_str.contains("429") || error_str.contains("rate limit") {
10557                        tracing::error!("Rate limited - wait before retrying");
10558                    } else if error_str.contains("500") || error_str.contains("internal server") {
10559                        tracing::error!(
10560                            "Server error - this may be a temporary issue, retry may help"
10561                        );
10562                    }
10563
10564                    AmpError::api(error_msg)
10565                },
10566            )?;
10567
10568        let api_call_duration = api_call_start.elapsed();
10569        tracing::info!(
10570            "Successfully created distribution: {} (took {:?})",
10571            response.distribution_uuid,
10572            api_call_duration
10573        );
10574
10575        // Validate response data
10576        if response.distribution_uuid.is_empty() {
10577            tracing::error!("API returned empty distribution UUID");
10578            return Err(AmpError::api("API returned empty distribution UUID"));
10579        }
10580
10581        if response.asset_id.is_empty() {
10582            tracing::error!("API returned empty asset ID");
10583            return Err(AmpError::api("API returned empty asset ID"));
10584        }
10585
10586        if response.map_address_amount.is_empty() {
10587            tracing::error!("API returned empty address mapping");
10588            return Err(AmpError::api("API returned empty address mapping"));
10589        }
10590
10591        tracing::debug!(
10592            "Distribution response validated - {} addresses mapped, asset_id: {}",
10593            response.map_address_amount.len(),
10594            response.asset_id
10595        );
10596
10597        Ok(response)
10598    }
10599
10600    /// Confirms a distribution with transaction and change data.
10601    ///
10602    /// This method submits the final confirmation for a distribution by providing
10603    /// the transaction details and any change UTXOs to the AMP API. This completes
10604    /// the distribution workflow after the transaction has been broadcast and confirmed
10605    /// on the blockchain.
10606    ///
10607    /// # Arguments
10608    /// * `asset_uuid` - The UUID of the asset being distributed
10609    /// * `distribution_uuid` - The UUID of the distribution to confirm (from `create_distribution` response)
10610    /// * `tx_data` - Transaction data containing details and txid from the blockchain
10611    /// * `change_data` - Vector of change UTXOs from the transaction
10612    ///
10613    /// # Errors
10614    /// Returns an error if:
10615    /// - Authentication fails
10616    /// - The asset UUID or distribution UUID is invalid
10617    /// - The transaction data is invalid or incomplete
10618    /// - The HTTP request fails
10619    /// - The server returns an error status
10620    /// - The response cannot be parsed
10621    ///
10622    /// # Examples
10623    /// ```no_run
10624    /// # use amp_rs::{ApiClient, model::{AmpTxData, Unspent}, AmpError};
10625    /// # #[tokio::main]
10626    /// # async fn main() -> Result<(), AmpError> {
10627    /// # let client = ApiClient::new().await?;
10628    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
10629    /// let distribution_uuid = "dist-550e8400-e29b-41d4-a716-446655440000";
10630    ///
10631    /// // Transaction data for AMP API confirmation
10632    /// let tx_data = AmpTxData {
10633    ///     details: serde_json::json!([{
10634    ///         "account": "",
10635    ///         "address": "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq",
10636    ///         "category": "send",
10637    ///         "amount": -100.0,
10638    ///         "vout": 0,
10639    ///         "fee": -0.001
10640    ///     }]),
10641    ///     txid: "abc123def456...".to_string(),
10642    /// };
10643    ///
10644    /// // Change UTXOs from Elements node listunspent call
10645    /// let change_data = vec![
10646    ///     Unspent {
10647    ///         txid: "abc123def456...".to_string(),
10648    ///         vout: 1,
10649    ///         amount: 25.0,
10650    ///         asset: "asset_id_hex".to_string(),
10651    ///         address: "change_address".to_string(),
10652    ///         spendable: true,
10653    ///         confirmations: Some(2),
10654    ///         scriptpubkey: Some("76a914...88ac".to_string()),
10655    ///         redeemscript: None,
10656    ///         witnessscript: None,
10657    ///         amountblinder: None,
10658    ///         assetblinder: None,
10659    ///     }
10660    /// ];
10661    ///
10662    /// client.confirm_distribution(asset_uuid, distribution_uuid, tx_data, change_data).await?;
10663    /// println!("Distribution confirmed successfully");
10664    /// # Ok(())
10665    /// # }
10666    /// ```
10667    ///
10668    /// # Related Methods
10669    /// - [`create_distribution`](Self::create_distribution) - Create a new distribution
10670    /// - [`get_asset_assignments`](Self::get_asset_assignments) - List assignments for an asset
10671    #[allow(clippy::cognitive_complexity, clippy::too_many_lines)]
10672    pub async fn confirm_distribution(
10673        &self,
10674        asset_uuid: &str,
10675        distribution_uuid: &str,
10676        tx_data: crate::model::AmpTxData,
10677        change_data: Vec<crate::model::Unspent>,
10678    ) -> Result<(), AmpError> {
10679        use crate::model::ConfirmDistributionRequest;
10680
10681        let confirm_span = tracing::debug_span!(
10682            "confirm_distribution",
10683            asset_uuid = %asset_uuid,
10684            distribution_uuid = %distribution_uuid,
10685            txid = %tx_data.txid,
10686            change_count = change_data.len()
10687        );
10688        let _enter = confirm_span.enter();
10689
10690        tracing::debug!(
10691            "Confirming distribution {} for asset {} with txid {} ({} change UTXOs)",
10692            distribution_uuid,
10693            asset_uuid,
10694            tx_data.txid,
10695            change_data.len()
10696        );
10697
10698        // Validate inputs
10699        if asset_uuid.is_empty() {
10700            tracing::error!("Distribution confirmation failed: empty asset UUID");
10701            return Err(AmpError::validation("Asset UUID cannot be empty"));
10702        }
10703
10704        if distribution_uuid.is_empty() {
10705            tracing::error!("Distribution confirmation failed: empty distribution UUID");
10706            return Err(AmpError::validation("Distribution UUID cannot be empty"));
10707        }
10708
10709        if tx_data.txid.is_empty() {
10710            tracing::error!("Distribution confirmation failed: empty transaction ID");
10711            return Err(AmpError::validation("Transaction ID cannot be empty"));
10712        }
10713
10714        // Log transaction details for debugging
10715        tracing::debug!("Transaction details array: {:?}", tx_data.details);
10716
10717        // Log change data details
10718        if change_data.is_empty() {
10719            tracing::debug!("No change UTXOs to include in confirmation");
10720        } else {
10721            let total_change: f64 = change_data.iter().map(|utxo| utxo.amount).sum();
10722            tracing::debug!(
10723                "Change data - {} UTXOs, total amount: {}",
10724                change_data.len(),
10725                total_change
10726            );
10727
10728            for (i, utxo) in change_data.iter().enumerate() {
10729                tracing::trace!(
10730                    "Change UTXO {}: txid={}, vout={}, amount={}, spendable={}",
10731                    i,
10732                    utxo.txid,
10733                    utxo.vout,
10734                    utxo.amount,
10735                    utxo.spendable
10736                );
10737            }
10738        }
10739
10740        let request = ConfirmDistributionRequest {
10741            tx_data: tx_data.clone(),
10742            change_data: change_data.clone(),
10743        };
10744
10745        tracing::debug!("Sending distribution confirmation request to AMP API");
10746        let api_call_start = std::time::Instant::now();
10747
10748        // Make the API call
10749        self.request_empty(
10750            Method::POST,
10751            &["assets", asset_uuid, "distributions", distribution_uuid, "confirm"],
10752            Some(&request),
10753        )
10754        .await
10755        .map_err(#[allow(clippy::cognitive_complexity)] |e| {
10756            let api_call_duration = api_call_start.elapsed();
10757            let error_msg = format!(
10758                "Failed to confirm distribution {} after {:?}: {}. IMPORTANT: Transaction {} was successful on blockchain. Use this txid to manually retry confirmation.",
10759                distribution_uuid, api_call_duration, e, tx_data.txid
10760            );
10761            tracing::error!("{}", error_msg);
10762
10763            // Check for specific API error patterns
10764            let error_str = e.to_string();
10765            if error_str.contains("404") || error_str.contains("not found") {
10766                tracing::error!("Distribution {} not found - verify distribution UUID is correct", distribution_uuid);
10767            } else if error_str.contains("400") || error_str.contains("bad request") {
10768                tracing::error!("Bad request - check transaction data format and change data");
10769            } else if error_str.contains("409") || error_str.contains("conflict") {
10770                tracing::error!("Conflict - distribution may already be confirmed");
10771            } else if error_str.contains("422") || error_str.contains("unprocessable") {
10772                tracing::error!("Unprocessable entity - check transaction confirmations and data validity");
10773            } else if error_str.contains("500") || error_str.contains("internal server") {
10774                tracing::error!("Server error - this may be a temporary issue, retry with txid: {}", tx_data.txid);
10775            }
10776
10777            AmpError::api(error_msg)
10778        })?;
10779
10780        let api_call_duration = api_call_start.elapsed();
10781        tracing::info!(
10782            "Successfully confirmed distribution: {} for asset: {} with txid: {} (took {:?})",
10783            distribution_uuid,
10784            asset_uuid,
10785            tx_data.txid,
10786            api_call_duration
10787        );
10788
10789        Ok(())
10790    }
10791
10792    /// Cancels an in-progress distribution for an asset.
10793    ///
10794    /// This method cancels a distribution that is currently in progress (unconfirmed status).
10795    /// Once a distribution is cancelled, it cannot be confirmed and the assigned amounts
10796    /// become available for new distributions.
10797    ///
10798    /// # Arguments
10799    /// * `asset_uuid` - The UUID of the asset
10800    /// * `distribution_uuid` - The UUID of the distribution to cancel
10801    ///
10802    /// # Returns
10803    /// Returns `Ok(())` if the distribution was successfully cancelled.
10804    ///
10805    /// # Errors
10806    /// Returns an error if:
10807    /// - Authentication fails
10808    /// - The HTTP request fails
10809    /// - The server returns an error status
10810    /// - The distribution is not found
10811    /// - The distribution is already confirmed and cannot be cancelled
10812    ///
10813    /// # Examples
10814    /// ```no_run
10815    /// use amp_rs::ApiClient;
10816    ///
10817    /// #[tokio::main]
10818    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
10819    ///     let client = ApiClient::new().await?;
10820    ///     
10821    ///     client.cancel_distribution(
10822    ///         "asset-uuid-123",
10823    ///         "distribution-uuid-456"
10824    ///     ).await?;
10825    ///     
10826    ///     println!("Distribution cancelled successfully");
10827    ///     Ok(())
10828    /// # }
10829    /// ```
10830    #[allow(clippy::cognitive_complexity)]
10831    pub async fn cancel_distribution(
10832        &self,
10833        asset_uuid: &str,
10834        distribution_uuid: &str,
10835    ) -> Result<(), AmpError> {
10836        let cancel_span = tracing::debug_span!(
10837            "cancel_distribution",
10838            asset_uuid = %asset_uuid,
10839            distribution_uuid = %distribution_uuid
10840        );
10841        let _enter = cancel_span.enter();
10842
10843        tracing::debug!(
10844            "Cancelling distribution {} for asset {}",
10845            distribution_uuid,
10846            asset_uuid
10847        );
10848
10849        // Validate inputs
10850        if asset_uuid.is_empty() {
10851            tracing::error!("Distribution cancellation failed: empty asset UUID");
10852            return Err(AmpError::validation("Asset UUID cannot be empty"));
10853        }
10854
10855        if distribution_uuid.is_empty() {
10856            tracing::error!("Distribution cancellation failed: empty distribution UUID");
10857            return Err(AmpError::validation("Distribution UUID cannot be empty"));
10858        }
10859
10860        let api_call_start = std::time::Instant::now();
10861
10862        self.request_empty(
10863            Method::DELETE,
10864            &[
10865                "assets",
10866                asset_uuid,
10867                "distributions",
10868                distribution_uuid,
10869                "cancel",
10870            ],
10871            None::<&()>,
10872        )
10873        .await
10874        .map_err(|e| {
10875            let api_call_duration = api_call_start.elapsed();
10876            let error_msg = format!(
10877                "Failed to cancel distribution {distribution_uuid} for asset {asset_uuid} after {api_call_duration:?}: {e}"
10878            );
10879            tracing::error!("{}", error_msg);
10880
10881            // Check for specific API error patterns
10882            let error_str = e.to_string();
10883            if error_str.contains("404") || error_str.contains("not found") {
10884                tracing::error!(
10885                    "Distribution {} not found - verify distribution UUID is correct",
10886                    distribution_uuid
10887                );
10888            } else if error_str.contains("400") || error_str.contains("bad request") {
10889                tracing::error!("Bad request - distribution may already be confirmed or invalid");
10890            } else if error_str.contains("409") || error_str.contains("conflict") {
10891                tracing::error!(
10892                    "Conflict - distribution may already be confirmed and cannot be cancelled"
10893                );
10894            } else if error_str.contains("422") || error_str.contains("unprocessable") {
10895                tracing::error!(
10896                    "Unprocessable entity - distribution is in a state that cannot be cancelled"
10897                );
10898            }
10899
10900            AmpError::api(error_msg)
10901        })?;
10902
10903        let api_call_duration = api_call_start.elapsed();
10904        tracing::info!(
10905            "Successfully cancelled distribution: {} for asset: {} (took {:?})",
10906            distribution_uuid,
10907            asset_uuid,
10908            api_call_duration
10909        );
10910
10911        Ok(())
10912    }
10913
10914    /// Gets all distributions for a specific asset.
10915    ///
10916    /// This method retrieves all distributions (both confirmed and unconfirmed) for the specified asset.
10917    /// This is useful for checking if there are any in-progress distributions before deleting an asset.
10918    ///
10919    /// # Arguments
10920    /// * `asset_uuid` - The UUID of the asset to get distributions for
10921    ///
10922    /// # Returns
10923    /// Returns a vector of `Distribution` objects for the asset.
10924    ///
10925    /// # Errors
10926    /// Returns an error if:
10927    /// - Authentication fails
10928    /// - The HTTP request fails
10929    /// - The server returns an error status
10930    /// - The response cannot be parsed
10931    ///
10932    /// # Examples
10933    /// ```no_run
10934    /// use amp_rs::ApiClient;
10935    ///
10936    /// #[tokio::main]
10937    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
10938    ///     let client = ApiClient::new().await?;
10939    ///     
10940    ///     let distributions = client.get_asset_distributions("asset-uuid-123").await?;
10941    ///     
10942    ///     for distribution in distributions {
10943    ///         println!("Distribution: {} - Status: {:?}",
10944    ///                  distribution.distribution_uuid,
10945    ///                  distribution.distribution_status);
10946    ///     }
10947    ///     Ok(())
10948    /// }
10949    /// ```
10950    pub async fn get_asset_distributions(
10951        &self,
10952        asset_uuid: &str,
10953    ) -> Result<Vec<crate::model::Distribution>, Error> {
10954        let distributions_span = tracing::debug_span!(
10955            "get_asset_distributions",
10956            asset_uuid = %asset_uuid
10957        );
10958        let _enter = distributions_span.enter();
10959
10960        tracing::debug!("Getting distributions for asset {}", asset_uuid);
10961
10962        // Validate input
10963        if asset_uuid.is_empty() {
10964            tracing::error!("Get distributions failed: empty asset UUID");
10965            return Err(Error::RequestFailed(
10966                "Asset UUID cannot be empty".to_string(),
10967            ));
10968        }
10969
10970        self.request_json(
10971            Method::GET,
10972            &["assets", asset_uuid, "distributions"],
10973            None::<&()>,
10974        )
10975        .await
10976    }
10977
10978    /// Gets a specific distribution by UUID for an asset.
10979    ///
10980    /// This method retrieves detailed information about a specific distribution,
10981    /// including its status, UUID, and associated transactions.
10982    ///
10983    /// # Arguments
10984    /// * `asset_uuid` - The UUID of the asset
10985    /// * `distribution_uuid` - The UUID of the distribution to retrieve
10986    ///
10987    /// # Returns
10988    /// Returns a `Distribution` struct containing:
10989    /// - `distribution_uuid` - The unique identifier for the distribution
10990    /// - `distribution_status` - Current status of the distribution
10991    /// - `transactions` - List of transactions associated with the distribution
10992    ///
10993    /// # Errors
10994    /// Returns an error if:
10995    /// - Authentication fails
10996    /// - The HTTP request fails
10997    /// - The server returns an error status
10998    /// - The response cannot be parsed as JSON
10999    /// - The asset UUID or distribution UUID is empty
11000    ///
11001    /// # Examples
11002    /// ```no_run
11003    /// # use amp_rs::ApiClient;
11004    /// # #[tokio::main]
11005    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
11006    /// let client = ApiClient::new().await?;
11007    ///
11008    /// let distribution = client.get_asset_distribution(
11009    ///     "asset-uuid-123",
11010    ///     "distribution-uuid-456"
11011    /// ).await?;
11012    ///
11013    /// println!("Distribution: {} - Status: {:?}",
11014    ///          distribution.distribution_uuid,
11015    ///          distribution.distribution_status);
11016    /// # Ok(())
11017    /// # }
11018    /// ```
11019    ///
11020    /// # Related Methods
11021    /// - [`get_asset_distributions`](Self::get_asset_distributions) - List all distributions for an asset
11022    /// - [`create_distribution`](Self::create_distribution) - Create a new distribution
11023    /// - [`confirm_distribution`](Self::confirm_distribution) - Confirm a distribution
11024    /// - [`cancel_distribution`](Self::cancel_distribution) - Cancel a distribution
11025    #[allow(clippy::cognitive_complexity)]
11026    pub async fn get_asset_distribution(
11027        &self,
11028        asset_uuid: &str,
11029        distribution_uuid: &str,
11030    ) -> Result<crate::model::Distribution, Error> {
11031        let distribution_span = tracing::debug_span!(
11032            "get_asset_distribution",
11033            asset_uuid = %asset_uuid,
11034            distribution_uuid = %distribution_uuid
11035        );
11036        let _enter = distribution_span.enter();
11037
11038        tracing::debug!(
11039            "Getting distribution {} for asset {}",
11040            distribution_uuid,
11041            asset_uuid
11042        );
11043
11044        // Validate inputs
11045        if asset_uuid.is_empty() {
11046            tracing::error!("Get distribution failed: empty asset UUID");
11047            return Err(Error::RequestFailed(
11048                "Asset UUID cannot be empty".to_string(),
11049            ));
11050        }
11051
11052        if distribution_uuid.is_empty() {
11053            tracing::error!("Get distribution failed: empty distribution UUID");
11054            return Err(Error::RequestFailed(
11055                "Distribution UUID cannot be empty".to_string(),
11056            ));
11057        }
11058
11059        self.request_json(
11060            Method::GET,
11061            &["assets", asset_uuid, "distributions", distribution_uuid],
11062            None::<&()>,
11063        )
11064        .await
11065    }
11066
11067    /// Gets a specific manager by ID.
11068    ///
11069    /// # Arguments
11070    /// * `manager_id` - The ID of the manager to retrieve
11071    ///
11072    /// # Errors
11073    /// Returns an error if:
11074    /// - Authentication fails
11075    /// - The HTTP request fails
11076    /// - The server returns an error status
11077    /// - The response cannot be parsed as JSON
11078    pub async fn get_manager(&self, manager_id: i64) -> Result<crate::model::Manager, Error> {
11079        self.request_json(
11080            Method::GET,
11081            &["managers", &manager_id.to_string()],
11082            None::<&()>,
11083        )
11084        .await
11085    }
11086
11087    /// Removes a manager's permissions to modify a specific asset.
11088    ///
11089    /// This method revokes a manager's access to a specific asset, preventing them from
11090    /// performing asset management operations such as creating assignments, managing ownership,
11091    /// or modifying asset properties. The manager will no longer be able to access this asset
11092    /// through their management interface.
11093    ///
11094    /// # Arguments
11095    /// * `manager_id` - The ID of the manager to remove permissions from
11096    /// * `asset_uuid` - The UUID of the asset to remove permissions for
11097    ///
11098    /// # Returns
11099    /// Returns `Ok(())` on successful permission removal.
11100    ///
11101    /// # Errors
11102    /// Returns an error if:
11103    /// - Authentication fails or insufficient permissions
11104    /// - The manager ID is invalid or does not exist
11105    /// - The asset UUID is invalid or does not exist
11106    /// - The manager does not currently have permissions for this asset
11107    /// - The HTTP request fails
11108    /// - The server returns an error status
11109    ///
11110    /// # Examples
11111    /// ```no_run
11112    /// # use amp_rs::ApiClient;
11113    /// # #[tokio::main]
11114    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
11115    /// let client = ApiClient::new().await?;
11116    ///
11117    /// let manager_id = 123;
11118    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
11119    ///
11120    /// client.manager_remove_asset(manager_id, asset_uuid).await?;
11121    /// println!("Removed asset {} from manager {}", asset_uuid, manager_id);
11122    /// # Ok(())
11123    /// # }
11124    /// ```
11125    ///
11126    /// # Related Methods
11127    /// - [`add_asset_to_manager`](Self::add_asset_to_manager) - Grant manager permissions for an asset
11128    /// - [`get_manager`](Self::get_manager) - Get manager information including current assets
11129    /// - [`revoke_manager`](Self::revoke_manager) - Remove all asset permissions from manager
11130    /// - [`lock_manager`](Self::lock_manager) - Lock manager account
11131    pub async fn manager_remove_asset(
11132        &self,
11133        manager_id: i64,
11134        asset_uuid: &str,
11135    ) -> Result<(), Error> {
11136        self.request_empty(
11137            Method::POST,
11138            &[
11139                "managers",
11140                &manager_id.to_string(),
11141                "assets",
11142                asset_uuid,
11143                "remove",
11144            ],
11145            None::<&()>,
11146        )
11147        .await
11148    }
11149
11150    /// Revokes all asset permissions for a manager.
11151    ///
11152    /// This method first retrieves the manager's current asset permissions,
11153    /// then removes the manager's access to each asset they currently have access to.
11154    ///
11155    /// # Arguments
11156    /// * `manager_id` - The ID of the manager to revoke permissions for
11157    ///
11158    /// # Errors
11159    /// Returns an error if:
11160    /// - Authentication fails
11161    /// - The HTTP request fails
11162    /// - The server returns an error status
11163    /// - Any individual asset removal fails
11164    pub async fn revoke_manager(&self, manager_id: i64) -> Result<(), Error> {
11165        // First, get the manager to see which assets they have access to
11166        let manager = self.get_manager(manager_id).await?;
11167
11168        // Remove the manager's access to each asset
11169        for asset_uuid in &manager.assets {
11170            self.manager_remove_asset(manager_id, asset_uuid).await?;
11171        }
11172
11173        Ok(())
11174    }
11175
11176    /// Gets the current manager information as raw JSON.
11177    ///
11178    /// This method calls the `/managers/me` endpoint to retrieve information
11179    /// about the currently authenticated manager.
11180    ///
11181    /// # Errors
11182    /// Returns an error if:
11183    /// - Authentication fails
11184    /// - The HTTP request fails
11185    /// - The server returns an error status
11186    /// - The response cannot be parsed as JSON
11187    pub async fn get_current_manager_raw(&self) -> Result<serde_json::Value, Error> {
11188        self.request_json(Method::GET, &["managers", "me"], None::<&()>)
11189            .await
11190    }
11191
11192    /// Locks a manager account to prevent further operations.
11193    ///
11194    /// This method sends a PUT request to lock the specified manager, preventing any further
11195    /// operations on that manager account. This is typically used for security purposes or
11196    /// when a manager needs to be temporarily disabled.
11197    ///
11198    /// # Arguments
11199    /// * `manager_id` - The ID of the manager to lock
11200    ///
11201    /// # Returns
11202    /// Returns `Ok(())` if the manager was successfully locked.
11203    ///
11204    /// # Errors
11205    /// Returns an error if:
11206    /// - Authentication fails
11207    /// - The HTTP request fails
11208    /// - The server returns an error status
11209    /// - The manager ID is invalid or does not exist
11210    /// - The manager is already locked
11211    ///
11212    /// # Example
11213    /// ```no_run
11214    /// # use amp_rs::ApiClient;
11215    /// # #[tokio::main]
11216    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
11217    /// let client = ApiClient::new().await?;
11218    ///
11219    /// // Lock manager with ID 123
11220    /// client.lock_manager(123).await?;
11221    /// println!("Manager 123 has been locked successfully");
11222    /// # Ok(())
11223    /// # }
11224    /// ```
11225    pub async fn lock_manager(&self, manager_id: i64) -> Result<(), Error> {
11226        self.request_empty(
11227            Method::PUT,
11228            &["managers", &manager_id.to_string(), "lock"],
11229            None::<&()>,
11230        )
11231        .await
11232    }
11233
11234    /// Unlocks a manager account.
11235    ///
11236    /// # Arguments
11237    /// * `manager_id` - The ID of the manager to unlock
11238    ///
11239    /// # Errors
11240    /// Returns an error if:
11241    /// - Authentication fails
11242    /// - The HTTP request fails
11243    /// - The server returns an error status
11244    pub async fn unlock_manager(&self, manager_id: i64) -> Result<(), Error> {
11245        self.request_empty(
11246            Method::PUT,
11247            &["managers", &manager_id.to_string(), "unlock"],
11248            None::<&()>,
11249        )
11250        .await
11251    }
11252
11253    /// Authorizes a manager to manage a specific asset.
11254    ///
11255    /// This method sends a PUT request to authorize the specified manager to manage the given asset.
11256    /// Once authorized, the manager will have permissions to perform operations on the asset such as
11257    /// creating assignments, managing ownership, and other asset-related operations.
11258    ///
11259    /// # Arguments
11260    /// * `manager_id` - The ID of the manager to authorize
11261    /// * `asset_uuid` - The UUID of the asset to add to the manager's authorized assets
11262    ///
11263    /// # Returns
11264    /// Returns `Ok(())` if the manager was successfully authorized for the asset.
11265    ///
11266    /// # Errors
11267    /// Returns an error if:
11268    /// - Authentication fails or insufficient permissions
11269    /// - The HTTP request fails
11270    /// - The server returns an error status
11271    /// - The manager ID is invalid or does not exist
11272    /// - The asset UUID is invalid or does not exist
11273    /// - The manager is already authorized for this asset
11274    /// - The manager is locked and cannot be modified
11275    ///
11276    /// # Examples
11277    /// ```no_run
11278    /// # use amp_rs::ApiClient;
11279    /// # #[tokio::main]
11280    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
11281    /// let client = ApiClient::new().await?;
11282    ///
11283    /// // Authorize manager 123 to manage asset with UUID "550e8400-e29b-41d4-a716-446655440000"
11284    /// let manager_id = 123;
11285    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
11286    ///
11287    /// client.add_asset_to_manager(manager_id, asset_uuid).await?;
11288    /// println!("Manager {} is now authorized to manage asset {}", manager_id, asset_uuid);
11289    /// # Ok(())
11290    /// # }
11291    /// ```
11292    ///
11293    /// # Related Methods
11294    /// - [`manager_remove_asset`](Self::manager_remove_asset) - Remove manager permissions for an asset
11295    /// - [`get_manager`](Self::get_manager) - Get manager information including current assets
11296    /// - [`get_manager_permissions`](Self::get_manager_permissions) - Get manager's current permissions
11297    /// - [`lock_manager`](Self::lock_manager) - Lock manager account
11298    pub async fn add_asset_to_manager(
11299        &self,
11300        manager_id: i64,
11301        asset_uuid: &str,
11302    ) -> Result<(), Error> {
11303        self.request_empty(
11304            Method::PUT,
11305            &[
11306                "managers",
11307                &manager_id.to_string(),
11308                "assets",
11309                asset_uuid,
11310                "add",
11311            ],
11312            None::<&()>,
11313        )
11314        .await
11315    }
11316
11317    /// Deletes a specific asset assignment.
11318    ///
11319    /// # Arguments
11320    /// * `asset_uuid` - The UUID of the asset
11321    /// * `assignment_id` - The ID of the assignment to delete
11322    ///
11323    /// # Errors
11324    /// Returns an error if:
11325    /// - Authentication fails
11326    /// - The HTTP request fails
11327    /// - The server returns an error status
11328    ///   Removes an asset assignment.
11329    ///
11330    /// This method permanently deletes an asset assignment, returning the allocated assets
11331    /// back to the available pool. This operation cannot be undone. If the assignment has
11332    /// already been distributed, this operation may fail.
11333    ///
11334    /// # Arguments
11335    /// * `asset_uuid` - The UUID of the asset containing the assignment
11336    /// * `assignment_id` - The ID of the assignment to delete
11337    ///
11338    /// # Returns
11339    /// Returns `Ok(())` on successful deletion.
11340    ///
11341    /// # Errors
11342    /// Returns an error if:
11343    /// - Authentication fails or insufficient permissions
11344    /// - The asset UUID is invalid or does not exist
11345    /// - The assignment ID is invalid or does not exist
11346    /// - The assignment has already been distributed and cannot be deleted
11347    /// - The assignment is locked and cannot be modified
11348    /// - The HTTP request fails
11349    /// - The server returns an error status
11350    ///
11351    /// # Examples
11352    /// ```no_run
11353    /// # use amp_rs::ApiClient;
11354    /// # #[tokio::main]
11355    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
11356    /// let client = ApiClient::new().await?;
11357    ///
11358    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
11359    /// let assignment_id = "123";
11360    ///
11361    /// client.delete_asset_assignment(asset_uuid, assignment_id).await?;
11362    /// println!("Successfully deleted assignment {}", assignment_id);
11363    /// # Ok(())
11364    /// # }
11365    /// ```
11366    ///
11367    /// # Related Methods
11368    /// - [`get_asset_assignment`](Self::get_asset_assignment) - Get assignment details before deletion
11369    /// - [`create_asset_assignments`](Self::create_asset_assignments) - Create new assignments
11370    /// - [`edit_asset_assignment`](Self::edit_asset_assignment) - Update assignment instead of deleting
11371    /// - [`lock_asset_assignment`](Self::lock_asset_assignment) - Lock assignment to prevent changes
11372    pub async fn delete_asset_assignment(
11373        &self,
11374        asset_uuid: &str,
11375        assignment_id: &str,
11376    ) -> Result<(), Error> {
11377        self.request_empty(
11378            Method::DELETE,
11379            &["assets", asset_uuid, "assignments", assignment_id, "delete"],
11380            None::<&()>,
11381        )
11382        .await
11383    }
11384
11385    /// Locks a specific asset assignment.
11386    ///
11387    /// # Arguments
11388    /// * `asset_uuid` - The UUID of the asset
11389    /// * `assignment_id` - The ID of the assignment to lock
11390    ///
11391    /// # Errors
11392    /// Returns an error if:
11393    /// - Authentication fails
11394    /// - The HTTP request fails
11395    /// - The server returns an error status
11396    pub async fn lock_asset_assignment(
11397        &self,
11398        asset_uuid: &str,
11399        assignment_id: &str,
11400    ) -> Result<Assignment, Error> {
11401        self.request_json(
11402            Method::PUT,
11403            &["assets", asset_uuid, "assignments", assignment_id, "lock"],
11404            None::<&()>,
11405        )
11406        .await
11407    }
11408
11409    /// Unlocks a specific asset assignment.
11410    ///
11411    /// # Arguments
11412    /// * `asset_uuid` - The UUID of the asset
11413    /// * `assignment_id` - The ID of the assignment to unlock
11414    ///
11415    /// # Errors
11416    /// Returns an error if:
11417    /// - Authentication fails
11418    /// - The HTTP request fails
11419    /// - The server returns an error status
11420    pub async fn unlock_asset_assignment(
11421        &self,
11422        asset_uuid: &str,
11423        assignment_id: &str,
11424    ) -> Result<Assignment, Error> {
11425        self.request_json(
11426            Method::PUT,
11427            &["assets", asset_uuid, "assignments", assignment_id, "unlock"],
11428            None::<&()>,
11429        )
11430        .await
11431    }
11432
11433    /// Adds categories to a registered user.
11434    ///
11435    /// # Arguments
11436    /// * `registered_user_id` - The ID of the registered user
11437    /// * `categories` - A slice of category IDs to add to the user
11438    ///
11439    /// # Errors
11440    /// Returns an error if:
11441    /// - Authentication fails
11442    /// - The HTTP request fails
11443    /// - The server returns an error status
11444    /// - The registered user ID is invalid
11445    /// - Any category ID is invalid
11446    pub async fn add_categories_to_registered_user(
11447        &self,
11448        registered_user_id: i64,
11449        categories: &[i64],
11450    ) -> Result<(), Error> {
11451        let request_body = CategoriesRequest {
11452            categories: categories.to_vec(),
11453        };
11454
11455        self.request_empty(
11456            Method::PUT,
11457            &[
11458                "registered_users",
11459                &registered_user_id.to_string(),
11460                "categories",
11461                "add",
11462            ],
11463            Some(request_body),
11464        )
11465        .await
11466    }
11467
11468    /// Removes categories from a registered user
11469    ///
11470    /// # Arguments
11471    /// * `registered_user_id` - The ID of the registered user
11472    /// * `categories` - A slice of category IDs to remove from the user
11473    ///
11474    /// # Returns
11475    /// Returns `Ok(())` if the categories are successfully removed, or an error if:
11476    /// - Authentication fails
11477    /// - The HTTP request fails
11478    /// - The server returns an error status
11479    /// - The registered user ID is invalid
11480    /// - Any category ID is not associated with the user
11481    pub async fn remove_categories_from_registered_user(
11482        &self,
11483        registered_user_id: i64,
11484        categories: &[i64],
11485    ) -> Result<(), Error> {
11486        let request_body = CategoriesRequest {
11487            categories: categories.to_vec(),
11488        };
11489
11490        self.request_empty(
11491            Method::PUT,
11492            &[
11493                "registered_users",
11494                &registered_user_id.to_string(),
11495                "categories",
11496                "delete",
11497            ],
11498            Some(request_body),
11499        )
11500        .await
11501    }
11502
11503    /// Distributes assets to multiple users through a comprehensive workflow
11504    ///
11505    /// This method orchestrates the complete asset distribution process:
11506    /// 1. Validates input parameters (asset UUID format, assignments structure)
11507    /// 2. Verifies `ElementsRpc` connection and signer interface availability
11508    /// 3. Authenticates with the AMP API using the client's token
11509    /// 4. Creates a distribution request via the AMP API
11510    /// 5. Constructs and signs the blockchain transaction using the provided signer
11511    /// 6. Broadcasts the transaction to the Elements network
11512    /// 7. Waits for blockchain confirmations (2 confirmations minimum)
11513    /// 8. Confirms the distribution with the AMP API
11514    ///
11515    /// # Arguments
11516    /// * `asset_uuid` - The UUID of the asset to distribute (must be valid UUID format)
11517    /// * `assignments` - Vector of assignments specifying `user_id`, address, and amount
11518    /// * `node_rpc` - `ElementsRpc` client for blockchain operations
11519    /// * `signer` - Signer implementation for transaction signing
11520    ///
11521    /// # Returns
11522    /// Returns `Ok(())` if the distribution completes successfully, or an `AmpError` if:
11523    /// - Input validation fails (invalid UUID format, empty assignments, etc.)
11524    /// - `ElementsRpc` connection cannot be established
11525    /// - Signer interface is not available
11526    /// - Authentication with AMP API fails
11527    /// - Distribution creation fails
11528    /// - Transaction construction or signing fails
11529    /// - Blockchain broadcasting fails
11530    /// - Confirmation timeout occurs
11531    /// - Distribution confirmation with AMP API fails
11532    ///
11533    /// # Examples
11534    /// ```no_run
11535    /// # use amp_rs::{ApiClient, ElementsRpc, AmpError};
11536    /// # use amp_rs::model::AssetDistributionAssignment;
11537    /// # use amp_rs::signer::{Signer, LwkSoftwareSigner};
11538    /// # #[tokio::main]
11539    /// # async fn main() -> Result<(), AmpError> {
11540    /// let client = ApiClient::new().await?;
11541    /// let elements_rpc = ElementsRpc::from_env()?;
11542    /// let (_, signer) = LwkSoftwareSigner::generate_new()?;
11543    ///
11544    /// let assignments = vec![
11545    ///     AssetDistributionAssignment {
11546    ///         user_id: "user123".to_string(),
11547    ///         address: "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
11548    ///         amount: 100.0,
11549    ///     },
11550    /// ];
11551    ///
11552    /// client.distribute_asset(
11553    ///     "550e8400-e29b-41d4-a716-446655440000",
11554    ///     assignments,
11555    ///     &elements_rpc,
11556    ///     "wallet_name",
11557    ///     &signer
11558    /// ).await?;
11559    /// # Ok(())
11560    /// # }
11561    /// ```
11562    ///
11563    /// # Requirements
11564    /// This method implements requirements:
11565    /// - 1.1: Single method for complete distribution workflow
11566    /// - 2.2: Assignment details validation
11567    /// - 2.4: Input validation for all parameters
11568    /// - 5.1: Comprehensive error handling with context
11569    #[allow(clippy::cognitive_complexity, clippy::too_many_lines)]
11570    pub async fn distribute_asset(
11571        &self,
11572        asset_uuid: &str,
11573        assignments: Vec<AssetDistributionAssignment>,
11574        node_rpc: &ElementsRpc,
11575        wallet_name: &str,
11576        signer: &dyn Signer,
11577    ) -> Result<(), AmpError> {
11578        let distribution_span = tracing::info_span!(
11579            "distribute_asset",
11580            asset_uuid = %asset_uuid,
11581            assignment_count = assignments.len()
11582        );
11583        let _enter = distribution_span.enter();
11584
11585        tracing::info!(
11586            "Starting asset distribution workflow for asset: {} with {} assignments",
11587            asset_uuid,
11588            assignments.len()
11589        );
11590
11591        // Step 1: Input validation - asset_uuid format
11592        tracing::debug!("Step 1: Validating asset UUID format");
11593        Self::validate_asset_uuid(asset_uuid).map_err(|e| {
11594            let error = AmpError::validation(format!("Invalid asset UUID: {e}"));
11595            tracing::error!("Asset UUID validation failed: {}", e);
11596            error.with_context("Step 1: Asset UUID validation")
11597        })?;
11598        tracing::debug!("Asset UUID validation passed");
11599
11600        // Step 2: Input validation - assignments data structure
11601        tracing::debug!("Step 2: Validating {} assignments", assignments.len());
11602        Self::validate_assignments(&assignments).map_err(|e| {
11603            let error = AmpError::validation(format!("Invalid assignments: {e}"));
11604            tracing::error!("Assignments validation failed: {}", e);
11605            error.with_context("Step 2: Assignments validation")
11606        })?;
11607        tracing::debug!("Assignments validation passed");
11608
11609        // Step 3: Check ElementsRpc connection availability
11610        tracing::debug!("Step 3: Validating Elements RPC connection");
11611        self.validate_elements_rpc_connection(node_rpc)
11612            .await
11613            .map_err(|e| {
11614                let error = AmpError::rpc(format!("ElementsRpc connection validation failed: {e}"));
11615                tracing::error!("Elements RPC connection validation failed: {}", e);
11616                error.with_context("Step 3: Elements RPC connection validation")
11617            })?;
11618        tracing::debug!("Elements RPC connection validation passed");
11619
11620        // Step 4: Check signer interface availability
11621        tracing::debug!("Step 4: Validating signer interface");
11622        self.validate_signer_interface(signer).await.map_err(|e| {
11623            let error = AmpError::validation(format!("Signer interface validation failed: {e}"));
11624            tracing::error!("Signer interface validation failed: {}", e);
11625            error.with_context("Step 4: Signer interface validation")
11626        })?;
11627        tracing::debug!("Signer interface validation passed");
11628
11629        tracing::info!("✓ All input validations completed successfully");
11630
11631        // Step 5: Authenticate with AMP API using existing TokenManager
11632        tracing::debug!("Step 5: Authenticating with AMP API");
11633        let _token = self.token_strategy.get_token().await.map_err(|e| {
11634            tracing::error!("AMP API authentication failed: {}", e);
11635            let amp_error = AmpError::Existing(e);
11636            if amp_error.is_retryable() {
11637                if let Some(instructions) = amp_error.retry_instructions() {
11638                    tracing::warn!("Retry instructions: {}", instructions);
11639                }
11640            }
11641            amp_error.with_context("Step 5: AMP API authentication")
11642        })?;
11643        tracing::info!("✓ Successfully authenticated with AMP API");
11644
11645        // Step 6: Create distribution request and parse response data
11646        tracing::debug!(
11647            "Step 6: Creating distribution request with {} assignments",
11648            assignments.len()
11649        );
11650        let distribution_response = self
11651            .create_distribution(asset_uuid, assignments)
11652            .await
11653            .map_err(|e| {
11654                tracing::error!("Distribution creation failed: {}", e);
11655                if e.is_retryable() {
11656                    if let Some(instructions) = e.retry_instructions() {
11657                        tracing::warn!("Retry instructions: {}", instructions);
11658                    }
11659                }
11660                e.with_context("Step 6: Distribution creation")
11661            })?;
11662
11663        tracing::info!(
11664            "✓ Distribution created successfully: {} with asset_id: {}",
11665            distribution_response.distribution_uuid,
11666            distribution_response.asset_id
11667        );
11668
11669        // Step 7: Verify Elements node status and execute transaction workflow
11670        tracing::debug!("Step 7: Verifying Elements node status");
11671        let (network_info, blockchain_info) = node_rpc.get_node_status().await.map_err(|e| {
11672            tracing::error!("Elements node status verification failed: {}", e);
11673            if e.is_retryable() {
11674                if let Some(instructions) = e.retry_instructions() {
11675                    tracing::warn!("Retry instructions: {}", instructions);
11676                }
11677            }
11678            e.with_context("Step 7: Elements node status verification")
11679        })?;
11680
11681        tracing::info!(
11682            "✓ Elements node verified - chain: {}, blocks: {}, connections: {}",
11683            blockchain_info.chain,
11684            blockchain_info.blocks,
11685            network_info.connections
11686        );
11687
11688        // Step 8: Send distribution transaction using Elements' sendmany
11689        tracing::debug!("Step 8: Sending distribution transaction using Elements sendmany");
11690
11691        // Create asset amounts map for sendmany (all outputs use the same asset)
11692        let mut asset_amounts = std::collections::HashMap::new();
11693        for address in distribution_response.map_address_amount.keys() {
11694            asset_amounts.insert(address.clone(), distribution_response.asset_id.clone());
11695        }
11696
11697        tracing::info!(
11698            "Using sendmany for {} outputs with asset {}",
11699            distribution_response.map_address_amount.len(),
11700            distribution_response.asset_id
11701        );
11702
11703        // Use Elements' sendmany which properly handles confidential transactions
11704        let txid = node_rpc
11705            .sendmany(
11706                wallet_name,
11707                distribution_response.map_address_amount.clone(),
11708                asset_amounts,
11709                Some(0), // min_conf: 0 to include unconfirmed UTXOs (matches Python implementation)
11710                Some("AMP asset distribution"), // comment
11711                None,    // subtract_fee_from: let Elements handle fees automatically
11712                Some(false), // replaceable: false for final transactions
11713                Some(1), // conf_target: 1 block for faster confirmation
11714                Some("UNSET"), // estimate_mode: let Elements choose
11715            )
11716            .await
11717            .map_err(|e| {
11718                tracing::error!("Sendmany transaction failed: {}", e);
11719                if e.is_retryable() {
11720                    if let Some(instructions) = e.retry_instructions() {
11721                        tracing::warn!("Retry instructions: {}", instructions);
11722                    }
11723                }
11724                e.with_context("Step 8: Sendmany transaction")
11725            })?;
11726
11727        tracing::info!("✓ Transaction sent successfully with ID: {}", txid);
11728
11729        // Step 9: Wait for confirmations
11730        tracing::debug!("Step 9: Waiting for blockchain confirmations (minimum 2 confirmations, 10-minute timeout)");
11731        let confirmation_start = std::time::Instant::now();
11732        let tx_detail = node_rpc.wait_for_confirmations(&txid, Some(2), Some(10)).await
11733            .map_err(|e| {
11734                let elapsed = confirmation_start.elapsed();
11735                tracing::error!(
11736                    "Confirmation waiting failed after {:?}: {}",
11737                    elapsed,
11738                    e
11739                );
11740
11741                if let AmpError::Timeout(_) = &e {
11742                    tracing::warn!(
11743                        "Confirmation timeout - transaction {} may still be pending. \
11744                        Use this txid to manually confirm the distribution if it gets confirmed later.",
11745                        txid
11746                    );
11747                    let timeout_error = AmpError::timeout(format!(
11748                        "Confirmation timeout for txid: {txid}. Use this txid to manually confirm the distribution."
11749                    ));
11750                    timeout_error.with_context("Step 9: Confirmation waiting")
11751                } else {
11752                    if e.is_retryable() {
11753                        if let Some(instructions) = e.retry_instructions() {
11754                            tracing::warn!("Retry instructions: {}", instructions);
11755                        }
11756                    }
11757                    e.with_context(format!("Step 9: Confirmation waiting for txid: {txid}"))
11758                }
11759            })?;
11760
11761        let confirmation_duration = confirmation_start.elapsed();
11762        tracing::info!(
11763            "✓ Transaction confirmed with {} confirmations at block height: {:?} (took {:?})",
11764            tx_detail.confirmations,
11765            tx_detail.blockheight,
11766            confirmation_duration
11767        );
11768
11769        // Step 10: Collect change data for confirmation
11770        tracing::debug!("Step 10: Collecting change data for distribution confirmation");
11771        let change_data = node_rpc
11772            .collect_change_data(
11773                &distribution_response.asset_id,
11774                &txid,
11775                node_rpc,
11776                wallet_name,
11777            )
11778            .await
11779            .map_err(|e| {
11780                tracing::error!("Change data collection failed: {}", e);
11781                if e.is_retryable() {
11782                    if let Some(instructions) = e.retry_instructions() {
11783                        tracing::warn!("Retry instructions: {}", instructions);
11784                    }
11785                }
11786                e.with_context("Step 10: Change data collection")
11787            })?;
11788
11789        tracing::info!("✓ Collected {} change UTXOs", change_data.len());
11790        if !change_data.is_empty() {
11791            tracing::debug!("Change UTXOs: {:?}", change_data);
11792        }
11793
11794        // Step 11: Submit final confirmation to AMP API
11795        tracing::debug!("Step 11: Submitting final confirmation to AMP API");
11796
11797        // Extract the details field from the transaction (matching Python implementation)
11798        // Python: details = rpc.call('gettransaction', txid).get('details')
11799        let transaction_details = tx_detail.details.unwrap_or_else(Vec::new);
11800        tracing::debug!(
11801            "Transaction details for confirmation: {:?}",
11802            transaction_details
11803        );
11804
11805        let amp_tx_data = crate::model::AmpTxData {
11806            details: serde_json::Value::Array(transaction_details),
11807            txid: txid.clone(),
11808        };
11809
11810        // Log the exact payload being sent to AMP for debugging
11811        tracing::info!("Sending confirmation payload to AMP:");
11812        tracing::info!("  tx_data.txid: {}", amp_tx_data.txid);
11813        tracing::info!("  tx_data.details: {:?}", amp_tx_data.details);
11814        tracing::info!("  change_data: {} UTXOs", change_data.len());
11815
11816        let confirmation_request = crate::model::ConfirmDistributionRequest {
11817            tx_data: amp_tx_data.clone(),
11818            change_data: change_data.clone(),
11819        };
11820
11821        if let Ok(payload_json) = serde_json::to_string_pretty(&confirmation_request) {
11822            tracing::debug!("Full confirmation payload: {}", payload_json);
11823        }
11824
11825        self.confirm_distribution(
11826            asset_uuid,
11827            &distribution_response.distribution_uuid,
11828            amp_tx_data,
11829            change_data,
11830        )
11831        .await
11832        .map_err(|e| {
11833            tracing::error!("Distribution confirmation failed: {}", e);
11834
11835            // For confirmation failures, always provide retry instructions with txid
11836            let confirmation_error = AmpError::api(format!(
11837                "Failed to confirm distribution {}: {}. \
11838                IMPORTANT: Transaction {} was successful on blockchain. \
11839                Use this txid to manually retry confirmation.",
11840                distribution_response.distribution_uuid, e, txid
11841            ));
11842
11843            if e.is_retryable() {
11844                if let Some(instructions) = e.retry_instructions() {
11845                    tracing::warn!("Retry instructions: {}", instructions);
11846                }
11847            }
11848
11849            confirmation_error.with_context("Step 11: Distribution confirmation")
11850        })?;
11851
11852        tracing::info!(
11853            "🎉 Asset distribution completed successfully for asset: {} with transaction: {}",
11854            asset_uuid,
11855            txid
11856        );
11857
11858        Ok(())
11859    }
11860
11861    /// Validates the asset UUID format
11862    ///
11863    /// Ensures the asset UUID follows the standard UUID format (8-4-4-4-12 hexadecimal digits)
11864    ///
11865    /// # Arguments
11866    /// * `asset_uuid` - The asset UUID string to validate
11867    ///
11868    /// # Returns
11869    /// Returns `Ok(())` if valid, or an error describing the validation failure
11870    ///
11871    /// # Errors
11872    /// - Empty or whitespace-only UUID
11873    /// - Invalid UUID format (not matching standard UUID pattern)
11874    /// - UUID contains invalid characters
11875    fn validate_asset_uuid(asset_uuid: &str) -> Result<(), String> {
11876        if asset_uuid.trim().is_empty() {
11877            return Err("Asset UUID cannot be empty".to_string());
11878        }
11879
11880        // Basic UUID format validation (8-4-4-4-12 pattern)
11881        // Expected format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
11882        let parts: Vec<&str> = asset_uuid.split('-').collect();
11883        if parts.len() != 5 {
11884            return Err(format!(
11885                "Asset UUID '{asset_uuid}' does not have 5 parts separated by hyphens"
11886            ));
11887        }
11888
11889        // Check each part has the correct length and contains only hex characters
11890        let expected_lengths = [8, 4, 4, 4, 12];
11891        for (i, (part, &expected_len)) in parts.iter().zip(expected_lengths.iter()).enumerate() {
11892            if part.len() != expected_len {
11893                return Err(format!(
11894                    "Asset UUID part {} has length {} but expected {}",
11895                    i + 1,
11896                    part.len(),
11897                    expected_len
11898                ));
11899            }
11900
11901            // Check if all characters are valid hexadecimal
11902            if !part.chars().all(|c| c.is_ascii_hexdigit()) {
11903                return Err(format!(
11904                    "Asset UUID part {} contains non-hexadecimal characters: '{}'",
11905                    i + 1,
11906                    part
11907                ));
11908            }
11909        }
11910
11911        tracing::debug!("Asset UUID validation passed: {}", asset_uuid);
11912        Ok(())
11913    }
11914
11915    /// Validates the assignments data structure
11916    ///
11917    /// Ensures assignments vector is not empty and each assignment has valid data
11918    ///
11919    /// # Arguments
11920    /// * `assignments` - Vector of assignments to validate
11921    ///
11922    /// # Returns
11923    /// Returns `Ok(())` if valid, or an error describing the validation failure
11924    ///
11925    /// # Errors
11926    /// - Empty assignments vector
11927    /// - Assignment with empty `user_id`
11928    /// - Assignment with empty address
11929    /// - Assignment with non-positive amount
11930    /// - Assignment with invalid address format
11931    #[allow(clippy::cognitive_complexity)]
11932    fn validate_assignments(assignments: &[AssetDistributionAssignment]) -> Result<(), String> {
11933        tracing::debug!("Validating {} assignments", assignments.len());
11934
11935        if assignments.is_empty() {
11936            tracing::error!("Assignments validation failed: empty assignments vector");
11937            return Err("Assignments vector cannot be empty".to_string());
11938        }
11939
11940        let mut total_amount = 0.0;
11941        let mut unique_addresses = std::collections::HashSet::new();
11942        let mut unique_users = std::collections::HashSet::new();
11943
11944        for (index, assignment) in assignments.iter().enumerate() {
11945            tracing::trace!(
11946                "Validating assignment {}: user_id={}, address={}, amount={}",
11947                index,
11948                assignment.user_id,
11949                assignment.address,
11950                assignment.amount
11951            );
11952
11953            // Validate user_id
11954            if assignment.user_id.trim().is_empty() {
11955                tracing::error!("Assignment {} validation failed: empty user_id", index);
11956                return Err(format!("Assignment {index} has empty user_id"));
11957            }
11958
11959            // Validate address
11960            if assignment.address.trim().is_empty() {
11961                tracing::error!("Assignment {} validation failed: empty address", index);
11962                return Err(format!("Assignment {index} has empty address"));
11963            }
11964
11965            // Basic address format validation (should start with appropriate prefix for Liquid)
11966            if !assignment.address.starts_with("lq")
11967                && !assignment.address.starts_with("vj")
11968                && !assignment.address.starts_with("VJ")
11969                && !assignment.address.starts_with("VT")
11970            {
11971                tracing::error!(
11972                    "Assignment {} validation failed: invalid address format '{}' (should start with 'lq', 'vj', 'VJ', or 'VT')",
11973                    index, assignment.address
11974                );
11975                return Err(format!(
11976                    "Assignment {} has invalid address format: '{}' (should start with 'lq', 'vj', 'VJ', or 'VT')",
11977                    index, assignment.address
11978                ));
11979            }
11980
11981            // Validate amount
11982            if assignment.amount <= 0.0 {
11983                tracing::error!(
11984                    "Assignment {} validation failed: non-positive amount {}",
11985                    index,
11986                    assignment.amount
11987                );
11988                return Err(format!(
11989                    "Assignment {} has non-positive amount: {}",
11990                    index, assignment.amount
11991                ));
11992            }
11993
11994            // Check for reasonable amount limits (prevent overflow issues)
11995            if assignment.amount > 21_000_000.0 {
11996                tracing::error!(
11997                    "Assignment {} validation failed: unreasonably large amount {} (max: 21,000,000)",
11998                    index, assignment.amount
11999                );
12000                return Err(format!(
12001                    "Assignment {} has unreasonably large amount: {} (max: 21,000,000)",
12002                    index, assignment.amount
12003                ));
12004            }
12005
12006            // Check for precision issues (more than 8 decimal places)
12007            let amount_str = format!("{:.8}", assignment.amount);
12008            if amount_str.len() > 20 {
12009                // Reasonable length check
12010                tracing::warn!(
12011                    "Assignment {} has high precision amount: {} - may cause precision issues",
12012                    index,
12013                    assignment.amount
12014                );
12015            }
12016
12017            // Track duplicates for warnings
12018            if !unique_addresses.insert(&assignment.address) {
12019                tracing::warn!(
12020                    "Assignment {} uses duplicate address: {} (this may be intentional)",
12021                    index,
12022                    assignment.address
12023                );
12024            }
12025
12026            if !unique_users.insert(&assignment.user_id) {
12027                tracing::warn!(
12028                    "Assignment {} uses duplicate user_id: {} (this may be intentional)",
12029                    index,
12030                    assignment.user_id
12031                );
12032            }
12033
12034            total_amount += assignment.amount;
12035        }
12036
12037        tracing::debug!(
12038            "Assignments validation passed - {} assignments, total amount: {}, unique addresses: {}, unique users: {}",
12039            assignments.len(),
12040            total_amount,
12041            unique_addresses.len(),
12042            unique_users.len()
12043        );
12044
12045        if total_amount > 100_000_000.0 {
12046            tracing::warn!(
12047                "Total distribution amount is very large: {} - ensure this is intentional",
12048                total_amount
12049            );
12050        }
12051
12052        Ok(())
12053    }
12054
12055    /// Validates `ElementsRpc` connection availability
12056    ///
12057    /// Attempts to connect to the Elements node and verify basic functionality
12058    ///
12059    /// # Arguments
12060    /// * `node_rpc` - `ElementsRpc` client to validate
12061    ///
12062    /// # Returns
12063    /// Returns `Ok(())` if connection is valid, or an error describing the failure
12064    ///
12065    /// # Errors
12066    /// - Cannot connect to Elements node
12067    /// - Node is not synchronized
12068    /// - Node version is incompatible
12069    /// - RPC authentication fails
12070    #[allow(clippy::cognitive_complexity)]
12071    async fn validate_elements_rpc_connection(&self, node_rpc: &ElementsRpc) -> Result<(), String> {
12072        tracing::debug!("Validating Elements RPC connection");
12073
12074        // Test basic connectivity by getting network info
12075        tracing::trace!("Testing Elements RPC connectivity with getnetworkinfo");
12076        let network_info = node_rpc.get_network_info().await.map_err(|e| {
12077            tracing::error!("Failed to get network info from Elements node: {}", e);
12078            format!("Failed to get network info: {e}")
12079        })?;
12080
12081        tracing::debug!(
12082            "Network info retrieved - version: {}, connections: {}, network_active: {}",
12083            network_info.version,
12084            network_info.connections,
12085            network_info.networkactive
12086        );
12087
12088        // Check if network is active
12089        if !network_info.networkactive {
12090            tracing::error!("Elements node network is not active");
12091            return Err("Elements node network is not active".to_string());
12092        }
12093
12094        // Verify we have active connections (for non-regtest environments)
12095        if network_info.connections == 0 {
12096            tracing::warn!("Elements node has no peer connections (may be regtest environment)");
12097        } else {
12098            tracing::debug!(
12099                "Elements node has {} peer connections",
12100                network_info.connections
12101            );
12102        }
12103
12104        // Test blockchain info to ensure node is operational
12105        tracing::trace!("Testing Elements RPC with getblockchaininfo");
12106        let blockchain_info = node_rpc.get_blockchain_info().await.map_err(|e| {
12107            tracing::error!("Failed to get blockchain info from Elements node: {}", e);
12108            format!("Failed to get blockchain info: {e}")
12109        })?;
12110
12111        let sync_progress = blockchain_info.verificationprogress.unwrap_or(1.0) * 100.0;
12112        tracing::debug!(
12113            "Blockchain info retrieved - chain: {}, blocks: {}, sync_progress: {:.2}%",
12114            blockchain_info.chain,
12115            blockchain_info.blocks,
12116            sync_progress
12117        );
12118
12119        // Check if node is still in initial block download
12120        if blockchain_info.initialblockdownload.unwrap_or(false) {
12121            tracing::error!(
12122                "Elements node is still in initial block download (sync progress: {:.2}%)",
12123                sync_progress
12124            );
12125            return Err(format!(
12126                "Elements node is still in initial block download (sync progress: {sync_progress:.2}%)"
12127            ));
12128        }
12129
12130        // Check sync progress
12131        if blockchain_info.verificationprogress.unwrap_or(1.0) < 0.99 {
12132            tracing::warn!(
12133                "Elements node may not be fully synced (sync progress: {:.2}%)",
12134                sync_progress
12135            );
12136        }
12137
12138        // Check for warnings
12139        if !network_info.warnings.is_empty() {
12140            tracing::warn!("Elements node network warnings: {}", network_info.warnings);
12141        }
12142
12143        if let Some(warnings) = &blockchain_info.warnings {
12144            if !warnings.is_empty() {
12145                tracing::warn!("Elements node blockchain warnings: {}", warnings);
12146            }
12147        }
12148
12149        tracing::debug!(
12150            "ElementsRpc connection validation passed - chain: {}, blocks: {}, connections: {}, sync: {:.2}%",
12151            blockchain_info.chain,
12152            blockchain_info.blocks,
12153            network_info.connections,
12154            sync_progress
12155        );
12156
12157        Ok(())
12158    }
12159
12160    /// Validates signer interface availability
12161    ///
12162    /// Tests the signer interface with a dummy transaction to ensure it's functional
12163    ///
12164    /// # Arguments
12165    /// * `signer` - Signer implementation to validate
12166    ///
12167    /// # Returns
12168    /// Returns `Ok(())` if signer is functional, or an error describing the failure
12169    ///
12170    /// # Errors
12171    /// - Signer interface is not responsive
12172    /// - Signer fails basic functionality test
12173    #[allow(clippy::cognitive_complexity)]
12174    async fn validate_signer_interface(&self, signer: &dyn Signer) -> Result<(), String> {
12175        tracing::debug!("Validating signer interface");
12176
12177        // Test signer with a minimal dummy transaction hex
12178        // This is a minimal Elements transaction structure that should parse but not be valid for signing
12179        let dummy_tx = "0200000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000";
12180
12181        tracing::trace!("Testing signer interface with dummy transaction");
12182
12183        // Attempt to sign the dummy transaction - we expect this to fail with a specific error
12184        // but the signer should be responsive and not panic
12185        let validation_start = std::time::Instant::now();
12186        match signer.sign_transaction(dummy_tx).await {
12187            Ok(signed_tx) => {
12188                // Unexpected success with dummy transaction - this might indicate an issue
12189                tracing::warn!(
12190                    "Signer unexpectedly succeeded with dummy transaction (returned: {} chars)",
12191                    signed_tx.len()
12192                );
12193                tracing::debug!("Signer validation passed despite unexpected success");
12194            }
12195            Err(SignerError::InvalidTransaction(msg)) => {
12196                // Expected error - signer is working and correctly identified invalid transaction
12197                tracing::debug!(
12198                    "Signer interface validation passed - correctly rejected dummy transaction: {}",
12199                    msg
12200                );
12201            }
12202            Err(SignerError::HexParse(msg)) => {
12203                // Also acceptable - signer is working and correctly identified parsing issue
12204                tracing::debug!(
12205                    "Signer interface validation passed - correctly identified hex parsing issue: {}",
12206                    msg
12207                );
12208            }
12209            Err(SignerError::Lwk(msg)) => {
12210                // LWK-specific errors might be acceptable depending on the message
12211                if msg.contains("invalid") || msg.contains("parse") || msg.contains("decode") {
12212                    tracing::debug!(
12213                        "Signer interface validation passed - LWK correctly identified invalid transaction: {}",
12214                        msg
12215                    );
12216                } else {
12217                    tracing::error!("Signer interface test failed with LWK error: {}", msg);
12218                    return Err(format!(
12219                        "Signer interface test failed with LWK error: {msg}"
12220                    ));
12221                }
12222            }
12223            Err(e) => {
12224                // Other errors might indicate signer interface issues
12225                tracing::error!("Signer interface test failed: {}", e);
12226                return Err(format!("Signer interface test failed: {e}"));
12227            }
12228        }
12229
12230        let validation_duration = validation_start.elapsed();
12231        tracing::debug!(
12232            "Signer interface validation completed in {:?}",
12233            validation_duration
12234        );
12235
12236        // Warn if signer is very slow (might indicate performance issues)
12237        if validation_duration > std::time::Duration::from_secs(5) {
12238            tracing::warn!(
12239                "Signer interface validation took {:?} - this may indicate performance issues",
12240                validation_duration
12241            );
12242        }
12243
12244        Ok(())
12245    }
12246}
12247
12248fn get_amp_api_base_url() -> Result<Url, Error> {
12249    let url_str = env::var("AMP_API_BASE_URL")
12250        .unwrap_or_else(|_| "https://amp-test.blockstream.com/api".to_string());
12251    Url::parse(&url_str).map_err(Error::from)
12252}
12253
12254/// Creates a token strategy based on automatic environment detection
12255///
12256/// This function detects the current environment and creates the appropriate strategy:
12257/// - Mock strategy for mock environments (isolated, no persistence)
12258/// - Live strategy for live environments (full token management)
12259///
12260/// # Arguments
12261/// * `mock_token` - Optional token to use for mock environments
12262///
12263/// # Errors
12264/// Returns an error if strategy creation fails
12265pub async fn create_auto_token_strategy(
12266    mock_token: Option<String>,
12267) -> Result<Box<dyn TokenStrategy>, Error> {
12268    TokenEnvironment::create_auto_strategy(mock_token).await
12269}
12270
12271/// Creates a mock token strategy with the specified token
12272///
12273/// # Arguments
12274/// * `token` - The mock token to use
12275#[must_use]
12276pub fn create_mock_token_strategy(token: String) -> Box<dyn TokenStrategy> {
12277    Box::new(MockTokenStrategy::new(token))
12278}
12279
12280/// Creates a live token strategy with default configuration
12281///
12282/// # Errors
12283/// Returns an error if the `TokenManager` cannot be initialized
12284pub async fn create_live_token_strategy() -> Result<Box<dyn TokenStrategy>, Error> {
12285    let strategy = LiveTokenStrategy::new().await?;
12286    Ok(Box::new(strategy))
12287}
12288
12289/// Creates a token strategy for the specified environment
12290///
12291/// # Arguments
12292/// * `environment` - The target environment
12293/// * `mock_token` - Optional token to use for mock environments
12294///
12295/// # Errors
12296/// Returns an error if strategy creation fails
12297pub async fn create_token_strategy_for_environment(
12298    environment: TokenEnvironment,
12299    mock_token: Option<String>,
12300) -> Result<Box<dyn TokenStrategy>, Error> {
12301    environment.create_strategy(mock_token).await
12302}
12303
12304#[cfg(test)]
12305mod tests {
12306    use super::*;
12307    use crate::signer::LwkSoftwareSigner;
12308    use tokio;
12309
12310    #[tokio::test]
12311    async fn test_mock_token_strategy_basic_functionality() {
12312        let mock_token = "mock_token_12_345".to_string();
12313        let strategy = MockTokenStrategy::new(mock_token.clone());
12314
12315        // Test get_token returns the mock token
12316        let result = strategy.get_token().await;
12317        assert!(result.is_ok());
12318        assert_eq!(result.unwrap(), mock_token);
12319
12320        // Test strategy type identification
12321        assert_eq!(strategy.strategy_type(), "mock");
12322
12323        // Test persistence is disabled
12324        assert!(!strategy.should_persist());
12325
12326        // Test clear_token is a no-op (should not fail)
12327        let clear_result = strategy.clear_token().await;
12328        assert!(clear_result.is_ok());
12329
12330        // Verify token is still available after clear (since it's a no-op for mock)
12331        let token_after_clear = strategy.get_token().await;
12332        assert!(token_after_clear.is_ok());
12333        assert_eq!(token_after_clear.unwrap(), mock_token);
12334    }
12335
12336    #[tokio::test]
12337    async fn test_mock_token_strategy_isolation() {
12338        let token1 = "token_instance_1".to_string();
12339        let token2 = "token_instance_2".to_string();
12340
12341        let strategy1 = MockTokenStrategy::new(token1.clone());
12342        let strategy2 = MockTokenStrategy::new(token2.clone());
12343
12344        // Test that different instances are isolated
12345        let result1 = strategy1.get_token().await.unwrap();
12346        let result2 = strategy2.get_token().await.unwrap();
12347
12348        assert_eq!(result1, token1);
12349        assert_eq!(result2, token2);
12350        assert_ne!(result1, result2);
12351
12352        // Test that operations on one don't affect the other
12353        let _ = strategy1.clear_token().await;
12354        let result2_after_clear = strategy2.get_token().await.unwrap();
12355        assert_eq!(result2_after_clear, token2);
12356    }
12357
12358    #[tokio::test]
12359    async fn test_live_token_strategy_creation() {
12360        // Test creating a live strategy with global instance
12361        let strategy_result = LiveTokenStrategy::new().await;
12362        assert!(strategy_result.is_ok());
12363
12364        let strategy = strategy_result.unwrap();
12365        assert_eq!(strategy.strategy_type(), "live");
12366        assert!(strategy.should_persist());
12367    }
12368
12369    #[tokio::test]
12370    async fn test_live_token_strategy_with_custom_manager() {
12371        // Create a custom token manager for testing
12372        let config = RetryConfig::for_tests();
12373        let base_url = Url::parse("http://localhost:8080").unwrap();
12374        let mock_token = "test_live_token".to_string();
12375
12376        let token_manager =
12377            Arc::new(TokenManager::with_mock_token(config, base_url, mock_token.clone()).unwrap());
12378
12379        let strategy = LiveTokenStrategy::with_token_manager(token_manager);
12380
12381        // Test strategy properties
12382        assert_eq!(strategy.strategy_type(), "live");
12383        assert!(strategy.should_persist());
12384
12385        // Test token retrieval
12386        let token_result = strategy.get_token().await;
12387        assert!(token_result.is_ok());
12388        assert_eq!(token_result.unwrap(), mock_token);
12389    }
12390
12391    #[tokio::test]
12392    async fn test_live_token_strategy_clear_token() {
12393        // Create a live strategy with a mock token manager
12394        let config = RetryConfig::for_tests();
12395        let base_url = Url::parse("http://localhost:8080").unwrap();
12396        let mock_token = "test_clear_token".to_string();
12397
12398        let token_manager =
12399            Arc::new(TokenManager::with_mock_token(config, base_url, mock_token.clone()).unwrap());
12400
12401        let strategy = LiveTokenStrategy::with_token_manager(token_manager);
12402
12403        // Verify token is available initially
12404        let initial_token = strategy.get_token().await;
12405        assert!(initial_token.is_ok());
12406        assert_eq!(initial_token.unwrap(), mock_token);
12407
12408        // Clear the token
12409        let clear_result = strategy.clear_token().await;
12410        assert!(clear_result.is_ok());
12411
12412        // Note: After clearing, the TokenManager would try to obtain a new token
12413        // In a real scenario, this would fail without proper credentials
12414        // But our mock token manager will still return the same token
12415    }
12416
12417    #[tokio::test]
12418    async fn test_strategy_type_identification() {
12419        let mock_strategy = MockTokenStrategy::new("test_token".to_string());
12420        let live_strategy = LiveTokenStrategy::new().await.unwrap();
12421
12422        // Test that we can identify strategy types for debugging
12423        assert_eq!(mock_strategy.strategy_type(), "mock");
12424        assert_eq!(live_strategy.strategy_type(), "live");
12425
12426        // Test persistence settings
12427        assert!(!mock_strategy.should_persist());
12428        assert!(live_strategy.should_persist());
12429    }
12430
12431    #[tokio::test]
12432    async fn test_strategy_debug_formatting() {
12433        let mock_strategy = MockTokenStrategy::new("debug_test_token".to_string());
12434        let debug_output = format!("{mock_strategy:?}");
12435
12436        // Verify debug output contains expected information
12437        assert!(debug_output.contains("MockTokenStrategy"));
12438        assert!(debug_output.contains("debug_test_token"));
12439    }
12440
12441    // Environment Detection Tests
12442
12443    #[test]
12444    fn test_token_environment_detect_live_via_amp_tests() {
12445        // Set up environment for live test detection
12446        env::set_var("AMP_TESTS", "live");
12447        env::set_var("AMP_USERNAME", "real_user");
12448        env::set_var("AMP_PASSWORD", "real_pass");
12449        env::remove_var("AMP_API_BASE_URL");
12450
12451        let environment = TokenEnvironment::detect();
12452        assert_eq!(environment, TokenEnvironment::Live);
12453
12454        // Clean up
12455        env::remove_var("AMP_TESTS");
12456        env::remove_var("AMP_USERNAME");
12457        env::remove_var("AMP_PASSWORD");
12458    }
12459
12460    #[test]
12461    fn test_token_environment_detect_mock_via_credentials() {
12462        // Set up environment for mock detection via username
12463        env::remove_var("AMP_TESTS");
12464        env::set_var("AMP_USERNAME", "mock_user");
12465        env::set_var("AMP_PASSWORD", "real_pass");
12466        env::remove_var("AMP_API_BASE_URL");
12467
12468        let environment = TokenEnvironment::detect();
12469        assert_eq!(environment, TokenEnvironment::Mock);
12470
12471        // Test mock detection via password
12472        env::set_var("AMP_USERNAME", "real_user");
12473        env::set_var("AMP_PASSWORD", "mock_pass");
12474
12475        let environment = TokenEnvironment::detect();
12476        assert_eq!(environment, TokenEnvironment::Mock);
12477
12478        // Clean up
12479        env::remove_var("AMP_USERNAME");
12480        env::remove_var("AMP_PASSWORD");
12481    }
12482
12483    #[test]
12484    fn test_token_environment_detect_mock_via_base_url() {
12485        // Set up environment for mock detection via localhost URL
12486        env::remove_var("AMP_TESTS");
12487        env::set_var("AMP_USERNAME", "real_user");
12488        env::set_var("AMP_PASSWORD", "real_pass");
12489        env::set_var("AMP_API_BASE_URL", "http://localhost:8080/api");
12490
12491        let environment = TokenEnvironment::detect();
12492        assert_eq!(environment, TokenEnvironment::Mock);
12493
12494        // Test with 127.0.0.1
12495        env::set_var("AMP_API_BASE_URL", "http://127.0.0.1:3000/api");
12496        let environment = TokenEnvironment::detect();
12497        assert_eq!(environment, TokenEnvironment::Mock);
12498
12499        // Test with mock in URL
12500        env::set_var("AMP_API_BASE_URL", "http://mock-server.example.com/api");
12501        let environment = TokenEnvironment::detect();
12502        assert_eq!(environment, TokenEnvironment::Mock);
12503
12504        // Clean up
12505        env::remove_var("AMP_USERNAME");
12506        env::remove_var("AMP_PASSWORD");
12507        env::remove_var("AMP_API_BASE_URL");
12508    }
12509
12510    #[test]
12511    fn test_token_environment_detect_live_via_real_credentials() {
12512        // Set up environment for live detection via real credentials
12513        env::remove_var("AMP_TESTS");
12514        env::set_var("AMP_USERNAME", "real_user");
12515        env::set_var("AMP_PASSWORD", "real_pass");
12516        env::set_var("AMP_API_BASE_URL", "https://amp-test.blockstream.com/api");
12517
12518        let environment = TokenEnvironment::detect();
12519        assert_eq!(environment, TokenEnvironment::Live);
12520
12521        // Clean up
12522        env::remove_var("AMP_USERNAME");
12523        env::remove_var("AMP_PASSWORD");
12524        env::remove_var("AMP_API_BASE_URL");
12525    }
12526
12527    #[test]
12528    fn test_token_environment_detect_mock_fallback() {
12529        // Set up environment with no credentials (fallback to mock)
12530        env::remove_var("AMP_TESTS");
12531        env::remove_var("AMP_USERNAME");
12532        env::remove_var("AMP_PASSWORD");
12533        env::remove_var("AMP_API_BASE_URL");
12534
12535        let environment = TokenEnvironment::detect();
12536        assert_eq!(environment, TokenEnvironment::Mock);
12537    }
12538
12539    #[test]
12540    fn test_has_mock_credentials() {
12541        // Test mock username detection
12542        assert!(TokenEnvironment::has_mock_credentials(
12543            "mock_user",
12544            "real_pass",
12545            ""
12546        ));
12547        assert!(TokenEnvironment::has_mock_credentials(
12548            "Mock_User",
12549            "real_pass",
12550            ""
12551        ));
12552        assert!(TokenEnvironment::has_mock_credentials(
12553            "user_mock",
12554            "real_pass",
12555            ""
12556        ));
12557
12558        // Test mock password detection
12559        assert!(TokenEnvironment::has_mock_credentials(
12560            "real_user",
12561            "mock_pass",
12562            ""
12563        ));
12564        assert!(TokenEnvironment::has_mock_credentials(
12565            "real_user",
12566            "Mock_Pass",
12567            ""
12568        ));
12569        assert!(TokenEnvironment::has_mock_credentials(
12570            "real_user",
12571            "pass_mock",
12572            ""
12573        ));
12574
12575        // Test mock URL detection
12576        assert!(TokenEnvironment::has_mock_credentials(
12577            "real_user",
12578            "real_pass",
12579            "http://localhost:8080"
12580        ));
12581        assert!(TokenEnvironment::has_mock_credentials(
12582            "real_user",
12583            "real_pass",
12584            "http://127.0.0.1:3000"
12585        ));
12586        assert!(TokenEnvironment::has_mock_credentials(
12587            "real_user",
12588            "real_pass",
12589            "http://mock-server.com"
12590        ));
12591        assert!(TokenEnvironment::has_mock_credentials(
12592            "real_user",
12593            "real_pass",
12594            "http://Mock-Server.com"
12595        ));
12596
12597        // Test non-mock credentials
12598        assert!(!TokenEnvironment::has_mock_credentials(
12599            "real_user",
12600            "real_pass",
12601            "https://amp-test.blockstream.com"
12602        ));
12603        assert!(!TokenEnvironment::has_mock_credentials("", "", ""));
12604    }
12605
12606    #[test]
12607    fn test_token_environment_should_persist_tokens() {
12608        assert!(!TokenEnvironment::Mock.should_persist_tokens());
12609        assert!(TokenEnvironment::Live.should_persist_tokens());
12610
12611        // Auto should delegate to detect()
12612        env::set_var("AMP_TESTS", "live");
12613        assert!(TokenEnvironment::Auto.should_persist_tokens());
12614
12615        env::set_var("AMP_USERNAME", "mock_user");
12616        env::set_var("AMP_PASSWORD", "some_password");
12617        env::remove_var("AMP_TESTS");
12618        env::remove_var("AMP_API_BASE_URL");
12619        assert!(!TokenEnvironment::Auto.should_persist_tokens());
12620
12621        // Clean up
12622        env::remove_var("AMP_USERNAME");
12623        env::remove_var("AMP_PASSWORD");
12624    }
12625
12626    #[test]
12627    fn test_token_environment_is_mock_and_is_live() {
12628        assert!(TokenEnvironment::Mock.is_mock());
12629        assert!(!TokenEnvironment::Mock.is_live());
12630
12631        assert!(!TokenEnvironment::Live.is_mock());
12632        assert!(TokenEnvironment::Live.is_live());
12633
12634        // Auto should delegate to detect()
12635        env::set_var("AMP_USERNAME", "mock_user");
12636        env::set_var("AMP_PASSWORD", "some_password");
12637        env::remove_var("AMP_TESTS");
12638        env::remove_var("AMP_API_BASE_URL");
12639        assert!(TokenEnvironment::Auto.is_mock());
12640        assert!(!TokenEnvironment::Auto.is_live());
12641
12642        env::set_var("AMP_TESTS", "live");
12643        assert!(!TokenEnvironment::Auto.is_mock());
12644        assert!(TokenEnvironment::Auto.is_live());
12645
12646        // Clean up
12647        env::remove_var("AMP_USERNAME");
12648        env::remove_var("AMP_PASSWORD");
12649        env::remove_var("AMP_TESTS");
12650    }
12651
12652    #[tokio::test]
12653    async fn test_token_environment_create_strategy_mock() {
12654        let mock_token = "test_mock_token".to_string();
12655        let strategy = TokenEnvironment::Mock
12656            .create_strategy(Some(mock_token.clone()))
12657            .await
12658            .unwrap();
12659
12660        assert_eq!(strategy.strategy_type(), "mock");
12661        assert!(!strategy.should_persist());
12662
12663        let token = strategy.get_token().await.unwrap();
12664        assert_eq!(token, mock_token);
12665    }
12666
12667    #[tokio::test]
12668    async fn test_token_environment_create_strategy_live() {
12669        let strategy = TokenEnvironment::Live.create_strategy(None).await.unwrap();
12670
12671        assert_eq!(strategy.strategy_type(), "live");
12672        assert!(strategy.should_persist());
12673    }
12674
12675    #[tokio::test]
12676    async fn test_token_environment_create_auto_strategy() {
12677        // Test with mock environment - need both username and password for proper detection
12678        env::set_var("AMP_USERNAME", "mock_user");
12679        env::set_var("AMP_PASSWORD", "some_password");
12680        env::remove_var("AMP_TESTS");
12681        env::remove_var("AMP_API_BASE_URL");
12682
12683        let mock_token = "auto_mock_token".to_string();
12684        let strategy = TokenEnvironment::create_auto_strategy(Some(mock_token.clone()))
12685            .await
12686            .unwrap();
12687
12688        assert_eq!(strategy.strategy_type(), "mock");
12689        let token = strategy.get_token().await.unwrap();
12690        assert_eq!(token, mock_token);
12691
12692        // Clean up
12693        env::remove_var("AMP_USERNAME");
12694        env::remove_var("AMP_PASSWORD");
12695    }
12696
12697    #[tokio::test]
12698    async fn test_mock_token_strategy_factory_methods() {
12699        // Test with_default_token
12700        let strategy = MockTokenStrategy::with_default_token();
12701        assert_eq!(strategy.strategy_type(), "mock");
12702        let token = strategy.get_token().await.unwrap();
12703        assert_eq!(token, "mock_token_default");
12704
12705        // Test for_test
12706        let strategy = MockTokenStrategy::for_test("my_test");
12707        let token = strategy.get_token().await.unwrap();
12708        assert_eq!(token, "mock_token_my_test");
12709    }
12710
12711    #[tokio::test]
12712    async fn test_live_token_strategy_factory_methods() {
12713        // Test for_testing
12714        let strategy = LiveTokenStrategy::for_testing().await.unwrap();
12715        assert_eq!(strategy.strategy_type(), "live");
12716        assert!(strategy.should_persist());
12717    }
12718
12719    #[tokio::test]
12720    async fn test_standalone_factory_functions() {
12721        // Test create_mock_token_strategy
12722        let mock_token = "standalone_mock".to_string();
12723        let strategy = create_mock_token_strategy(mock_token.clone());
12724        assert_eq!(strategy.strategy_type(), "mock");
12725        let token = strategy.get_token().await.unwrap();
12726        assert_eq!(token, mock_token);
12727
12728        // Test create_live_token_strategy
12729        let strategy = create_live_token_strategy().await.unwrap();
12730        assert_eq!(strategy.strategy_type(), "live");
12731
12732        // Test create_auto_token_strategy with mock environment
12733        env::set_var("AMP_USERNAME", "mock_user");
12734        env::set_var("AMP_PASSWORD", "some_password");
12735        env::remove_var("AMP_TESTS");
12736        env::remove_var("AMP_API_BASE_URL");
12737
12738        let auto_mock_token = "auto_standalone_mock".to_string();
12739        let strategy = create_auto_token_strategy(Some(auto_mock_token.clone()))
12740            .await
12741            .unwrap();
12742        assert_eq!(strategy.strategy_type(), "mock");
12743        let token = strategy.get_token().await.unwrap();
12744        assert_eq!(token, auto_mock_token);
12745
12746        // Test create_token_strategy_for_environment
12747        let env_mock_token = "env_mock".to_string();
12748        let strategy = create_token_strategy_for_environment(
12749            TokenEnvironment::Mock,
12750            Some(env_mock_token.clone()),
12751        )
12752        .await
12753        .unwrap();
12754        assert_eq!(strategy.strategy_type(), "mock");
12755        let token = strategy.get_token().await.unwrap();
12756        assert_eq!(token, env_mock_token);
12757
12758        // Clean up
12759        env::remove_var("AMP_USERNAME");
12760        env::remove_var("AMP_PASSWORD");
12761    }
12762
12763    #[test]
12764    fn test_environment_detection_with_various_credential_combinations() {
12765        // Test case 1: AMP_TESTS=live overrides everything
12766        env::set_var("AMP_TESTS", "live");
12767        env::set_var("AMP_USERNAME", "mock_user");
12768        env::set_var("AMP_PASSWORD", "mock_pass");
12769        env::set_var("AMP_API_BASE_URL", "http://localhost:8080");
12770        assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Live);
12771
12772        // Test case 2: Mock username with real password and URL
12773        env::remove_var("AMP_TESTS");
12774        env::set_var("AMP_USERNAME", "mock_user");
12775        env::set_var("AMP_PASSWORD", "real_password");
12776        env::set_var("AMP_API_BASE_URL", "https://amp-test.blockstream.com/api");
12777        assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Mock);
12778
12779        // Test case 3: Real username with mock password
12780        env::set_var("AMP_USERNAME", "real_user");
12781        env::set_var("AMP_PASSWORD", "mock_password");
12782        env::set_var("AMP_API_BASE_URL", "https://amp-test.blockstream.com/api");
12783        assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Mock);
12784
12785        // Test case 4: Real credentials with localhost URL
12786        env::set_var("AMP_USERNAME", "real_user");
12787        env::set_var("AMP_PASSWORD", "real_password");
12788        env::set_var("AMP_API_BASE_URL", "http://localhost:3000/api");
12789        assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Mock);
12790
12791        // Test case 5: All real credentials
12792        env::set_var("AMP_USERNAME", "real_user");
12793        env::set_var("AMP_PASSWORD", "real_password");
12794        env::set_var("AMP_API_BASE_URL", "https://amp-test.blockstream.com/api");
12795        assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Live);
12796
12797        // Test case 6: Empty credentials
12798        env::remove_var("AMP_USERNAME");
12799        env::remove_var("AMP_PASSWORD");
12800        env::remove_var("AMP_API_BASE_URL");
12801        assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Mock);
12802
12803        // Test case 7: Only username set
12804        env::set_var("AMP_USERNAME", "real_user");
12805        env::remove_var("AMP_PASSWORD");
12806        assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Mock);
12807
12808        // Test case 8: Only password set
12809        env::remove_var("AMP_USERNAME");
12810        env::set_var("AMP_PASSWORD", "real_password");
12811        assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Mock);
12812
12813        // Clean up all environment variables
12814        env::remove_var("AMP_TESTS");
12815        env::remove_var("AMP_USERNAME");
12816        env::remove_var("AMP_PASSWORD");
12817        env::remove_var("AMP_API_BASE_URL");
12818    }
12819
12820    #[tokio::test]
12821    async fn test_distribute_asset_input_validation() {
12822        // Create a mock client for testing
12823        let client = ApiClient::with_mock_token(
12824            reqwest::Url::parse("http://localhost:8080/api").unwrap(),
12825            "test_token".to_string(),
12826        )
12827        .unwrap();
12828
12829        // Test invalid asset UUID
12830        let assignments = vec![AssetDistributionAssignment {
12831            user_id: "user123".to_string(),
12832            address: "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
12833            amount: 100.0,
12834        }];
12835
12836        // Create a mock ElementsRpc (this will fail connection validation, but that's expected)
12837        let elements_rpc = ElementsRpc::new(
12838            "http://localhost:18884".to_string(),
12839            "user".to_string(),
12840            "pass".to_string(),
12841        );
12842
12843        // Create a mock signer
12844        let (_, signer) = LwkSoftwareSigner::generate_new().unwrap();
12845
12846        // Test with invalid UUID format
12847        let result = client
12848            .distribute_asset(
12849                "invalid-uuid",
12850                assignments.clone(),
12851                &elements_rpc,
12852                "test_wallet",
12853                &signer,
12854            )
12855            .await;
12856
12857        assert!(result.is_err());
12858        if let Err(AmpError::Validation(msg)) = result {
12859            assert!(msg.contains("Invalid asset UUID"));
12860        } else {
12861            panic!("Expected validation error for invalid UUID");
12862        }
12863
12864        // Test with empty assignments
12865        let result = client
12866            .distribute_asset(
12867                "550e8400-e29b-41d4-a716-446655440000",
12868                vec![],
12869                &elements_rpc,
12870                "test_wallet",
12871                &signer,
12872            )
12873            .await;
12874
12875        assert!(result.is_err());
12876        if let Err(AmpError::Validation(msg)) = result {
12877            assert!(msg.contains("Invalid assignments"));
12878        } else {
12879            panic!("Expected validation error for empty assignments");
12880        }
12881    }
12882
12883    #[test]
12884    fn test_validate_asset_uuid() {
12885        let _client = ApiClient::with_mock_token(
12886            reqwest::Url::parse("http://localhost:8080/api").unwrap(),
12887            "test_token".to_string(),
12888        )
12889        .unwrap();
12890
12891        // Valid UUID
12892        assert!(ApiClient::validate_asset_uuid("550e8400-e29b-41d4-a716-446655440000").is_ok());
12893
12894        // Invalid UUIDs
12895        assert!(ApiClient::validate_asset_uuid("").is_err());
12896        assert!(ApiClient::validate_asset_uuid("invalid").is_err());
12897        assert!(ApiClient::validate_asset_uuid("550e8400-e29b-41d4-a716").is_err()); // Too short
12898        assert!(
12899            ApiClient::validate_asset_uuid("550e8400-e29b-41d4-a716-446655440000-extra").is_err()
12900        ); // Too long
12901        assert!(ApiClient::validate_asset_uuid("550e8400xe29bx41d4xa716x446655440000").is_err()); // Wrong separators
12902        assert!(ApiClient::validate_asset_uuid("550e8400-e29g-41d4-a716-446655440000").is_err());
12903        // Invalid hex char
12904    }
12905
12906    #[test]
12907    fn test_validate_assignments() {
12908        let _client = ApiClient::with_mock_token(
12909            reqwest::Url::parse("http://localhost:8080/api").unwrap(),
12910            "test_token".to_string(),
12911        )
12912        .unwrap();
12913
12914        // Valid assignments
12915        let valid_assignments = vec![AssetDistributionAssignment {
12916            user_id: "user123".to_string(),
12917            address: "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
12918            amount: 100.0,
12919        }];
12920        assert!(ApiClient::validate_assignments(&valid_assignments).is_ok());
12921
12922        // Empty assignments
12923        assert!(ApiClient::validate_assignments(&[]).is_err());
12924
12925        // Assignment with empty user_id
12926        let invalid_assignments = vec![AssetDistributionAssignment {
12927            user_id: "".to_string(),
12928            address: "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
12929            amount: 100.0,
12930        }];
12931        assert!(ApiClient::validate_assignments(&invalid_assignments).is_err());
12932
12933        // Assignment with empty address
12934        let invalid_assignments = vec![AssetDistributionAssignment {
12935            user_id: "user123".to_string(),
12936            address: "".to_string(),
12937            amount: 100.0,
12938        }];
12939        assert!(ApiClient::validate_assignments(&invalid_assignments).is_err());
12940
12941        // Assignment with invalid address format
12942        let invalid_assignments = vec![AssetDistributionAssignment {
12943            user_id: "user123".to_string(),
12944            address: "invalid_address".to_string(),
12945            amount: 100.0,
12946        }];
12947        assert!(ApiClient::validate_assignments(&invalid_assignments).is_err());
12948
12949        // Assignment with non-positive amount
12950        let invalid_assignments = vec![AssetDistributionAssignment {
12951            user_id: "user123".to_string(),
12952            address: "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
12953            amount: 0.0,
12954        }];
12955        assert!(ApiClient::validate_assignments(&invalid_assignments).is_err());
12956
12957        // Assignment with unreasonably large amount
12958        let invalid_assignments = vec![AssetDistributionAssignment {
12959            user_id: "user123".to_string(),
12960            address: "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
12961            amount: 25_000_000.0,
12962        }];
12963        assert!(ApiClient::validate_assignments(&invalid_assignments).is_err());
12964    }
12965
12966    #[test]
12967    fn test_enhanced_error_handling_and_logging() {
12968        // Test AmpError creation and context enhancement
12969        let api_error = AmpError::api("Distribution creation failed");
12970        let contextual_error = api_error.with_context("Step 6: Distribution creation");
12971
12972        match contextual_error {
12973            AmpError::Api(msg) => {
12974                assert!(msg.contains("Step 6: Distribution creation"));
12975                assert!(msg.contains("Distribution creation failed"));
12976            }
12977            _ => panic!("Expected Api error variant"),
12978        }
12979
12980        // Test retry instructions for different error types
12981        let rpc_error = AmpError::rpc("Connection failed");
12982        assert!(rpc_error.is_retryable());
12983        assert!(rpc_error.retry_instructions().is_some());
12984        assert!(rpc_error
12985            .retry_instructions()
12986            .unwrap()
12987            .contains("Elements node"));
12988
12989        let validation_error = AmpError::validation("Invalid UUID");
12990        assert!(!validation_error.is_retryable());
12991        assert!(validation_error.retry_instructions().is_none());
12992
12993        let timeout_error = AmpError::timeout("Confirmation timeout for txid abc123");
12994        assert!(!timeout_error.is_retryable());
12995        let instructions = timeout_error.retry_instructions();
12996        assert!(instructions.is_some());
12997        assert!(instructions.unwrap().contains("transaction ID"));
12998
12999        // Test error helper methods
13000        let signer_error =
13001            AmpError::Signer(crate::signer::SignerError::Lwk("Test error".to_string()));
13002        assert!(!signer_error.is_retryable());
13003        assert!(signer_error.retry_instructions().is_none());
13004
13005        // Test serialization error
13006        let json_error = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
13007        let serialization_error = AmpError::from(json_error);
13008        assert!(matches!(serialization_error, AmpError::Serialization(_)));
13009        assert!(!serialization_error.is_retryable());
13010    }
13011}