Skip to main content

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, Clone)]
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    /// Reissues an asset using the Elements RPC reissueasset command
2622    ///
2623    /// This method reissues the specified amount of an asset.
2624    /// It requires the asset to be reissuable and the reissuance token to be available.
2625    ///
2626    /// # Arguments
2627    /// * `asset_id` - The asset ID (hex string) to reissue
2628    /// * `amount` - The amount to reissue (in satoshis for the asset)
2629    ///
2630    /// # Returns
2631    /// Returns a JSON value containing the reissuance output with txid and vin fields
2632    ///
2633    /// # Errors
2634    /// Returns an error if:
2635    /// - The asset ID is invalid
2636    /// - The asset is not reissuable
2637    /// - The reissuance token is not available
2638    /// - The RPC call fails
2639    ///
2640    /// # Examples
2641    /// ```no_run
2642    /// # use amp_rs::ElementsRpc;
2643    /// # #[tokio::main]
2644    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2645    /// let rpc = ElementsRpc::from_env()?;
2646    /// let asset_id = "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d";
2647    /// let amount = 1000000.0; // 0.01 of an asset with 8 decimals
2648    /// let result = rpc.reissueasset(asset_id, amount).await?;
2649    /// println!("Reissuance txid: {}, vin: {}", result["txid"], result["vin"]);
2650    /// # Ok(())
2651    /// # }
2652    /// ```
2653    pub async fn reissueasset(
2654        &self,
2655        asset_id: &str,
2656        amount: f64,
2657    ) -> Result<serde_json::Value, AmpError> {
2658        tracing::debug!("Reissuing asset {} with amount {}", asset_id, amount);
2659
2660        let params = serde_json::json!([asset_id, amount]);
2661
2662        let result: serde_json::Value = self
2663            .rpc_call("reissueasset", params)
2664            .await
2665            .map_err(|e| {
2666                e.with_context(format!(
2667                    "Failed to reissue asset {asset_id}. \
2668                    Ensure the asset is reissuable and the reissuance token is available in the wallet."
2669                ))
2670            })?;
2671
2672        // Extract txid and vin from result for logging
2673        let txid = result
2674            .get("txid")
2675            .and_then(|v| v.as_str())
2676            .unwrap_or("unknown");
2677        let vin = result
2678            .get("vin")
2679            .and_then(serde_json::Value::as_u64)
2680            .unwrap_or(0);
2681
2682        tracing::info!("Reissuance transaction created: txid={}, vin={}", txid, vin);
2683
2684        Ok(result)
2685    }
2686
2687    /// Lists all issuances for a specific asset or all assets
2688    ///
2689    /// This method retrieves issuance information including initial issuances
2690    /// and reissuances. If an `asset_id` is provided, only issuances for that
2691    /// asset are returned.
2692    ///
2693    /// # Arguments
2694    /// * `asset_id` - Optional asset ID to filter issuances by. If None, returns all issuances
2695    ///
2696    /// # Returns
2697    /// Returns a vector of JSON values, each containing issuance information
2698    ///
2699    /// # Errors
2700    /// Returns an error if the RPC call fails
2701    ///
2702    /// # Examples
2703    /// ```no_run
2704    /// # use amp_rs::ElementsRpc;
2705    /// # #[tokio::main]
2706    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2707    /// let rpc = ElementsRpc::from_env()?;
2708    /// let asset_id = "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d";
2709    /// let issuances = rpc.list_issuances(Some(asset_id)).await?;
2710    /// for issuance in issuances {
2711    ///     if let Some(is_reissuance) = issuance.get("isreissuance").and_then(|v| v.as_bool()) {
2712    ///         println!("Reissuance: {}", is_reissuance);
2713    ///     }
2714    /// }
2715    /// # Ok(())
2716    /// # }
2717    /// ```
2718    pub async fn list_issuances(
2719        &self,
2720        asset_id: Option<&str>,
2721    ) -> Result<Vec<serde_json::Value>, AmpError> {
2722        tracing::debug!("Listing issuances for asset: {:?}", asset_id);
2723
2724        let params = asset_id.map_or_else(
2725            || serde_json::Value::Array(vec![]),
2726            |asset| serde_json::json!([asset]),
2727        );
2728
2729        let issuances: Vec<serde_json::Value> = self
2730            .rpc_call("listissuances", params)
2731            .await
2732            .map_err(|e| e.with_context("Failed to list issuances"))?;
2733
2734        tracing::debug!("Found {} issuances", issuances.len());
2735
2736        Ok(issuances)
2737    }
2738
2739    /// Destroys (burns) a specific amount of an asset
2740    ///
2741    /// This method calls the Elements node's `destroyamount` RPC to permanently
2742    /// remove (burn) a specified amount of an asset from the wallet.
2743    ///
2744    /// # Arguments
2745    /// * `asset_id` - The asset ID to burn
2746    /// * `amount` - The amount to burn (as a floating point number)
2747    ///
2748    /// # Returns
2749    /// Returns a JSON value containing the transaction ID of the burn transaction
2750    ///
2751    /// # Errors
2752    /// Returns an error if the RPC call fails or if insufficient balance exists
2753    ///
2754    /// # Examples
2755    /// ```no_run
2756    /// # use amp_rs::ElementsRpc;
2757    /// # #[tokio::main]
2758    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2759    /// let rpc = ElementsRpc::from_env()?;
2760    /// let asset_id = "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d";
2761    /// let amount = 1000.0; // Burn 1000 units
2762    ///
2763    /// let txid = rpc.destroyamount(asset_id, amount).await?;
2764    /// println!("Burn transaction created: {}", txid);
2765    /// # Ok(())
2766    /// # }
2767    /// ```
2768    pub async fn destroyamount(&self, asset_id: &str, amount: f64) -> Result<String, AmpError> {
2769        tracing::debug!("Burning asset {} with amount {}", asset_id, amount);
2770
2771        let params = serde_json::json!([asset_id, amount]);
2772
2773        let result: String = self.rpc_call("destroyamount", params).await.map_err(|e| {
2774            e.with_context(format!(
2775                "Failed to burn asset {asset_id}. \
2776                    Ensure sufficient balance exists in the wallet."
2777            ))
2778        })?;
2779
2780        tracing::info!("Burn transaction created: txid={}", result);
2781
2782        Ok(result)
2783    }
2784
2785    /// Gets the balance for all assets or a specific asset
2786    ///
2787    /// This method calls the Elements node's `getbalance` RPC to retrieve
2788    /// the wallet balance. If an `asset_id` is provided, returns the balance
2789    /// for that specific asset. If None, returns balances for all assets.
2790    ///
2791    /// # Arguments
2792    /// * `asset_id` - Optional asset ID to get balance for. If None, returns all asset balances
2793    ///
2794    /// # Returns
2795    /// Returns a JSON value containing asset balances (as a map of `asset_id` -> balance)
2796    ///
2797    /// # Errors
2798    /// Returns an error if the RPC call fails
2799    ///
2800    /// # Examples
2801    /// ```no_run
2802    /// # use amp_rs::ElementsRpc;
2803    /// # #[tokio::main]
2804    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2805    /// let rpc = ElementsRpc::from_env()?;
2806    /// let asset_id = "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d";
2807    ///
2808    /// let balances = rpc.get_balance(None).await?;
2809    /// if let Some(balance) = balances.get(asset_id) {
2810    ///     println!("Balance for asset {}: {}", asset_id, balance);
2811    /// }
2812    /// # Ok(())
2813    /// # }
2814    /// ```
2815    pub async fn get_balance(&self, asset_id: Option<&str>) -> Result<serde_json::Value, AmpError> {
2816        tracing::debug!("Getting balance for asset: {:?}", asset_id);
2817
2818        // getbalance RPC signature: getbalance ( "dummy" minconf include_watchonly )
2819        // We use "*" as the account, 0 minconf, false for include_watchonly
2820        let params = serde_json::json!(["*", 0, false]);
2821
2822        let balances: serde_json::Value = self
2823            .rpc_call("getbalance", params)
2824            .await
2825            .map_err(|e| e.with_context("Failed to get balance"))?;
2826
2827        // If asset_id is specified, return just that balance
2828        if let Some(asset_id) = asset_id {
2829            if let Some(balance) = balances.get(asset_id) {
2830                return Ok(balance.clone());
2831            }
2832            return Ok(serde_json::json!(0.0));
2833        }
2834
2835        tracing::debug!(
2836            "Retrieved balances for {} assets",
2837            balances.as_object().map_or(0, serde_json::Map::len)
2838        );
2839
2840        Ok(balances)
2841    }
2842
2843    /// Selects appropriate UTXOs to cover the required amount plus fees
2844    ///
2845    /// This method implements a simple UTXO selection algorithm that:
2846    /// 1. Filters UTXOs by asset ID and spendability
2847    /// 2. Sorts UTXOs by amount (largest first) for efficiency
2848    /// 3. Selects UTXOs until the target amount plus estimated fees is covered
2849    ///
2850    /// # Arguments
2851    /// * `asset_id` - The asset ID to select UTXOs for
2852    /// * `target_amount` - The total amount needed for distribution
2853    /// * `estimated_fee` - Estimated transaction fee in the same asset
2854    ///
2855    /// # Returns
2856    /// Returns a tuple of (`selected_utxos`, `total_selected_amount`)
2857    ///
2858    /// # Errors
2859    /// Returns an error if insufficient UTXOs are available or RPC calls fail
2860    ///
2861    /// # Examples
2862    /// ```no_run
2863    /// # use amp_rs::ElementsRpc;
2864    /// # #[tokio::main]
2865    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2866    /// let rpc = ElementsRpc::from_env()?;
2867    /// let (selected_utxos, total_amount) = rpc.select_utxos_for_amount(
2868    ///     "wallet_name",
2869    ///     "asset_id_hex",
2870    ///     150.0,
2871    ///     0.001
2872    /// ).await?;
2873    /// println!("Selected {} UTXOs totaling {}", selected_utxos.len(), total_amount);
2874    /// # Ok(())
2875    /// # }
2876    /// ```
2877    pub async fn select_utxos_for_amount(
2878        &self,
2879        wallet_name: &str,
2880        asset_id: &str,
2881        target_amount: f64,
2882        estimated_fee: f64,
2883    ) -> Result<(Vec<Unspent>, f64), AmpError> {
2884        tracing::debug!(
2885            "Selecting UTXOs for asset {} from wallet {} - target: {}, fee: {}",
2886            asset_id,
2887            wallet_name,
2888            target_amount,
2889            estimated_fee
2890        );
2891
2892        // Get all UTXOs for this asset from the specified wallet
2893        let mut utxos = self
2894            .list_unspent_for_wallet(wallet_name, Some(asset_id))
2895            .await?;
2896
2897        // Filter for spendable UTXOs only
2898        utxos.retain(|utxo| utxo.spendable && utxo.asset == asset_id);
2899
2900        if utxos.is_empty() {
2901            return Err(AmpError::validation(format!(
2902                "No spendable UTXOs found for asset {asset_id}. \
2903                This typically means:\n\
2904                1. The treasury address is not imported in the Elements node as a watch-only address\n\
2905                2. The asset issuance transaction hasn't been confirmed yet\n\
2906                3. The UTXOs have already been spent\n\
2907                \n\
2908                To fix this:\n\
2909                - Ensure the treasury address is imported: `elements-cli importaddress <treasury_address> treasury false`\n\
2910                - Wait for the asset issuance transaction to be confirmed\n\
2911                - Check that the treasury address matches the one used for asset issuance"
2912            )));
2913        }
2914
2915        // Sort UTXOs by amount (largest first) for efficient selection
2916        utxos.sort_by(|a, b| {
2917            b.amount
2918                .partial_cmp(&a.amount)
2919                .unwrap_or(std::cmp::Ordering::Equal)
2920        });
2921
2922        let required_amount = target_amount + estimated_fee;
2923        let mut selected_utxos = Vec::new();
2924        let mut total_selected = 0.0;
2925
2926        // Select UTXOs until we have enough to cover the required amount
2927        for utxo in utxos {
2928            selected_utxos.push(utxo.clone());
2929            total_selected += utxo.amount;
2930
2931            if total_selected >= required_amount {
2932                break;
2933            }
2934        }
2935
2936        // Check if we have sufficient funds
2937        if total_selected < required_amount {
2938            return Err(AmpError::validation(format!(
2939                "Insufficient UTXOs: need {required_amount}, have {total_selected} (target: {target_amount}, fee: {estimated_fee})"
2940            )));
2941        }
2942
2943        tracing::info!(
2944            "Selected {} UTXOs totaling {} for target {} + fee {}",
2945            selected_utxos.len(),
2946            total_selected,
2947            target_amount,
2948            estimated_fee
2949        );
2950
2951        Ok((selected_utxos, total_selected))
2952    }
2953
2954    /// Builds a raw transaction for asset distribution with proper change handling
2955    ///
2956    /// This method orchestrates the complete transaction building process:
2957    /// 1. Selects appropriate UTXOs using `select_utxos_for_amount`
2958    /// 2. Creates transaction inputs from selected UTXOs
2959    /// 3. Creates outputs for distribution addresses
2960    /// 4. Calculates and creates change output if necessary
2961    /// 5. Builds the raw transaction using `create_raw_transaction`
2962    ///
2963    /// # Arguments
2964    /// * `asset_id` - The asset ID being distributed
2965    /// * `address_amounts` - Map of recipient addresses to amounts
2966    /// * `change_address` - Address to send change to (if any)
2967    /// * `estimated_fee` - Estimated transaction fee
2968    ///
2969    /// # Returns
2970    /// Returns a tuple of (`raw_transaction_hex`, `selected_utxos`, `change_amount`)
2971    ///
2972    /// # Errors
2973    /// Returns an error if UTXO selection fails or transaction building fails
2974    ///
2975    /// # Examples
2976    /// ```no_run
2977    /// # use amp_rs::ElementsRpc;
2978    /// # use std::collections::HashMap;
2979    /// # #[tokio::main]
2980    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2981    /// let rpc = ElementsRpc::from_env()?;
2982    /// let mut address_amounts = HashMap::new();
2983    /// address_amounts.insert("address1".to_string(), 100.0);
2984    /// address_amounts.insert("address2".to_string(), 50.0);
2985    ///
2986    /// let (raw_tx, utxos, change) = rpc.build_distribution_transaction(
2987    ///     "wallet_name",
2988    ///     "asset_id_hex",
2989    ///     address_amounts,
2990    ///     "change_address",
2991    ///     0.001
2992    /// ).await?;
2993    /// println!("Built transaction with {} inputs, change: {}", utxos.len(), change);
2994    /// # Ok(())
2995    /// # }
2996    /// ```
2997    #[allow(clippy::cognitive_complexity)]
2998    #[allow(clippy::too_many_lines)]
2999    pub async fn build_distribution_transaction(
3000        &self,
3001        wallet_name: &str,
3002        asset_id: &str,
3003        address_amounts: std::collections::HashMap<String, f64>,
3004        change_address: &str,
3005        _estimated_fee: f64,
3006    ) -> Result<(String, Vec<Unspent>, f64), AmpError> {
3007        const DUST_THRESHOLD: f64 = 0.00001;
3008        const LBTC_ASSET_ID: &str =
3009            "144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49"; // L-BTC on Liquid testnet
3010
3011        tracing::debug!(
3012            "Building distribution transaction for asset {} with {} outputs",
3013            asset_id,
3014            address_amounts.len()
3015        );
3016
3017        // Calculate total distribution amount
3018        let total_distribution: f64 = address_amounts.values().sum();
3019
3020        if total_distribution <= 0.0 {
3021            return Err(AmpError::validation(
3022                "Total distribution amount must be greater than zero".to_string(),
3023            ));
3024        }
3025
3026        // Select UTXOs to cover the distribution (custom asset)
3027        let (selected_asset_utxos, total_selected) = self
3028            .select_utxos_for_amount(wallet_name, asset_id, total_distribution, 0.0)
3029            .await?;
3030
3031        // Also select L-BTC UTXOs for transaction fees
3032        // Elements requires L-BTC inputs for fees even when distributing custom assets
3033        let min_lbtc_fee = 0.00001; // Minimum L-BTC needed for fees
3034        let (selected_lbtc_utxos, lbtc_total) = match self
3035            .select_utxos_for_amount(wallet_name, LBTC_ASSET_ID, 0.0, min_lbtc_fee)
3036            .await
3037        {
3038            Ok((utxos, total)) => {
3039                tracing::info!(
3040                    "Selected {} L-BTC UTXOs totaling {} for fees",
3041                    utxos.len(),
3042                    total
3043                );
3044                (utxos, total)
3045            }
3046            Err(e) => {
3047                tracing::warn!(
3048                    "Could not select L-BTC UTXOs for fees: {}. Transaction may fail.",
3049                    e
3050                );
3051                (Vec::new(), 0.0)
3052            }
3053        };
3054
3055        // Combine custom asset UTXOs and L-BTC UTXOs
3056        let mut all_utxos = selected_asset_utxos.clone();
3057        all_utxos.extend(selected_lbtc_utxos.clone());
3058
3059        if selected_lbtc_utxos.is_empty() {
3060            tracing::warn!(
3061                "No L-BTC UTXOs selected for fees. Transaction may fail during broadcast."
3062            );
3063        } else {
3064            tracing::info!(
3065                "Transaction includes {} custom asset UTXOs and {} L-BTC UTXOs for fees",
3066                selected_asset_utxos.len(),
3067                selected_lbtc_utxos.len()
3068            );
3069        }
3070
3071        // Create transaction inputs from all selected UTXOs
3072        let inputs: Vec<TxInput> = all_utxos
3073            .iter()
3074            .map(|utxo| TxInput {
3075                txid: utxo.txid.clone(),
3076                vout: utxo.vout,
3077                sequence: None, // Use default sequence
3078            })
3079            .collect();
3080
3081        // Create outputs for distribution (custom asset)
3082        // We need to track outputs as a vector since we may have multiple outputs to the same address
3083        // (e.g., custom asset change + L-BTC change to the same change address)
3084        let mut output_list = Vec::new();
3085
3086        // Add distribution outputs (custom asset)
3087        for (address, amount) in &address_amounts {
3088            output_list.push((address.clone(), *amount, asset_id.to_string()));
3089        }
3090
3091        // Calculate change amount for custom asset (total selected - distribution)
3092        let asset_change_amount = total_selected - total_distribution;
3093
3094        // Add asset change output if there's a significant amount left
3095        if asset_change_amount > DUST_THRESHOLD {
3096            output_list.push((
3097                change_address.to_string(),
3098                asset_change_amount,
3099                asset_id.to_string(),
3100            ));
3101
3102            tracing::debug!(
3103                "Adding asset change output: {} {} to address {}",
3104                asset_change_amount,
3105                asset_id,
3106                change_address
3107            );
3108        } else if asset_change_amount > 0.0 {
3109            tracing::warn!(
3110                "Asset change amount {} is below dust threshold {}, will be lost",
3111                asset_change_amount,
3112                DUST_THRESHOLD
3113            );
3114        }
3115
3116        // Handle L-BTC change if we selected L-BTC UTXOs for fees
3117        // In Elements, the fee is implicit - it's the difference between L-BTC inputs and outputs
3118        // We should NOT subtract the fee from outputs; Elements calculates it automatically
3119        if !selected_lbtc_utxos.is_empty() {
3120            tracing::debug!(
3121                "L-BTC input total: {}, minimum fee needed: {}",
3122                lbtc_total,
3123                min_lbtc_fee
3124            );
3125
3126            // Check if we have enough L-BTC for the minimum fee
3127            if lbtc_total < min_lbtc_fee {
3128                return Err(AmpError::validation(format!(
3129                    "Insufficient L-BTC for fees: have {lbtc_total}, need at least {min_lbtc_fee}"
3130                )));
3131            }
3132
3133            // For now, let's try NOT adding any L-BTC change output
3134            // and let Elements handle the fee automatically from the input/output difference
3135            tracing::info!(
3136                "Using L-BTC input {} for fees - no explicit L-BTC change output (Elements will handle fee automatically)",
3137                lbtc_total
3138            );
3139
3140            // Note: If this approach works, the entire L-BTC input will become the fee
3141            // If we need change, we'll need to figure out the correct way to handle it
3142        }
3143
3144        // For confidential addresses, we need to import them into the wallet first
3145        // so Elements knows about the blinding keys
3146        for address in address_amounts.keys() {
3147            if address.starts_with('v') {
3148                // Confidential address
3149                tracing::debug!("Importing confidential address into wallet: {}", address);
3150                if let Err(e) = self
3151                    .import_address_to_wallet(wallet_name, address, None, false)
3152                    .await
3153                {
3154                    tracing::warn!("Failed to import confidential address {}: {}", address, e);
3155                    // Continue anyway - the address might already be imported
3156                }
3157            }
3158        }
3159
3160        // Build the raw transaction using wallet-specific endpoint for confidential transactions
3161        // For confidential transactions, we need to use blindrawtransaction to properly handle blinding
3162        let raw_transaction = self
3163            .create_raw_transaction_with_outputs(wallet_name, inputs, output_list)
3164            .await
3165            .map_err(|e| {
3166                // Provide more helpful error message for the common L-BTC fee issue
3167                if e.to_string().contains("bad-txns-in-ne-out") || e.to_string().contains("value in != value out") {
3168                    AmpError::validation(format!(
3169                        "Transaction failed due to confidential transaction blinding mismatch. \
3170                        This occurs when Elements creates blinding factors that don't match LWK's expectations. \
3171                        To fix this:\n\
3172                        1. Ensure the wallet has proper blinding keys for all addresses\n\
3173                        2. Use blindrawtransaction before signing\n\
3174                        3. Verify UTXO blinding factors match between Elements and LWK\n\
3175                        4. Original error: {e}"
3176                    ))
3177                } else {
3178                    e.with_context("Failed to build distribution transaction")
3179                }
3180            })?;
3181
3182        // For confidential transactions, we need to blind the transaction properly
3183        // This ensures the blinding factors are compatible with LWK signing
3184        tracing::debug!("Blinding raw transaction for confidential asset distribution");
3185        let blinded_transaction = self
3186            .blind_raw_transaction(wallet_name, &raw_transaction)
3187            .await
3188            .map_err(|e| {
3189                tracing::warn!(
3190                    "Failed to blind transaction, proceeding with unblinded: {}",
3191                    e
3192                );
3193                // If blinding fails, we'll try to proceed with the unblinded transaction
3194                // This might work for some cases but could fail during broadcast
3195                e.with_context("Transaction blinding failed")
3196            })
3197            .unwrap_or_else(|_| {
3198                tracing::warn!("Using unblinded transaction - this may cause broadcast failures");
3199                raw_transaction.clone()
3200            });
3201
3202        tracing::info!(
3203            "Built distribution transaction: {} inputs, {} outputs, asset change: {}",
3204            all_utxos.len(),
3205            address_amounts.len() + usize::from(asset_change_amount > DUST_THRESHOLD),
3206            if asset_change_amount > DUST_THRESHOLD {
3207                asset_change_amount
3208            } else {
3209                0.0
3210            }
3211        );
3212
3213        Ok((blinded_transaction, all_utxos, asset_change_amount))
3214    }
3215
3216    /// Creates a raw transaction with multiple outputs that can handle multiple assets to the same address
3217    ///
3218    /// This method is similar to `create_raw_transaction_with_wallet` but handles the case where
3219    /// multiple outputs with different assets need to go to the same address (e.g., asset change + L-BTC change).
3220    ///
3221    /// # Arguments
3222    /// * `wallet_name` - Name of the Elements wallet to use
3223    /// * `inputs` - Vector of transaction inputs
3224    /// * `outputs` - Vector of (address, amount, `asset_id`) tuples
3225    ///
3226    /// # Returns
3227    /// Returns the raw transaction hex string
3228    #[allow(clippy::cognitive_complexity)]
3229    async fn create_raw_transaction_with_outputs(
3230        &self,
3231        wallet_name: &str,
3232        inputs: Vec<TxInput>,
3233        outputs: Vec<(String, f64, String)>, // (address, amount, asset_id)
3234    ) -> Result<String, AmpError> {
3235        tracing::debug!(
3236            "Creating raw transaction with wallet {} - {} inputs and {} outputs",
3237            wallet_name,
3238            inputs.len(),
3239            outputs.len()
3240        );
3241
3242        // First load the wallet to ensure it's available
3243        self.load_wallet(wallet_name).await?;
3244
3245        // Elements RPC createrawtransaction expects outputs as an array of objects
3246        // Each output object should contain both address, amount, and asset
3247        let mut outputs_array = Vec::new();
3248
3249        for (address, amount, asset_id) in &outputs {
3250            // Convert amount to string with proper precision for Elements
3251            let amount_str = format!("{amount:.8}");
3252
3253            outputs_array.push(serde_json::json!({
3254                address.clone(): amount_str,
3255                "asset": asset_id
3256            }));
3257        }
3258
3259        let params = serde_json::json!([
3260            inputs,        // inputs as TxInput array
3261            outputs_array, // outputs as array of {address: amount, asset: id} objects
3262            0,             // locktime (0 = no locktime)
3263            false,         // replaceable (false = not replaceable)
3264        ]);
3265
3266        // Debug: Log the exact parameters being sent to createrawtransaction
3267        tracing::error!("createrawtransaction parameters (wallet-specific, corrected format):");
3268        tracing::error!("  wallet: {}", wallet_name);
3269        tracing::error!(
3270            "  inputs: {}",
3271            serde_json::to_string_pretty(&inputs).unwrap_or_default()
3272        );
3273        tracing::error!(
3274            "  outputs_array: {}",
3275            serde_json::to_string_pretty(&outputs_array).unwrap_or_default()
3276        );
3277
3278        // Use the wallet-specific RPC endpoint
3279        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
3280
3281        let request = RpcRequest {
3282            jsonrpc: "1.0".to_string(),
3283            id: "amp-client".to_string(),
3284            method: "createrawtransaction".to_string(),
3285            params,
3286        };
3287
3288        let response = self
3289            .client
3290            .post(&wallet_url)
3291            .basic_auth(&self.username, Some(&self.password))
3292            .json(&request)
3293            .send()
3294            .await
3295            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
3296
3297        if !response.status().is_success() {
3298            let status = response.status();
3299            let error_body = response
3300                .text()
3301                .await
3302                .unwrap_or_else(|_| "Unable to read error body".to_string());
3303            return Err(AmpError::rpc(format!(
3304                "RPC request failed with status: {status} - Body: {error_body}"
3305            )));
3306        }
3307
3308        let rpc_response: RpcResponse<String> = response
3309            .json()
3310            .await
3311            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
3312
3313        if let Some(error) = rpc_response.error {
3314            return Err(AmpError::rpc(format!(
3315                "RPC error creating raw transaction: {} (code: {})",
3316                error.message, error.code
3317            )));
3318        }
3319
3320        Ok(rpc_response.result.unwrap_or_default())
3321    }
3322
3323    /// Blinds a raw transaction for confidential transactions
3324    ///
3325    /// This method uses Elements' blindrawtransaction RPC to properly blind a transaction
3326    /// for confidential asset transfers. This is crucial for Liquid transactions to ensure
3327    /// the blinding factors are properly balanced.
3328    ///
3329    /// # Arguments
3330    /// * `wallet_name` - Name of the Elements wallet to use for blinding
3331    /// * `raw_transaction` - The raw transaction hex to blind
3332    ///
3333    /// # Returns
3334    /// Returns the blinded transaction hex string
3335    ///
3336    /// # Errors
3337    /// Returns an error if the RPC call fails or blinding is not possible
3338    pub async fn blind_raw_transaction(
3339        &self,
3340        wallet_name: &str,
3341        raw_transaction: &str,
3342    ) -> Result<String, AmpError> {
3343        tracing::debug!(
3344            "Blinding raw transaction for wallet {} - tx length: {} chars",
3345            wallet_name,
3346            raw_transaction.len()
3347        );
3348
3349        // First load the wallet to ensure it's available
3350        self.load_wallet(wallet_name).await?;
3351
3352        // Elements blindrawtransaction parameters:
3353        // 1. Raw transaction hex
3354        // 2. Input blinding data (can be empty array for auto-detection)
3355        // 3. Input amounts (can be empty array for auto-detection from UTXOs)
3356        // 4. Input assets (can be empty array for auto-detection from UTXOs)
3357        // 5. Input asset blinders (can be empty array for auto-detection)
3358        // 6. Input amount blinders (can be empty array for auto-detection)
3359        let params = serde_json::json!([
3360            raw_transaction, // Raw transaction hex
3361            [],              // Input blinding data (empty for auto-detection)
3362            [],              // Input amounts (empty for auto-detection)
3363            [],              // Input assets (empty for auto-detection)
3364            [],              // Input asset blinders (empty for auto-detection)
3365            []               // Input amount blinders (empty for auto-detection)
3366        ]);
3367
3368        // Use the wallet-specific RPC endpoint
3369        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
3370
3371        let request = RpcRequest {
3372            jsonrpc: "1.0".to_string(),
3373            id: "amp-client".to_string(),
3374            method: "blindrawtransaction".to_string(),
3375            params,
3376        };
3377
3378        let response = self
3379            .client
3380            .post(&wallet_url)
3381            .basic_auth(&self.username, Some(&self.password))
3382            .json(&request)
3383            .send()
3384            .await
3385            .map_err(|e| {
3386                AmpError::rpc(format!("Failed to send blindrawtransaction request: {e}"))
3387            })?;
3388
3389        if !response.status().is_success() {
3390            let status = response.status();
3391            let error_body = response
3392                .text()
3393                .await
3394                .unwrap_or_else(|_| "Unable to read error body".to_string());
3395            return Err(AmpError::rpc(format!(
3396                "blindrawtransaction failed with status: {status} - Body: {error_body}"
3397            )));
3398        }
3399
3400        let rpc_response: RpcResponse<String> = response.json().await.map_err(|e| {
3401            AmpError::rpc(format!("Failed to parse blindrawtransaction response: {e}"))
3402        })?;
3403
3404        if let Some(error) = rpc_response.error {
3405            return Err(AmpError::rpc(format!(
3406                "RPC error blinding transaction: {} (code: {})",
3407                error.message, error.code
3408            )));
3409        }
3410
3411        let blinded_tx = rpc_response.result.unwrap_or_default();
3412
3413        tracing::info!(
3414            "Successfully blinded transaction - original: {} chars, blinded: {} chars",
3415            raw_transaction.len(),
3416            blinded_tx.len()
3417        );
3418
3419        Ok(blinded_tx)
3420    }
3421
3422    /// Signs a raw transaction using the provided signer callback
3423    ///
3424    /// This method integrates with the Signer trait to sign unsigned transactions.
3425    /// It handles the complete signing workflow including:
3426    /// 1. Validation of the unsigned transaction hex format
3427    /// 2. Calling the signer's `sign_transaction` method
3428    /// 3. Validation of the signed transaction format and structure
3429    /// 4. Proper error handling and context propagation
3430    ///
3431    /// # Arguments
3432    /// * `unsigned_tx_hex` - The unsigned transaction in hexadecimal format
3433    /// * `signer` - Implementation of the Signer trait for transaction signing
3434    ///
3435    /// # Returns
3436    /// Returns the signed transaction as a hex string
3437    ///
3438    /// # Errors
3439    /// Returns an error if:
3440    /// - The unsigned transaction hex is invalid or malformed
3441    /// - The signer fails to sign the transaction
3442    /// - The signed transaction format is invalid
3443    /// - Any validation checks fail
3444    ///
3445    /// # Examples
3446    /// ```no_run
3447    /// # use amp_rs::{ElementsRpc, signer::{Signer, LwkSoftwareSigner}};
3448    /// # #[tokio::main]
3449    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3450    /// let rpc = ElementsRpc::from_env()?;
3451    /// let (_, signer) = LwkSoftwareSigner::generate_new()?;
3452    /// let unsigned_tx = "020000000001..."; // Unsigned transaction hex
3453    /// let signed_tx = rpc.sign_transaction(unsigned_tx, &signer).await?;
3454    /// println!("Transaction signed successfully: {}", signed_tx);
3455    /// # Ok(())
3456    /// # }
3457    /// ```
3458    #[allow(clippy::cognitive_complexity)]
3459    pub async fn sign_transaction(
3460        &self,
3461        unsigned_tx_hex: &str,
3462        signer: &dyn crate::signer::Signer,
3463    ) -> Result<String, AmpError> {
3464        const MIN_TX_SIZE: usize = 10; // Minimum bytes for a valid transaction
3465
3466        tracing::debug!(
3467            "Signing transaction: {}...",
3468            &unsigned_tx_hex[..std::cmp::min(unsigned_tx_hex.len(), 64)]
3469        );
3470
3471        // Validate unsigned transaction hex format
3472        if unsigned_tx_hex.is_empty() {
3473            return Err(AmpError::validation(
3474                "Unsigned transaction hex cannot be empty".to_string(),
3475            ));
3476        }
3477
3478        // Check if hex string has valid format (even length, valid hex characters)
3479        if unsigned_tx_hex.len() % 2 != 0 {
3480            return Err(AmpError::validation(
3481                "Unsigned transaction hex must have even length".to_string(),
3482            ));
3483        }
3484
3485        // Validate hex characters
3486        if !unsigned_tx_hex.chars().all(|c| c.is_ascii_hexdigit()) {
3487            return Err(AmpError::validation(
3488                "Unsigned transaction contains invalid hex characters".to_string(),
3489            ));
3490        }
3491
3492        // Attempt to decode hex to validate transaction structure
3493        let tx_bytes = hex::decode(unsigned_tx_hex).map_err(|e| {
3494            AmpError::validation(format!("Failed to decode unsigned transaction hex: {e}"))
3495        })?;
3496
3497        tracing::debug!("Unsigned transaction validation passed, calling signer");
3498
3499        // Call the signer to sign the transaction
3500        let signed_tx_hex = signer
3501            .sign_transaction(unsigned_tx_hex)
3502            .await
3503            .map_err(|e| {
3504                tracing::error!("Transaction signing failed: {}", e);
3505                AmpError::Signer(e).with_context("Failed to sign transaction")
3506            })?;
3507
3508        tracing::debug!(
3509            "Signer returned signed transaction: {}...",
3510            &signed_tx_hex[..std::cmp::min(signed_tx_hex.len(), 64)]
3511        );
3512
3513        // Validate signed transaction format and structure
3514        if signed_tx_hex.is_empty() {
3515            return Err(AmpError::validation(
3516                "Signed transaction hex cannot be empty".to_string(),
3517            ));
3518        }
3519
3520        // Check if signed transaction has valid hex format
3521        if signed_tx_hex.len() % 2 != 0 {
3522            return Err(AmpError::validation(
3523                "Signed transaction hex must have even length".to_string(),
3524            ));
3525        }
3526
3527        // Validate hex characters in signed transaction
3528        if !signed_tx_hex.chars().all(|c| c.is_ascii_hexdigit()) {
3529            return Err(AmpError::validation(
3530                "Signed transaction contains invalid hex characters".to_string(),
3531            ));
3532        }
3533
3534        // Attempt to decode signed transaction to validate structure
3535        let signed_tx_bytes = hex::decode(&signed_tx_hex).map_err(|e| {
3536            AmpError::validation(format!("Failed to decode signed transaction hex: {e}"))
3537        })?;
3538
3539        // Basic validation: signed transaction should be at least as long as unsigned
3540        // (signatures add data, so signed tx should be larger or equal)
3541        if signed_tx_bytes.len() < tx_bytes.len() {
3542            return Err(AmpError::validation(
3543                "Signed transaction is shorter than unsigned transaction, which is invalid"
3544                    .to_string(),
3545            ));
3546        }
3547
3548        // Additional validation: check that the transaction structure is reasonable
3549        // Minimum transaction size for Elements (very basic check)
3550        if signed_tx_bytes.len() < MIN_TX_SIZE {
3551            return Err(AmpError::validation(format!(
3552                "Signed transaction does not meet minimum size ({} bytes), minimum is {} bytes",
3553                signed_tx_bytes.len(),
3554                MIN_TX_SIZE
3555            )));
3556        }
3557
3558        tracing::info!(
3559            "Transaction signed successfully - unsigned: {} bytes, signed: {} bytes",
3560            tx_bytes.len(),
3561            signed_tx_bytes.len()
3562        );
3563
3564        Ok(signed_tx_hex)
3565    }
3566
3567    /// Signs and broadcasts a transaction in a single operation
3568    ///
3569    /// This is a convenience method that combines transaction signing and broadcasting.
3570    /// It performs the complete workflow of signing an unsigned transaction and
3571    /// immediately broadcasting it to the network.
3572    ///
3573    /// # Arguments
3574    /// * `unsigned_tx_hex` - The unsigned transaction in hexadecimal format
3575    /// * `signer` - Implementation of the Signer trait for transaction signing
3576    ///
3577    /// # Returns
3578    /// Returns the transaction ID of the broadcast transaction
3579    ///
3580    /// # Errors
3581    /// Returns an error if signing or broadcasting fails
3582    ///
3583    /// # Examples
3584    /// ```no_run
3585    /// # use amp_rs::{ElementsRpc, signer::{Signer, LwkSoftwareSigner}};
3586    /// # #[tokio::main]
3587    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3588    /// let rpc = ElementsRpc::from_env()?;
3589    /// let (_, signer) = LwkSoftwareSigner::generate_new()?;
3590    /// let unsigned_tx = "020000000001..."; // Unsigned transaction hex
3591    /// let txid = rpc.sign_and_broadcast_transaction(unsigned_tx, &signer).await?;
3592    /// println!("Transaction broadcast with ID: {}", txid);
3593    /// # Ok(())
3594    /// # }
3595    /// ```
3596    pub async fn sign_and_broadcast_transaction(
3597        &self,
3598        unsigned_tx_hex: &str,
3599        signer: &dyn crate::signer::Signer,
3600    ) -> Result<String, AmpError> {
3601        tracing::info!("Signing and broadcasting transaction");
3602
3603        // Sign the transaction
3604        let signed_tx_hex = self
3605            .sign_transaction(unsigned_tx_hex, signer)
3606            .await
3607            .map_err(|e| e.with_context("Failed during transaction signing phase"))?;
3608
3609        // Broadcast the signed transaction
3610        let txid = self
3611            .send_raw_transaction(&signed_tx_hex)
3612            .await
3613            .map_err(|e| e.with_context("Failed during transaction broadcast phase"))?;
3614
3615        tracing::info!("Successfully signed and broadcast transaction: {}", txid);
3616        Ok(txid)
3617    }
3618
3619    /// Signs and broadcasts a transaction with UTXO information for proper PSBT construction
3620    ///
3621    /// This method provides UTXO information to the signer for proper PSBT construction,
3622    /// which is required for confidential transactions where the signer needs to know
3623    /// the previous transaction outputs being spent.
3624    ///
3625    /// # Arguments
3626    /// * `unsigned_tx_hex` - The unsigned transaction in hexadecimal format
3627    /// * `utxos` - Vector of UTXOs being spent in the transaction
3628    /// * `signer` - Implementation of the Signer trait for transaction signing
3629    ///
3630    /// # Returns
3631    /// Returns the transaction ID of the broadcast transaction
3632    ///
3633    /// # Errors
3634    /// Returns an error if signing or broadcasting fails
3635    #[allow(clippy::cognitive_complexity)]
3636    pub async fn sign_and_broadcast_transaction_with_utxos(
3637        &self,
3638        unsigned_tx_hex: &str,
3639        utxos: &[Unspent],
3640        signer: &dyn crate::signer::Signer,
3641    ) -> Result<String, AmpError> {
3642        tracing::info!(
3643            "Signing and broadcasting transaction with {} UTXOs",
3644            utxos.len()
3645        );
3646
3647        // Try to use the enhanced signing method if the signer supports it
3648        let signed_tx_hex = if let Some(lwk_signer) = signer
3649            .as_any()
3650            .downcast_ref::<crate::signer::LwkSoftwareSigner>(
3651        ) {
3652            // Use the enhanced signing method with UTXO information
3653            tracing::debug!("Using LWK signer with UTXO information");
3654            lwk_signer
3655                .sign_transaction_with_utxos(unsigned_tx_hex, utxos)
3656                .await
3657                .map_err(|e| {
3658                    AmpError::Signer(e)
3659                        .with_context("Failed during enhanced transaction signing phase")
3660                })?
3661        } else {
3662            // Fall back to standard signing method
3663            tracing::debug!("Using standard signing method (no UTXO information)");
3664            self.sign_transaction(unsigned_tx_hex, signer)
3665                .await
3666                .map_err(|e| e.with_context("Failed during transaction signing phase"))?
3667        };
3668
3669        // Broadcast the signed transaction
3670        let txid = self
3671            .send_raw_transaction(&signed_tx_hex)
3672            .await
3673            .map_err(|e| e.with_context("Failed during transaction broadcast phase"))?;
3674
3675        tracing::info!("Successfully signed and broadcast transaction: {}", txid);
3676        Ok(txid)
3677    }
3678
3679    /// Collects change data from a confirmed transaction for distribution confirmation
3680    ///
3681    /// This method queries the Elements node to find change UTXOs from a specific transaction
3682    /// that belong to the specified asset. It's used after a distribution transaction is
3683    /// confirmed to collect the change outputs for the final confirmation API call.
3684    ///
3685    /// # Arguments
3686    /// * `asset_id` - The asset ID to filter change UTXOs for
3687    /// * `txid` - The transaction ID to filter change UTXOs from
3688    ///
3689    /// # Returns
3690    /// Returns a vector of Unspent UTXOs that represent change outputs from the transaction.
3691    /// Returns an empty vector if no change outputs exist for the specified asset and transaction.
3692    ///
3693    /// # Errors
3694    /// Returns an error if the RPC call fails or if there are issues querying the Elements node
3695    ///
3696    /// # Examples
3697    /// ```no_run
3698    /// # use amp_rs::ElementsRpc;
3699    /// # #[tokio::main]
3700    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3701    /// let rpc = ElementsRpc::from_env()?;
3702    /// let change_data = rpc.collect_change_data(
3703    ///     "asset_id_hex",
3704    ///     "transaction_id_hex",
3705    ///     &rpc,
3706    ///     "wallet_name"
3707    /// ).await?;
3708    ///
3709    /// if change_data.is_empty() {
3710    ///     println!("No change outputs found for this transaction");
3711    /// } else {
3712    ///     println!("Found {} change outputs", change_data.len());
3713    /// }
3714    /// # Ok(())
3715    /// # }
3716    /// ```
3717    #[allow(clippy::cognitive_complexity)]
3718    pub async fn collect_change_data(
3719        &self,
3720        asset_id: &str,
3721        txid: &str,
3722        node_rpc: &Self,
3723        wallet_name: &str,
3724    ) -> Result<Vec<Unspent>, AmpError> {
3725        tracing::debug!(
3726            "Collecting change data for asset {} from transaction {}",
3727            asset_id,
3728            txid
3729        );
3730
3731        // Use the raw listunspent RPC call to get full blinding information
3732        // This is essential for confidential transactions as the AMP API requires
3733        // both amountblinder and assetblinder fields
3734        let all_utxos = node_rpc
3735            .list_unspent_with_blinding_data(wallet_name)
3736            .await
3737            .map_err(|e| {
3738                e.with_context(
3739                    "Failed to query unspent outputs with blinding data for change data collection",
3740                )
3741            })?;
3742
3743        // Filter UTXOs to only include those from the specified transaction
3744        let change_utxos: Vec<Unspent> = all_utxos
3745            .into_iter()
3746            .filter(|utxo| {
3747                // Match UTXOs that:
3748                // 1. Come from the specified transaction (txid matches)
3749                // 2. Are for the correct asset
3750                // 3. Are spendable
3751                utxo.txid == txid && utxo.asset == asset_id && utxo.spendable
3752            })
3753            .collect();
3754
3755        tracing::info!(
3756            "Collected {} change UTXOs for asset {} from transaction {}",
3757            change_utxos.len(),
3758            asset_id,
3759            txid
3760        );
3761
3762        // Log details of found change UTXOs for debugging
3763        for (index, utxo) in change_utxos.iter().enumerate() {
3764            tracing::debug!(
3765                "Change UTXO {}: txid={}, vout={}, amount={}, asset={}, amountblinder={:?}, assetblinder={:?}",
3766                index + 1,
3767                utxo.txid,
3768                utxo.vout,
3769                utxo.amount,
3770                utxo.asset,
3771                utxo.amountblinder,
3772                utxo.assetblinder
3773            );
3774        }
3775
3776        // Handle the case where no change outputs exist
3777        if change_utxos.is_empty() {
3778            tracing::info!(
3779                "No change outputs found for asset {} in transaction {} - this is normal if all funds were distributed",
3780                asset_id,
3781                txid
3782            );
3783        }
3784
3785        Ok(change_utxos)
3786    }
3787
3788    /// Lists unspent outputs with full blinding data for confidential transactions
3789    ///
3790    /// This method calls the raw `listunspent` RPC to get complete UTXO information
3791    /// including blinding data (amountblinder and assetblinder) which is required
3792    /// for confidential transaction confirmation with the AMP API.
3793    ///
3794    /// # Arguments
3795    /// * `wallet_name` - Name of the Elements wallet to query
3796    ///
3797    /// # Returns
3798    /// Returns a vector of `Unspent` structs with complete blinding information
3799    ///
3800    /// # Errors
3801    /// Returns an error if the RPC call fails
3802    ///
3803    /// # Examples
3804    /// ```no_run
3805    /// # use amp_rs::ElementsRpc;
3806    /// # #[tokio::main]
3807    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3808    /// let rpc = ElementsRpc::from_env()?;
3809    /// let utxos = rpc.list_unspent_with_blinding_data("wallet_name").await?;
3810    /// for utxo in utxos {
3811    ///     println!("UTXO: {} with blinders: {:?}, {:?}",
3812    ///              utxo.txid, utxo.amountblinder, utxo.assetblinder);
3813    /// }
3814    /// # Ok(())
3815    /// # }
3816    /// ```
3817    pub async fn list_unspent_with_blinding_data(
3818        &self,
3819        wallet_name: &str,
3820    ) -> Result<Vec<Unspent>, AmpError> {
3821        tracing::debug!(
3822            "Listing unspent outputs with blinding data for wallet: {}",
3823            wallet_name
3824        );
3825
3826        // First load the wallet to ensure it's available
3827        self.load_wallet(wallet_name).await?;
3828
3829        // Call listunspent with parameters to get all UTXOs
3830        // Parameters: minconf, maxconf, addresses, include_unsafe, query_options
3831        let params = serde_json::json!([
3832            0,         // minconf: include unconfirmed
3833            9_999_999, // maxconf: include all confirmed
3834            [],        // addresses: empty array means all addresses
3835            true,      // include_unsafe: include unconfirmed transactions
3836            {}         // query_options: empty object for default options
3837        ]);
3838
3839        // Use the wallet-specific RPC endpoint
3840        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
3841
3842        let request = RpcRequest {
3843            jsonrpc: "1.0".to_string(),
3844            id: "amp-client".to_string(),
3845            method: "listunspent".to_string(),
3846            params,
3847        };
3848
3849        let response = self
3850            .client
3851            .post(&wallet_url)
3852            .basic_auth(&self.username, Some(&self.password))
3853            .json(&request)
3854            .send()
3855            .await
3856            .map_err(|e| AmpError::rpc(format!("Failed to send listunspent RPC request: {e}")))?;
3857
3858        if !response.status().is_success() {
3859            let status = response.status();
3860            let error_body = response
3861                .text()
3862                .await
3863                .unwrap_or_else(|_| "Unable to read error body".to_string());
3864            return Err(AmpError::rpc(format!(
3865                "Listunspent RPC request failed with status: {status} - Body: {error_body}"
3866            )));
3867        }
3868
3869        let rpc_response: RpcResponse<Vec<Unspent>> = response
3870            .json()
3871            .await
3872            .map_err(|e| AmpError::rpc(format!("Failed to parse listunspent RPC response: {e}")))?;
3873
3874        if let Some(error) = rpc_response.error {
3875            return Err(AmpError::rpc(format!(
3876                "Listunspent RPC error: {} (code: {})",
3877                error.message, error.code
3878            )));
3879        }
3880
3881        let utxos = rpc_response.result.unwrap_or_default();
3882        tracing::info!(
3883            "Retrieved {} UTXOs with blinding data from wallet {}",
3884            utxos.len(),
3885            wallet_name
3886        );
3887
3888        Ok(utxos)
3889    }
3890
3891    /// Creates a standard wallet in Elements (Elements-first approach)
3892    ///
3893    /// This method creates a new standard wallet in the Elements node that can generate
3894    /// addresses and private keys. This is part of the Elements-first approach where
3895    /// we create the wallet in Elements first, then export keys to LWK.
3896    ///
3897    /// # Arguments
3898    /// * `wallet_name` - Name for the new wallet
3899    ///
3900    /// # Errors
3901    /// Returns an error if the RPC call fails
3902    ///
3903    /// # Examples
3904    /// ```no_run
3905    /// # use amp_rs::ElementsRpc;
3906    /// # #[tokio::main]
3907    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3908    /// let rpc = ElementsRpc::from_env()?;
3909    /// rpc.create_elements_wallet("test_wallet").await?;
3910    /// # Ok(())
3911    /// # }
3912    /// ```
3913    pub async fn create_elements_wallet(&self, wallet_name: &str) -> Result<(), AmpError> {
3914        let params = serde_json::json!([wallet_name]);
3915
3916        let _result: serde_json::Value = self.rpc_call("createwallet", params).await?;
3917
3918        tracing::info!("Successfully created Elements wallet: {}", wallet_name);
3919        Ok(())
3920    }
3921
3922    /// Get a new address from an Elements wallet
3923    ///
3924    /// This method requests a new address from the specified Elements wallet.
3925    /// The address will be generated by Elements and can be used for receiving funds.
3926    /// Defaults to native segwit (bech32) addresses for optimal compatibility.
3927    ///
3928    /// # Arguments
3929    /// * `wallet_name` - Name of the wallet to get address from
3930    /// * `address_type` - Optional address type ("bech32", "legacy", "p2sh-segwit"). Defaults to "bech32"
3931    ///
3932    /// # Errors
3933    /// Returns an error if the RPC call fails or the response format is unexpected
3934    ///
3935    /// # Examples
3936    /// ```no_run
3937    /// # use amp_rs::ElementsRpc;
3938    /// # #[tokio::main]
3939    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3940    /// let rpc = ElementsRpc::from_env()?;
3941    ///
3942    /// // Generate native segwit address (default)
3943    /// let address = rpc.get_new_address("test_wallet", None).await?;
3944    ///
3945    /// // Or explicitly request native segwit
3946    /// let bech32_address = rpc.get_new_address("test_wallet", Some("bech32")).await?;
3947    ///
3948    /// println!("Native segwit address: {}", address);
3949    /// # Ok(())
3950    /// # }
3951    /// ```
3952    pub async fn get_new_address(
3953        &self,
3954        wallet_name: &str,
3955        address_type: Option<&str>,
3956    ) -> Result<String, AmpError> {
3957        // First load the wallet to ensure it's available
3958        self.load_wallet(wallet_name).await?;
3959
3960        // Set default to native segwit (bech32) for Elements
3961        let addr_type = address_type.unwrap_or("bech32");
3962
3963        // For Elements, we need to use the correct parameters for getnewaddress
3964        // getnewaddress [label] [address_type]
3965        let params = serde_json::json!(["", addr_type]);
3966
3967        // Create RPC request for getnewaddress
3968        let request = RpcRequest {
3969            jsonrpc: "1.0".to_string(),
3970            id: "amp-client".to_string(),
3971            method: "getnewaddress".to_string(),
3972            params,
3973        };
3974
3975        // Use the wallet-specific RPC endpoint
3976        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
3977
3978        let response = self
3979            .client
3980            .post(&wallet_url)
3981            .basic_auth(&self.username, Some(&self.password))
3982            .json(&request)
3983            .send()
3984            .await
3985            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
3986
3987        if !response.status().is_success() {
3988            let status = response.status();
3989            let error_body = response
3990                .text()
3991                .await
3992                .unwrap_or_else(|_| "Unable to read error body".to_string());
3993            return Err(AmpError::rpc(format!(
3994                "RPC request failed with status: {status} - Body: {error_body}"
3995            )));
3996        }
3997
3998        let rpc_response: RpcResponse<serde_json::Value> = response
3999            .json()
4000            .await
4001            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
4002
4003        if let Some(error) = rpc_response.error {
4004            return Err(AmpError::rpc(format!(
4005                "RPC error getting new address: {} (code: {})",
4006                error.message, error.code
4007            )));
4008        }
4009
4010        if let Some(result) = rpc_response.result {
4011            if let Some(address) = result.as_str() {
4012                tracing::info!("Generated new {} address: {}", addr_type, address);
4013                return Ok(address.to_string());
4014            }
4015        }
4016
4017        Err(AmpError::rpc(format!(
4018            "Failed to get new address from wallet '{wallet_name}': unexpected response format"
4019        )))
4020    }
4021
4022    /// Get the confidential version of an address from Elements wallet
4023    ///
4024    /// This method takes a regular (unconfidential) address and returns its confidential
4025    /// counterpart, which includes blinding keys for confidential transactions.
4026    ///
4027    /// # Arguments
4028    ///
4029    /// * `wallet_name` - Name of the Elements wallet
4030    /// * `address` - The unconfidential address to get info for
4031    ///
4032    /// # Returns
4033    ///
4034    /// Returns the confidential address string
4035    ///
4036    /// # Example
4037    ///
4038    /// ```no_run
4039    /// # use amp_rs::ElementsRpc;
4040    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
4041    /// let rpc = ElementsRpc::from_env()?;
4042    /// let unconfidential_address = "tex1q...";
4043    /// // Note: This would need to be called in an async context
4044    /// // let confidential_address = rpc.get_confidential_address("test_wallet", unconfidential_address).await?;
4045    /// // println!("Confidential address: {}", confidential_address);
4046    /// # Ok(())
4047    /// # }
4048    /// ```
4049    /// Gets the confidential address for a given unconfidential address from a wallet
4050    ///
4051    /// # Errors
4052    /// Returns an error if the RPC call fails or the response format is unexpected
4053    pub async fn get_confidential_address(
4054        &self,
4055        wallet_name: &str,
4056        address: &str,
4057    ) -> Result<String, AmpError> {
4058        // First load the wallet to ensure it's available
4059        self.load_wallet(wallet_name).await?;
4060
4061        let params = serde_json::json!([address]);
4062
4063        // Create RPC request for getaddressinfo
4064        let request = RpcRequest {
4065            jsonrpc: "1.0".to_string(),
4066            id: "amp-client".to_string(),
4067            method: "getaddressinfo".to_string(),
4068            params,
4069        };
4070
4071        // Use the wallet-specific RPC endpoint
4072        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
4073
4074        let response = self
4075            .client
4076            .post(&wallet_url)
4077            .basic_auth(&self.username, Some(&self.password))
4078            .json(&request)
4079            .send()
4080            .await
4081            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
4082
4083        if !response.status().is_success() {
4084            let status = response.status();
4085            let error_body = response
4086                .text()
4087                .await
4088                .unwrap_or_else(|_| "Unable to read error body".to_string());
4089            return Err(AmpError::rpc(format!(
4090                "RPC request failed with status: {status} - Body: {error_body}"
4091            )));
4092        }
4093
4094        let rpc_response: RpcResponse<serde_json::Value> = response
4095            .json()
4096            .await
4097            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
4098
4099        if let Some(error) = rpc_response.error {
4100            return Err(AmpError::rpc(format!(
4101                "RPC error getting address info: {} (code: {})",
4102                error.message, error.code
4103            )));
4104        }
4105
4106        if let Some(result) = rpc_response.result {
4107            if let Some(confidential_address) = result.get("confidential").and_then(|v| v.as_str())
4108            {
4109                tracing::info!("Retrieved confidential address for: {}", address);
4110                return Ok(confidential_address.to_string());
4111            }
4112        }
4113
4114        Err(AmpError::rpc(format!(
4115            "Failed to get confidential address for '{address}': unexpected response format"
4116        )))
4117    }
4118
4119    /// Get the private key for an address from Elements wallet
4120    ///
4121    /// This method exports the private key for a specific address from the Elements wallet.
4122    /// The private key can then be imported into LWK for signing.
4123    ///
4124    /// Note: This is a simplified implementation that returns a placeholder private key.
4125    /// For production use, implement proper wallet-specific RPC calls.
4126    ///
4127    /// # Arguments
4128    /// * `wallet_name` - Name of the wallet containing the address
4129    /// * `address` - The address to get the private key for
4130    ///
4131    /// # Errors
4132    /// Returns an error if the RPC call fails
4133    ///
4134    /// # Examples
4135    /// ```no_run
4136    /// # use amp_rs::ElementsRpc;
4137    /// # #[tokio::main]
4138    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4139    /// let rpc = ElementsRpc::from_env()?;
4140    /// let address = rpc.get_new_address("test_wallet", None).await?;
4141    /// let private_key = rpc.dump_private_key("test_wallet", &address).await?;
4142    /// println!("Private key: {}", private_key);
4143    /// # Ok(())
4144    /// # }
4145    /// ```
4146    pub async fn dump_private_key(
4147        &self,
4148        wallet_name: &str,
4149        address: &str,
4150    ) -> Result<String, AmpError> {
4151        // First load the wallet to ensure it's available
4152        self.load_wallet(wallet_name).await?;
4153
4154        let params = serde_json::json!([address]);
4155
4156        // Create RPC request for dumpprivkey
4157        let request = RpcRequest {
4158            jsonrpc: "1.0".to_string(),
4159            id: "amp-client".to_string(),
4160            method: "dumpprivkey".to_string(),
4161            params,
4162        };
4163
4164        // Use the wallet-specific RPC endpoint
4165        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
4166
4167        let response = self
4168            .client
4169            .post(&wallet_url)
4170            .basic_auth(&self.username, Some(&self.password))
4171            .json(&request)
4172            .send()
4173            .await
4174            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
4175
4176        if !response.status().is_success() {
4177            return Err(AmpError::rpc(format!(
4178                "RPC request failed with status: {}",
4179                response.status()
4180            )));
4181        }
4182
4183        let rpc_response: RpcResponse<serde_json::Value> = response
4184            .json()
4185            .await
4186            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
4187
4188        if let Some(error) = rpc_response.error {
4189            return Err(AmpError::rpc(format!(
4190                "RPC error dumping private key: {} (code: {})",
4191                error.message, error.code
4192            )));
4193        }
4194
4195        if let Some(result) = rpc_response.result {
4196            if let Some(private_key) = result.as_str() {
4197                tracing::info!("Successfully exported private key for address: {}", address);
4198                return Ok(private_key.to_string());
4199            }
4200        }
4201
4202        Err(AmpError::rpc(format!(
4203            "Failed to dump private key for address '{address}': unexpected response format"
4204        )))
4205    }
4206
4207    /// Creates a descriptor wallet in Elements
4208    ///
4209    /// # Arguments
4210    /// * `wallet_name` - Name for the new wallet
4211    ///
4212    /// # Errors
4213    /// Returns an error if the RPC call fails
4214    ///
4215    /// # Examples
4216    /// ```no_run
4217    /// # use amp_rs::ElementsRpc;
4218    /// # #[tokio::main]
4219    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4220    /// let rpc = ElementsRpc::from_env()?;
4221    /// rpc.create_descriptor_wallet("test_wallet").await?;
4222    /// # Ok(())
4223    /// # }
4224    /// ```
4225    pub async fn create_descriptor_wallet(&self, wallet_name: &str) -> Result<(), AmpError> {
4226        let params = serde_json::json!([wallet_name, true]); // true enables descriptors
4227
4228        let _result: serde_json::Value = self.rpc_call("createwallet", params).await?;
4229
4230        tracing::info!("Successfully created descriptor wallet: {}", wallet_name);
4231        Ok(())
4232    }
4233
4234    /// Imports a single descriptor into an Elements wallet
4235    ///
4236    /// This method imports a descriptor that enables the wallet to scan and recognize
4237    /// addresses/UTXOs from a mnemonic. For LWK descriptors with `<0;1>/*` format,
4238    /// a single descriptor covers both receive and change addresses.
4239    ///
4240    /// # Arguments
4241    /// * `wallet_name` - Name of the wallet to import descriptor into
4242    /// * `descriptor` - The descriptor to import
4243    ///
4244    /// # Errors
4245    /// Returns an error if the RPC call fails
4246    ///
4247    /// # Examples
4248    /// ```no_run
4249    /// # use amp_rs::ElementsRpc;
4250    /// # #[tokio::main]
4251    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4252    /// let rpc = ElementsRpc::from_env()?;
4253    /// let descriptor = "ct(slip77(...),elwpkh([...]/84h/1h/0h]tpub.../<0;1>/*))#checksum";
4254    /// rpc.import_descriptor("test_wallet", descriptor).await?;
4255    /// # Ok(())
4256    /// # }
4257    /// ```
4258    pub async fn import_descriptor(
4259        &self,
4260        wallet_name: &str,
4261        descriptor: &str,
4262    ) -> Result<(), AmpError> {
4263        tracing::info!("Importing descriptor into wallet: {}", wallet_name);
4264        tracing::debug!("Descriptor: {}", descriptor);
4265
4266        let descriptors = serde_json::json!([
4267            {
4268                "desc": descriptor,
4269                "timestamp": "now",
4270                "active": true,
4271                "internal": false  // For LWK descriptors with <0;1>/*, this covers both chains
4272            }
4273        ]);
4274
4275        // Use -rpcwallet parameter to specify the wallet
4276        let request = RpcRequest {
4277            jsonrpc: "1.0".to_string(),
4278            id: "amp-client".to_string(),
4279            method: "importdescriptors".to_string(),
4280            params: descriptors,
4281        };
4282
4283        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
4284
4285        let response = self
4286            .client
4287            .post(&wallet_url)
4288            .basic_auth(&self.username, Some(&self.password))
4289            .json(&request)
4290            .send()
4291            .await
4292            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
4293
4294        if !response.status().is_success() {
4295            return Err(AmpError::rpc(format!(
4296                "RPC request failed with status: {}",
4297                response.status()
4298            )));
4299        }
4300
4301        let rpc_response: RpcResponse<serde_json::Value> = response
4302            .json()
4303            .await
4304            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
4305
4306        if let Some(error) = rpc_response.error {
4307            return Err(AmpError::rpc(format!(
4308                "RPC error {}: {}",
4309                error.code, error.message
4310            )));
4311        }
4312
4313        let result = rpc_response
4314            .result
4315            .ok_or_else(|| AmpError::rpc("RPC response missing result field".to_string()))?;
4316
4317        // Check if descriptor was imported successfully
4318        if let Some(results) = result.as_array() {
4319            if let Some(result) = results.first() {
4320                if let Some(success) = result.get("success").and_then(serde_json::Value::as_bool) {
4321                    if !success {
4322                        let error_msg = result
4323                            .get("error")
4324                            .and_then(|e| e.get("message"))
4325                            .and_then(|m| m.as_str())
4326                            .unwrap_or("Unknown error");
4327                        return Err(AmpError::rpc(format!(
4328                            "Failed to import descriptor: {error_msg}"
4329                        )));
4330                    }
4331                } else {
4332                    return Err(AmpError::rpc(format!(
4333                        "Invalid response format for descriptor import: {result:?}"
4334                    )));
4335                }
4336            }
4337        } else {
4338            return Err(AmpError::rpc(format!(
4339                "Invalid response format: expected array, got {result:?}"
4340            )));
4341        }
4342
4343        tracing::info!(
4344            "Successfully imported descriptor into wallet: {}",
4345            wallet_name
4346        );
4347        Ok(())
4348    }
4349
4350    /// Imports descriptors into an Elements wallet (legacy method for compatibility)
4351    ///
4352    /// This method imports descriptors that enable the wallet to scan and recognize
4353    /// addresses/UTXOs from a mnemonic. If both descriptors are the same (as with LWK
4354    /// descriptors using `<0;1>/*` format), only one descriptor is imported.
4355    ///
4356    /// # Arguments
4357    /// * `wallet_name` - Name of the wallet to import descriptors into
4358    /// * `receive_descriptor` - The receive descriptor
4359    /// * `change_descriptor` - The change descriptor
4360    ///
4361    /// # Errors
4362    /// Returns an error if the RPC call fails
4363    ///
4364    /// # Examples
4365    /// ```no_run
4366    /// # use amp_rs::ElementsRpc;
4367    /// # #[tokio::main]
4368    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4369    /// let rpc = ElementsRpc::from_env()?;
4370    /// let descriptor = "ct(slip77(...),elwpkh([...]/84h/1h/0h]tpub.../<0;1>/*))#checksum";
4371    /// rpc.import_descriptors("test_wallet", descriptor, descriptor).await?;
4372    /// # Ok(())
4373    /// # }
4374    /// ```
4375    #[allow(clippy::cognitive_complexity)]
4376    pub async fn import_descriptors(
4377        &self,
4378        wallet_name: &str,
4379        receive_descriptor: &str,
4380        change_descriptor: &str,
4381    ) -> Result<(), AmpError> {
4382        // If both descriptors are the same (LWK case), import only once
4383        if receive_descriptor == change_descriptor {
4384            return self
4385                .import_descriptor(wallet_name, receive_descriptor)
4386                .await;
4387        }
4388
4389        tracing::info!(
4390            "Importing separate receive and change descriptors into wallet: {}",
4391            wallet_name
4392        );
4393        tracing::debug!("Receive descriptor: {}", receive_descriptor);
4394        tracing::debug!("Change descriptor: {}", change_descriptor);
4395
4396        let descriptors = serde_json::json!([
4397            {
4398                "desc": receive_descriptor,
4399                "timestamp": "now",
4400                "active": true,
4401                "internal": false
4402            },
4403            {
4404                "desc": change_descriptor,
4405                "timestamp": "now",
4406                "active": true,
4407                "internal": true
4408            }
4409        ]);
4410
4411        // Use -rpcwallet parameter to specify the wallet
4412        let request = RpcRequest {
4413            jsonrpc: "1.0".to_string(),
4414            id: "amp-client".to_string(),
4415            method: "importdescriptors".to_string(),
4416            params: descriptors,
4417        };
4418
4419        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
4420
4421        let response = self
4422            .client
4423            .post(&wallet_url)
4424            .basic_auth(&self.username, Some(&self.password))
4425            .json(&request)
4426            .send()
4427            .await
4428            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
4429
4430        if !response.status().is_success() {
4431            return Err(AmpError::rpc(format!(
4432                "RPC request failed with status: {}",
4433                response.status()
4434            )));
4435        }
4436
4437        let rpc_response: RpcResponse<serde_json::Value> = response
4438            .json()
4439            .await
4440            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
4441
4442        if let Some(error) = rpc_response.error {
4443            return Err(AmpError::rpc(format!(
4444                "RPC error {}: {}",
4445                error.code, error.message
4446            )));
4447        }
4448
4449        let result = rpc_response
4450            .result
4451            .ok_or_else(|| AmpError::rpc("RPC response missing result field".to_string()))?;
4452
4453        // Check if both descriptors were imported successfully
4454        if let Some(results) = result.as_array() {
4455            for (i, result) in results.iter().enumerate() {
4456                if let Some(success) = result.get("success").and_then(serde_json::Value::as_bool) {
4457                    if !success {
4458                        let desc_type = if i == 0 { "receive" } else { "change" };
4459                        let error_msg = result
4460                            .get("error")
4461                            .and_then(|e| e.get("message"))
4462                            .and_then(|m| m.as_str())
4463                            .unwrap_or("Unknown error");
4464                        return Err(AmpError::rpc(format!(
4465                            "Failed to import {desc_type} descriptor: {error_msg}"
4466                        )));
4467                    }
4468                } else {
4469                    return Err(AmpError::rpc(format!(
4470                        "Invalid response format for descriptor import: {result:?}"
4471                    )));
4472                }
4473            }
4474        } else {
4475            return Err(AmpError::rpc(format!(
4476                "Invalid response format: expected array, got {result:?}"
4477            )));
4478        }
4479
4480        tracing::info!(
4481            "Successfully imported descriptors into wallet: {}",
4482            wallet_name
4483        );
4484        Ok(())
4485    }
4486
4487    /// Sets up a wallet with descriptors from a mnemonic
4488    ///
4489    /// This is a convenience method that combines wallet creation and descriptor import.
4490    /// It creates a descriptor wallet and imports the receive and change descriptors
4491    /// generated from the provided mnemonic.
4492    ///
4493    /// # Arguments
4494    /// * `wallet_name` - Name for the new wallet
4495    /// * `receive_descriptor` - The receive descriptor (external chain /0/*)
4496    /// * `change_descriptor` - The change descriptor (internal chain /1/*)
4497    ///
4498    /// # Errors
4499    /// Returns an error if wallet creation or descriptor import fails
4500    ///
4501    /// # Examples
4502    /// ```no_run
4503    /// # use amp_rs::ElementsRpc;
4504    /// # #[tokio::main]
4505    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4506    /// let rpc = ElementsRpc::from_env()?;
4507    /// let receive_desc = "wpkh([d34db33f/84h/1h/0h]xprv.../0/*)#checksum";
4508    /// let change_desc = "wpkh([d34db33f/84h/1h/0h]xprv.../1/*)#checksum";
4509    /// rpc.setup_wallet_with_descriptors("test_wallet", receive_desc, change_desc).await?;
4510    /// # Ok(())
4511    /// # }
4512    /// ```
4513    #[allow(clippy::cognitive_complexity)]
4514    pub async fn setup_wallet_with_descriptors(
4515        &self,
4516        wallet_name: &str,
4517        receive_descriptor: &str,
4518        change_descriptor: &str,
4519    ) -> Result<(), AmpError> {
4520        tracing::info!("Setting up wallet with descriptors: {}", wallet_name);
4521
4522        // Try to create the wallet (may fail if it already exists)
4523        match self.create_descriptor_wallet(wallet_name).await {
4524            Ok(()) => {
4525                tracing::info!("Created new descriptor wallet: {}", wallet_name);
4526            }
4527            Err(e) => {
4528                let error_msg = e.to_string();
4529                if error_msg.contains("already exists")
4530                    || error_msg.contains("Database already exists")
4531                {
4532                    tracing::info!(
4533                        "Wallet {} already exists, proceeding with descriptor import",
4534                        wallet_name
4535                    );
4536                } else {
4537                    return Err(e);
4538                }
4539            }
4540        }
4541
4542        // Import the descriptors
4543        self.import_descriptors(wallet_name, receive_descriptor, change_descriptor)
4544            .await?;
4545
4546        tracing::info!(
4547            "Successfully set up wallet with descriptors: {}",
4548            wallet_name
4549        );
4550        Ok(())
4551    }
4552
4553    /// Exports a wallet to a file using dumpwallet RPC
4554    ///
4555    /// # Arguments
4556    /// * `wallet_name` - Name of the wallet to export
4557    /// * `file_path` - Path where the wallet dump file will be created
4558    ///
4559    /// # Errors
4560    /// Returns an error if the RPC call fails or the wallet cannot be exported
4561    ///
4562    /// # Examples
4563    /// ```no_run
4564    /// # use amp_rs::ElementsRpc;
4565    /// # #[tokio::main]
4566    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4567    /// let rpc = ElementsRpc::from_env()?;
4568    /// rpc.dump_wallet("my_wallet", "/tmp/wallet_export.dat").await?;
4569    /// # Ok(())
4570    /// # }
4571    /// ```
4572    pub async fn dump_wallet(&self, wallet_name: &str, file_path: &str) -> Result<(), AmpError> {
4573        // First load the wallet to ensure it's available
4574        self.load_wallet(wallet_name).await?;
4575
4576        let params = serde_json::json!([file_path]);
4577
4578        // Create RPC request for dumpwallet
4579        let request = RpcRequest {
4580            jsonrpc: "1.0".to_string(),
4581            id: "amp-client".to_string(),
4582            method: "dumpwallet".to_string(),
4583            params,
4584        };
4585
4586        // Use the wallet-specific RPC endpoint
4587        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
4588
4589        let response = self
4590            .client
4591            .post(&wallet_url)
4592            .basic_auth(&self.username, Some(&self.password))
4593            .json(&request)
4594            .send()
4595            .await
4596            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
4597
4598        if !response.status().is_success() {
4599            return Err(AmpError::rpc(format!(
4600                "RPC request failed with status: {}",
4601                response.status()
4602            )));
4603        }
4604
4605        let rpc_response: RpcResponse<serde_json::Value> = response
4606            .json()
4607            .await
4608            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
4609
4610        if let Some(error) = rpc_response.error {
4611            return Err(AmpError::rpc(format!(
4612                "RPC error dumping wallet: {} (code: {})",
4613                error.message, error.code
4614            )));
4615        }
4616
4617        tracing::info!(
4618            "Successfully exported wallet {} to {}",
4619            wallet_name,
4620            file_path
4621        );
4622        Ok(())
4623    }
4624
4625    /// Imports a wallet from a file using importwallet RPC
4626    ///
4627    /// # Arguments
4628    /// * `wallet_name` - Name of the wallet to import into
4629    /// * `file_path` - Path to the wallet dump file to import
4630    ///
4631    /// # Errors
4632    /// Returns an error if the RPC call fails or the wallet cannot be imported
4633    ///
4634    /// # Examples
4635    /// ```no_run
4636    /// # use amp_rs::ElementsRpc;
4637    /// # #[tokio::main]
4638    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4639    /// let rpc = ElementsRpc::from_env()?;
4640    /// rpc.import_wallet("my_wallet", "/tmp/wallet_export.dat").await?;
4641    /// # Ok(())
4642    /// # }
4643    /// ```
4644    pub async fn import_wallet(&self, wallet_name: &str, file_path: &str) -> Result<(), AmpError> {
4645        // First load the wallet to ensure it's available
4646        self.load_wallet(wallet_name).await?;
4647
4648        let params = serde_json::json!([file_path]);
4649
4650        // Create RPC request for importwallet
4651        let request = RpcRequest {
4652            jsonrpc: "1.0".to_string(),
4653            id: "amp-client".to_string(),
4654            method: "importwallet".to_string(),
4655            params,
4656        };
4657
4658        // Use the wallet-specific RPC endpoint
4659        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
4660
4661        let response = self
4662            .client
4663            .post(&wallet_url)
4664            .basic_auth(&self.username, Some(&self.password))
4665            .json(&request)
4666            .send()
4667            .await
4668            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
4669
4670        if !response.status().is_success() {
4671            return Err(AmpError::rpc(format!(
4672                "RPC request failed with status: {}",
4673                response.status()
4674            )));
4675        }
4676
4677        let rpc_response: RpcResponse<serde_json::Value> = response
4678            .json()
4679            .await
4680            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
4681
4682        if let Some(error) = rpc_response.error {
4683            return Err(AmpError::rpc(format!(
4684                "RPC error importing wallet: {} (code: {})",
4685                error.message, error.code
4686            )));
4687        }
4688
4689        tracing::info!(
4690            "Successfully imported wallet {} from {}",
4691            wallet_name,
4692            file_path
4693        );
4694        Ok(())
4695    }
4696
4697    /// Exports a blinding key for a confidential address using dumpblindingkey RPC
4698    ///
4699    /// # Arguments
4700    /// * `wallet_name` - Name of the wallet containing the address
4701    /// * `address` - The confidential address to export the blinding key for
4702    ///
4703    /// # Errors
4704    /// Returns an error if the RPC call fails or the address doesn't have a blinding key
4705    ///
4706    /// # Examples
4707    /// ```no_run
4708    /// # use amp_rs::ElementsRpc;
4709    /// # #[tokio::main]
4710    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4711    /// let rpc = ElementsRpc::from_env()?;
4712    /// let key = rpc.dump_blinding_key("my_wallet", "VTpz...").await?;
4713    /// println!("Blinding key: {}", key);
4714    /// # Ok(())
4715    /// # }
4716    /// ```
4717    pub async fn dump_blinding_key(
4718        &self,
4719        wallet_name: &str,
4720        address: &str,
4721    ) -> Result<String, AmpError> {
4722        // First load the wallet to ensure it's available
4723        self.load_wallet(wallet_name).await?;
4724
4725        let params = serde_json::json!([address]);
4726
4727        // Create RPC request for dumpblindingkey
4728        let request = RpcRequest {
4729            jsonrpc: "1.0".to_string(),
4730            id: "amp-client".to_string(),
4731            method: "dumpblindingkey".to_string(),
4732            params,
4733        };
4734
4735        // Use the wallet-specific RPC endpoint
4736        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
4737
4738        let response = self
4739            .client
4740            .post(&wallet_url)
4741            .basic_auth(&self.username, Some(&self.password))
4742            .json(&request)
4743            .send()
4744            .await
4745            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
4746
4747        if !response.status().is_success() {
4748            return Err(AmpError::rpc(format!(
4749                "RPC request failed with status: {}",
4750                response.status()
4751            )));
4752        }
4753
4754        let rpc_response: RpcResponse<serde_json::Value> = response
4755            .json()
4756            .await
4757            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
4758
4759        if let Some(error) = rpc_response.error {
4760            return Err(AmpError::rpc(format!(
4761                "RPC error dumping blinding key: {} (code: {})",
4762                error.message, error.code
4763            )));
4764        }
4765
4766        if let Some(result) = rpc_response.result {
4767            if let Some(blinding_key) = result.as_str() {
4768                tracing::info!(
4769                    "Successfully exported blinding key for address: {}",
4770                    address
4771                );
4772                return Ok(blinding_key.to_string());
4773            }
4774        }
4775
4776        Err(AmpError::rpc(format!(
4777            "Failed to dump blinding key for address '{address}': unexpected response format"
4778        )))
4779    }
4780
4781    /// Imports a blinding key for a confidential address using importblindingkey RPC
4782    ///
4783    /// # Arguments
4784    /// * `wallet_name` - Name of the wallet to import the blinding key into
4785    /// * `address` - The confidential address to import the blinding key for
4786    /// * `blinding_key` - The blinding key to import
4787    ///
4788    /// # Errors
4789    /// Returns an error if the RPC call fails or the blinding key cannot be imported
4790    ///
4791    /// # Examples
4792    /// ```no_run
4793    /// # use amp_rs::ElementsRpc;
4794    /// # #[tokio::main]
4795    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4796    /// let rpc = ElementsRpc::from_env()?;
4797    /// rpc.import_blinding_key("my_wallet", "VTpz...", "blinding_key_hex").await?;
4798    /// # Ok(())
4799    /// # }
4800    /// ```
4801    pub async fn import_blinding_key(
4802        &self,
4803        wallet_name: &str,
4804        address: &str,
4805        blinding_key: &str,
4806    ) -> Result<(), AmpError> {
4807        // First load the wallet to ensure it's available
4808        self.load_wallet(wallet_name).await?;
4809
4810        let params = serde_json::json!([address, blinding_key]);
4811
4812        // Create RPC request for importblindingkey
4813        let request = RpcRequest {
4814            jsonrpc: "1.0".to_string(),
4815            id: "amp-client".to_string(),
4816            method: "importblindingkey".to_string(),
4817            params,
4818        };
4819
4820        // Use the wallet-specific RPC endpoint
4821        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
4822
4823        let response = self
4824            .client
4825            .post(&wallet_url)
4826            .basic_auth(&self.username, Some(&self.password))
4827            .json(&request)
4828            .send()
4829            .await
4830            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
4831
4832        if !response.status().is_success() {
4833            return Err(AmpError::rpc(format!(
4834                "RPC request failed with status: {}",
4835                response.status()
4836            )));
4837        }
4838
4839        let rpc_response: RpcResponse<serde_json::Value> = response
4840            .json()
4841            .await
4842            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
4843
4844        if let Some(error) = rpc_response.error {
4845            return Err(AmpError::rpc(format!(
4846                "RPC error importing blinding key: {} (code: {})",
4847                error.message, error.code
4848            )));
4849        }
4850
4851        tracing::info!(
4852            "Successfully imported blinding key for address: {}",
4853            address
4854        );
4855        Ok(())
4856    }
4857
4858    /// Gets wallet information using getwalletinfo RPC
4859    ///
4860    /// # Arguments
4861    /// * `wallet_name` - Name of the wallet to get information for
4862    ///
4863    /// # Errors
4864    /// Returns an error if the RPC call fails
4865    ///
4866    /// # Examples
4867    /// ```no_run
4868    /// # use amp_rs::ElementsRpc;
4869    /// # #[tokio::main]
4870    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4871    /// let rpc = ElementsRpc::from_env()?;
4872    /// let info = rpc.get_wallet_info("my_wallet").await?;
4873    /// println!("Wallet info: {:?}", info);
4874    /// # Ok(())
4875    /// # }
4876    /// ```
4877    pub async fn get_wallet_info(&self, wallet_name: &str) -> Result<serde_json::Value, AmpError> {
4878        // First load the wallet to ensure it's available
4879        self.load_wallet(wallet_name).await?;
4880
4881        let params = serde_json::json!([]);
4882
4883        // Create RPC request for getwalletinfo
4884        let request = RpcRequest {
4885            jsonrpc: "1.0".to_string(),
4886            id: "amp-client".to_string(),
4887            method: "getwalletinfo".to_string(),
4888            params,
4889        };
4890
4891        // Use the wallet-specific RPC endpoint
4892        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
4893
4894        let response = self
4895            .client
4896            .post(&wallet_url)
4897            .basic_auth(&self.username, Some(&self.password))
4898            .json(&request)
4899            .send()
4900            .await
4901            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
4902
4903        if !response.status().is_success() {
4904            return Err(AmpError::rpc(format!(
4905                "RPC request failed with status: {}",
4906                response.status()
4907            )));
4908        }
4909
4910        let rpc_response: RpcResponse<serde_json::Value> = response
4911            .json()
4912            .await
4913            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
4914
4915        if let Some(error) = rpc_response.error {
4916            return Err(AmpError::rpc(format!(
4917                "RPC error getting wallet info: {} (code: {})",
4918                error.message, error.code
4919            )));
4920        }
4921
4922        if let Some(result) = rpc_response.result {
4923            tracing::info!("Successfully retrieved wallet info for: {}", wallet_name);
4924            return Ok(result);
4925        }
4926
4927        Err(AmpError::rpc(format!(
4928            "Failed to get wallet info for '{wallet_name}': unexpected response format"
4929        )))
4930    }
4931
4932    /// Gets the unconfidential address for a confidential address
4933    ///
4934    /// # Arguments
4935    /// * `wallet_name` - Name of the wallet
4936    /// * `confidential_address` - The confidential address to convert
4937    ///
4938    /// # Errors
4939    /// Returns an error if the RPC call fails
4940    ///
4941    /// # Examples
4942    /// ```no_run
4943    /// # use amp_rs::ElementsRpc;
4944    /// # #[tokio::main]
4945    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4946    /// let rpc = ElementsRpc::from_env()?;
4947    /// let unconf = rpc.get_unconfidential_address("my_wallet", "VTpz...").await?;
4948    /// println!("Unconfidential address: {}", unconf);
4949    /// # Ok(())
4950    /// # }
4951    /// ```
4952    pub async fn get_unconfidential_address(
4953        &self,
4954        wallet_name: &str,
4955        confidential_address: &str,
4956    ) -> Result<String, AmpError> {
4957        // First load the wallet to ensure it's available
4958        self.load_wallet(wallet_name).await?;
4959
4960        let params = serde_json::json!([confidential_address]);
4961
4962        // Create RPC request for getunconfidentialaddress
4963        let request = RpcRequest {
4964            jsonrpc: "1.0".to_string(),
4965            id: "amp-client".to_string(),
4966            method: "getunconfidentialaddress".to_string(),
4967            params,
4968        };
4969
4970        // Use the wallet-specific RPC endpoint
4971        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
4972
4973        let response = self
4974            .client
4975            .post(&wallet_url)
4976            .basic_auth(&self.username, Some(&self.password))
4977            .json(&request)
4978            .send()
4979            .await
4980            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
4981
4982        if !response.status().is_success() {
4983            return Err(AmpError::rpc(format!(
4984                "RPC request failed with status: {}",
4985                response.status()
4986            )));
4987        }
4988
4989        let rpc_response: RpcResponse<serde_json::Value> = response
4990            .json()
4991            .await
4992            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
4993
4994        if let Some(error) = rpc_response.error {
4995            return Err(AmpError::rpc(format!(
4996                "RPC error getting unconfidential address: {} (code: {})",
4997                error.message, error.code
4998            )));
4999        }
5000
5001        if let Some(result) = rpc_response.result {
5002            if let Some(address) = result.as_str() {
5003                tracing::info!(
5004                    "Successfully got unconfidential address for: {}",
5005                    confidential_address
5006                );
5007                return Ok(address.to_string());
5008            }
5009        }
5010
5011        Err(AmpError::rpc(format!(
5012            "Failed to get unconfidential address for '{confidential_address}': unexpected response format"
5013        )))
5014    }
5015
5016    /// Imports a private key into the wallet using importprivkey RPC
5017    ///
5018    /// # Arguments
5019    /// * `wallet_name` - Name of the wallet to import into
5020    /// * `private_key` - The private key in WIF format
5021    /// * `label` - Optional label for the address
5022    /// * `rescan` - Whether to rescan the blockchain for transactions
5023    ///
5024    /// # Errors
5025    /// Returns an error if the RPC call fails
5026    ///
5027    /// # Examples
5028    /// ```no_run
5029    /// # use amp_rs::ElementsRpc;
5030    /// # #[tokio::main]
5031    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
5032    /// let rpc = ElementsRpc::from_env()?;
5033    /// rpc.import_private_key("my_wallet", "cT1...", Some("my_address"), Some(false)).await?;
5034    /// # Ok(())
5035    /// # }
5036    /// ```
5037    pub async fn import_private_key(
5038        &self,
5039        wallet_name: &str,
5040        private_key: &str,
5041        label: Option<&str>,
5042        rescan: Option<bool>,
5043    ) -> Result<(), AmpError> {
5044        // First load the wallet to ensure it's available
5045        self.load_wallet(wallet_name).await?;
5046
5047        let params = serde_json::json!([private_key, label.unwrap_or(""), rescan.unwrap_or(false)]);
5048
5049        // Create RPC request for importprivkey
5050        let request = RpcRequest {
5051            jsonrpc: "1.0".to_string(),
5052            id: "amp-client".to_string(),
5053            method: "importprivkey".to_string(),
5054            params,
5055        };
5056
5057        // Use the wallet-specific RPC endpoint
5058        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
5059
5060        let response = self
5061            .client
5062            .post(&wallet_url)
5063            .basic_auth(&self.username, Some(&self.password))
5064            .json(&request)
5065            .send()
5066            .await
5067            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
5068
5069        if !response.status().is_success() {
5070            return Err(AmpError::rpc(format!(
5071                "RPC request failed with status: {}",
5072                response.status()
5073            )));
5074        }
5075
5076        let rpc_response: RpcResponse<serde_json::Value> = response
5077            .json()
5078            .await
5079            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
5080
5081        if let Some(error) = rpc_response.error {
5082            return Err(AmpError::rpc(format!(
5083                "RPC error importing private key: {} (code: {})",
5084                error.message, error.code
5085            )));
5086        }
5087
5088        tracing::info!("Successfully imported private key");
5089        Ok(())
5090    }
5091
5092    /// Lists all descriptors in a wallet using listdescriptors RPC
5093    ///
5094    /// # Arguments
5095    /// * `wallet_name` - Name of the wallet
5096    /// * `private_keys` - Whether to include private keys in the output
5097    ///
5098    /// # Errors
5099    /// Returns an error if the RPC call fails
5100    ///
5101    /// # Examples
5102    /// ```no_run
5103    /// # use amp_rs::ElementsRpc;
5104    /// # #[tokio::main]
5105    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
5106    /// let rpc = ElementsRpc::from_env()?;
5107    /// let descriptors = rpc.list_descriptors("my_wallet", Some(true)).await?;
5108    /// for desc in descriptors {
5109    ///     println!("Descriptor: {}", desc);
5110    /// }
5111    /// # Ok(())
5112    /// # }
5113    /// ```
5114    pub async fn list_descriptors(
5115        &self,
5116        wallet_name: &str,
5117        private_keys: Option<bool>,
5118    ) -> Result<Vec<String>, AmpError> {
5119        // First load the wallet to ensure it's available
5120        self.load_wallet(wallet_name).await?;
5121
5122        let params = serde_json::json!([private_keys.unwrap_or(false)]);
5123
5124        // Create RPC request for listdescriptors
5125        let request = RpcRequest {
5126            jsonrpc: "1.0".to_string(),
5127            id: "amp-client".to_string(),
5128            method: "listdescriptors".to_string(),
5129            params,
5130        };
5131
5132        // Use the wallet-specific RPC endpoint
5133        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
5134
5135        let response = self
5136            .client
5137            .post(&wallet_url)
5138            .basic_auth(&self.username, Some(&self.password))
5139            .json(&request)
5140            .send()
5141            .await
5142            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
5143
5144        if !response.status().is_success() {
5145            return Err(AmpError::rpc(format!(
5146                "RPC request failed with status: {}",
5147                response.status()
5148            )));
5149        }
5150
5151        let rpc_response: RpcResponse<serde_json::Value> = response
5152            .json()
5153            .await
5154            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
5155
5156        if let Some(error) = rpc_response.error {
5157            return Err(AmpError::rpc(format!(
5158                "RPC error listing descriptors: {} (code: {})",
5159                error.message, error.code
5160            )));
5161        }
5162
5163        if let Some(result) = rpc_response.result {
5164            // Result has a "descriptors" array with objects containing "desc" field
5165            if let Some(descriptors_array) = result.get("descriptors").and_then(|v| v.as_array()) {
5166                let descriptors: Vec<String> = descriptors_array
5167                    .iter()
5168                    .filter_map(|d| d.get("desc").and_then(|v| v.as_str()).map(String::from))
5169                    .collect();
5170                tracing::info!(
5171                    "Successfully retrieved {} descriptors for wallet: {}",
5172                    descriptors.len(),
5173                    wallet_name
5174                );
5175                return Ok(descriptors);
5176            }
5177        }
5178
5179        Ok(Vec::new())
5180    }
5181
5182    /// Gets all addresses in a wallet by label using getaddressesbylabel RPC
5183    ///
5184    /// # Arguments
5185    /// * `wallet_name` - Name of the wallet
5186    /// * `label` - Label to filter by (empty string for all addresses)
5187    ///
5188    /// # Errors
5189    /// Returns an error if the RPC call fails
5190    ///
5191    /// # Examples
5192    /// ```no_run
5193    /// # use amp_rs::ElementsRpc;
5194    /// # #[tokio::main]
5195    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
5196    /// let rpc = ElementsRpc::from_env()?;
5197    /// let addresses = rpc.get_addresses_by_label("my_wallet", "").await?;
5198    /// for addr in addresses {
5199    ///     println!("Address: {}", addr);
5200    /// }
5201    /// # Ok(())
5202    /// # }
5203    /// ```
5204    pub async fn get_addresses_by_label(
5205        &self,
5206        wallet_name: &str,
5207        label: &str,
5208    ) -> Result<Vec<String>, AmpError> {
5209        // First load the wallet to ensure it's available
5210        self.load_wallet(wallet_name).await?;
5211
5212        let params = serde_json::json!([label]);
5213
5214        // Create RPC request for getaddressesbylabel
5215        let request = RpcRequest {
5216            jsonrpc: "1.0".to_string(),
5217            id: "amp-client".to_string(),
5218            method: "getaddressesbylabel".to_string(),
5219            params,
5220        };
5221
5222        // Use the wallet-specific RPC endpoint
5223        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
5224
5225        let response = self
5226            .client
5227            .post(&wallet_url)
5228            .basic_auth(&self.username, Some(&self.password))
5229            .json(&request)
5230            .send()
5231            .await
5232            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
5233
5234        if !response.status().is_success() {
5235            return Err(AmpError::rpc(format!(
5236                "RPC request failed with status: {}",
5237                response.status()
5238            )));
5239        }
5240
5241        let rpc_response: RpcResponse<serde_json::Value> = response
5242            .json()
5243            .await
5244            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
5245
5246        if let Some(error) = rpc_response.error {
5247            return Err(AmpError::rpc(format!(
5248                "RPC error getting addresses by label: {} (code: {})",
5249                error.message, error.code
5250            )));
5251        }
5252
5253        if let Some(result) = rpc_response.result {
5254            // Result is an object with addresses as keys
5255            if let Some(obj) = result.as_object() {
5256                let addresses: Vec<String> = obj.keys().cloned().collect();
5257                tracing::info!(
5258                    "Successfully retrieved {} addresses for wallet: {}",
5259                    addresses.len(),
5260                    wallet_name
5261                );
5262                return Ok(addresses);
5263            }
5264        }
5265
5266        Ok(Vec::new())
5267    }
5268
5269    /// Lists addresses that have received transactions using listreceivedbyaddress RPC
5270    ///
5271    /// # Arguments
5272    /// * `wallet_name` - Name of the wallet to list addresses for
5273    /// * `min_conf` - Minimum number of confirmations (0 for unconfirmed)
5274    /// * `include_empty` - Whether to include addresses that haven't received payments
5275    ///
5276    /// # Errors
5277    /// Returns an error if the RPC call fails
5278    ///
5279    /// # Examples
5280    /// ```no_run
5281    /// # use amp_rs::ElementsRpc;
5282    /// # #[tokio::main]
5283    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
5284    /// let rpc = ElementsRpc::from_env()?;
5285    /// let addresses = rpc.list_received_by_address("my_wallet", 0, true).await?;
5286    /// for addr in addresses {
5287    ///     println!("Address: {:?}", addr);
5288    /// }
5289    /// # Ok(())
5290    /// # }
5291    /// ```
5292    pub async fn list_received_by_address(
5293        &self,
5294        wallet_name: &str,
5295        min_conf: u32,
5296        include_empty: bool,
5297    ) -> Result<Vec<ReceivedByAddress>, AmpError> {
5298        // First load the wallet to ensure it's available
5299        self.load_wallet(wallet_name).await?;
5300
5301        let params = serde_json::json!([min_conf, include_empty]);
5302
5303        // Create RPC request for listreceivedbyaddress
5304        let request = RpcRequest {
5305            jsonrpc: "1.0".to_string(),
5306            id: "amp-client".to_string(),
5307            method: "listreceivedbyaddress".to_string(),
5308            params,
5309        };
5310
5311        // Use the wallet-specific RPC endpoint
5312        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
5313
5314        let response = self
5315            .client
5316            .post(&wallet_url)
5317            .basic_auth(&self.username, Some(&self.password))
5318            .json(&request)
5319            .send()
5320            .await
5321            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
5322
5323        if !response.status().is_success() {
5324            return Err(AmpError::rpc(format!(
5325                "RPC request failed with status: {}",
5326                response.status()
5327            )));
5328        }
5329
5330        let rpc_response: RpcResponse<Vec<ReceivedByAddress>> = response
5331            .json()
5332            .await
5333            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
5334
5335        if let Some(error) = rpc_response.error {
5336            return Err(AmpError::rpc(format!(
5337                "RPC error listing received by address: {} (code: {})",
5338                error.message, error.code
5339            )));
5340        }
5341
5342        if let Some(result) = rpc_response.result {
5343            tracing::info!(
5344                "Successfully listed {} addresses for wallet: {}",
5345                result.len(),
5346                wallet_name
5347            );
5348            return Ok(result);
5349        }
5350
5351        Ok(Vec::new())
5352    }
5353}
5354
5355#[cfg(test)]
5356mod elements_rpc_tests {
5357    use super::*;
5358    use httpmock::prelude::*;
5359    use serial_test::serial;
5360    use std::collections::HashMap;
5361
5362    #[test]
5363    fn test_elements_rpc_new() {
5364        let rpc = ElementsRpc::new(
5365            "http://localhost:18884".to_string(),
5366            "user".to_string(),
5367            "pass".to_string(),
5368        );
5369
5370        assert_eq!(rpc.base_url, "http://localhost:18884");
5371        assert_eq!(rpc.username, "user");
5372        assert_eq!(rpc.password, "pass");
5373    }
5374
5375    #[test]
5376    #[serial]
5377    fn test_elements_rpc_from_env_missing_vars() {
5378        // Store original values to restore later
5379        let original_url = env::var("ELEMENTS_RPC_URL").ok();
5380        let original_user = env::var("ELEMENTS_RPC_USER").ok();
5381        let original_password = env::var("ELEMENTS_RPC_PASSWORD").ok();
5382
5383        // Clear environment variables to test error handling
5384        env::remove_var("ELEMENTS_RPC_URL");
5385        env::remove_var("ELEMENTS_RPC_USER");
5386        env::remove_var("ELEMENTS_RPC_PASSWORD");
5387
5388        let result = ElementsRpc::from_env();
5389        assert!(
5390            result.is_err(),
5391            "ElementsRpc::from_env() should fail when env vars are missing"
5392        );
5393
5394        match result.unwrap_err() {
5395            AmpError::Validation(msg) => {
5396                assert!(
5397                    msg.contains("ELEMENTS_RPC_URL"),
5398                    "Error message should mention missing ELEMENTS_RPC_URL"
5399                );
5400            }
5401            _ => panic!("Expected validation error"),
5402        }
5403
5404        // Restore original values or keep removed if they weren't set
5405        if let Some(val) = original_url {
5406            env::set_var("ELEMENTS_RPC_URL", val);
5407        }
5408        if let Some(val) = original_user {
5409            env::set_var("ELEMENTS_RPC_USER", val);
5410        }
5411        if let Some(val) = original_password {
5412            env::set_var("ELEMENTS_RPC_PASSWORD", val);
5413        }
5414    }
5415
5416    #[test]
5417    #[serial]
5418    fn test_elements_rpc_from_env_success() {
5419        // Store original values to restore later
5420        let original_url = env::var("ELEMENTS_RPC_URL").ok();
5421        let original_user = env::var("ELEMENTS_RPC_USER").ok();
5422        let original_password = env::var("ELEMENTS_RPC_PASSWORD").ok();
5423
5424        // Set test values
5425        env::set_var("ELEMENTS_RPC_URL", "http://localhost:18884");
5426        env::set_var("ELEMENTS_RPC_USER", "testuser");
5427        env::set_var("ELEMENTS_RPC_PASSWORD", "testpass");
5428
5429        let result = ElementsRpc::from_env();
5430        assert!(
5431            result.is_ok(),
5432            "ElementsRpc::from_env() should succeed when all env vars are set"
5433        );
5434
5435        let rpc = result.unwrap();
5436        assert_eq!(rpc.base_url, "http://localhost:18884");
5437        assert_eq!(rpc.username, "testuser");
5438        assert_eq!(rpc.password, "testpass");
5439
5440        // Restore original values or remove if they weren't set
5441        match original_url {
5442            Some(val) => env::set_var("ELEMENTS_RPC_URL", val),
5443            None => env::remove_var("ELEMENTS_RPC_URL"),
5444        }
5445        match original_user {
5446            Some(val) => env::set_var("ELEMENTS_RPC_USER", val),
5447            None => env::remove_var("ELEMENTS_RPC_USER"),
5448        }
5449        match original_password {
5450            Some(val) => env::set_var("ELEMENTS_RPC_PASSWORD", val),
5451            None => env::remove_var("ELEMENTS_RPC_PASSWORD"),
5452        }
5453    }
5454
5455    #[test]
5456    fn test_elements_rpc_method_signatures() {
5457        // Test that all new methods have correct signatures and can be called
5458        let rpc = ElementsRpc::new(
5459            "http://localhost:18884".to_string(),
5460            "user".to_string(),
5461            "pass".to_string(),
5462        );
5463
5464        // Test that methods exist and have correct signatures (compilation test)
5465        let _: std::pin::Pin<
5466            Box<dyn std::future::Future<Output = Result<Vec<Unspent>, AmpError>> + Send + '_>,
5467        > = Box::pin(rpc.list_unspent(Some("test_asset")));
5468
5469        let inputs = vec![TxInput {
5470            txid: "test_txid".to_string(),
5471            vout: 0,
5472            sequence: None,
5473        }];
5474        let outputs = std::collections::HashMap::new();
5475        let assets = std::collections::HashMap::new();
5476
5477        let _: std::pin::Pin<
5478            Box<dyn std::future::Future<Output = Result<String, AmpError>> + Send + '_>,
5479        > = Box::pin(rpc.create_raw_transaction(inputs, outputs, assets));
5480
5481        let _: std::pin::Pin<
5482            Box<dyn std::future::Future<Output = Result<String, AmpError>> + Send + '_>,
5483        > = Box::pin(rpc.send_raw_transaction("test_hex"));
5484
5485        let _: std::pin::Pin<
5486            Box<dyn std::future::Future<Output = Result<TransactionDetail, AmpError>> + Send + '_>,
5487        > = Box::pin(rpc.get_transaction("test_txid"));
5488    }
5489
5490    // Mock RPC response tests for UTXO and transaction operations
5491
5492    #[tokio::test]
5493    async fn test_get_network_info_success() {
5494        let server = MockServer::start();
5495
5496        let mock_response = serde_json::json!({
5497            "jsonrpc": "1.0",
5498            "id": "amp-client",
5499            "result": {
5500                "version": 220000,
5501                "subversion": "/Liquid:22.0.0/",
5502                "protocolversion": 70016,
5503                "localservices": "0000000000000409",
5504                "localrelay": true,
5505                "timeoffset": 0,
5506                "networkactive": true,
5507                "connections": 8,
5508                "networks": [],
5509                "relayfee": 0.00001000,
5510                "incrementalfee": 0.00001000,
5511                "localaddresses": [],
5512                "warnings": ""
5513            }
5514        });
5515
5516        let mock = server.mock(|when, then| {
5517            when.method(POST)
5518                .path("/")
5519                .header("authorization", "Basic dXNlcjpwYXNz") // base64 of "user:pass"
5520                .json_body(serde_json::json!({
5521                    "jsonrpc": "1.0",
5522                    "id": "amp-client",
5523                    "method": "getnetworkinfo",
5524                    "params": []
5525                }));
5526            then.status(200)
5527                .header("content-type", "application/json")
5528                .json_body(mock_response);
5529        });
5530
5531        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5532        let result = rpc.get_network_info().await;
5533
5534        assert!(result.is_ok());
5535        let network_info = result.unwrap();
5536        assert_eq!(network_info.version, 220000);
5537        assert_eq!(network_info.subversion, "/Liquid:22.0.0/");
5538        assert_eq!(network_info.connections, 8);
5539
5540        mock.assert();
5541    }
5542
5543    #[tokio::test]
5544    async fn test_get_blockchain_info_success() {
5545        let server = MockServer::start();
5546
5547        let mock_response = serde_json::json!({
5548            "jsonrpc": "1.0",
5549            "id": "amp-client",
5550            "result": {
5551                "chain": "liquidregtest",
5552                "blocks": 12345,
5553                "headers": 12345,
5554                "bestblockhash": "abc123def456789",
5555                "difficulty": 4.656542373906925e-10,
5556                "mediantime": 1640995200,
5557                "verificationprogress": 1.0,
5558                "initialblockdownload": false,
5559                "chainwork": "0000000000000000000000000000000000000000000000000000000000003039",
5560                "size_on_disk": 1234567,
5561                "pruned": false,
5562                "softforks": {},
5563                "warnings": ""
5564            }
5565        });
5566
5567        let mock = server.mock(|when, then| {
5568            when.method(POST)
5569                .path("/")
5570                .header("authorization", "Basic dXNlcjpwYXNz")
5571                .json_body(serde_json::json!({
5572                    "jsonrpc": "1.0",
5573                    "id": "amp-client",
5574                    "method": "getblockchaininfo",
5575                    "params": []
5576                }));
5577            then.status(200)
5578                .header("content-type", "application/json")
5579                .json_body(mock_response);
5580        });
5581
5582        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5583        let result = rpc.get_blockchain_info().await;
5584
5585        assert!(result.is_ok());
5586        let blockchain_info = result.unwrap();
5587        assert_eq!(blockchain_info.chain, "liquidregtest");
5588        assert_eq!(blockchain_info.blocks, 12345);
5589        assert_eq!(blockchain_info.bestblockhash, "abc123def456789");
5590
5591        mock.assert();
5592    }
5593
5594    #[tokio::test]
5595    async fn test_list_unspent_with_asset_filter() {
5596        let server = MockServer::start();
5597
5598        let mock_response = serde_json::json!({
5599            "jsonrpc": "1.0",
5600            "id": "amp-client",
5601            "result": [
5602                {
5603                    "txid": "abc123def456789",
5604                    "vout": 0,
5605                    "amount": 100.0,
5606                    "asset": "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d",
5607                    "address": "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq",
5608                    "spendable": true,
5609                    "confirmations": 6,
5610                    "scriptpubkey": "76a914abc123def456789abc123def456789abc123de88ac"
5611                },
5612                {
5613                    "txid": "def456abc123789",
5614                    "vout": 1,
5615                    "amount": 50.0,
5616                    "asset": "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d",
5617                    "address": "lq1qq3xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq",
5618                    "spendable": true,
5619                    "confirmations": 3
5620                }
5621            ]
5622        });
5623
5624        let asset_id = "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d";
5625
5626        let mock = server.mock(|when, then| {
5627            when.method(POST)
5628                .path("/")
5629                .header("authorization", "Basic dXNlcjpwYXNz")
5630                .json_body(serde_json::json!({
5631                    "jsonrpc": "1.0",
5632                    "id": "amp-client",
5633                    "method": "listunspent",
5634                    "params": [1, 9999999, [], true, {"asset": asset_id}]
5635                }));
5636            then.status(200)
5637                .header("content-type", "application/json")
5638                .json_body(mock_response);
5639        });
5640
5641        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5642        let result = rpc.list_unspent(Some(asset_id)).await;
5643
5644        assert!(result.is_ok());
5645        let utxos = result.unwrap();
5646        assert_eq!(utxos.len(), 2);
5647        assert_eq!(utxos[0].txid, "abc123def456789");
5648        assert_eq!(utxos[0].amount, 100.0);
5649        assert_eq!(utxos[0].asset, asset_id);
5650        assert_eq!(utxos[1].txid, "def456abc123789");
5651        assert_eq!(utxos[1].amount, 50.0);
5652
5653        mock.assert();
5654    }
5655
5656    #[tokio::test]
5657    async fn test_list_unspent_without_filter() {
5658        let server = MockServer::start();
5659
5660        let mock_response = serde_json::json!({
5661            "jsonrpc": "1.0",
5662            "id": "amp-client",
5663            "result": [
5664                {
5665                    "txid": "ghi789jkl012345",
5666                    "vout": 0,
5667                    "amount": 25.0,
5668                    "asset": "different_asset_id",
5669                    "address": "lq1qq4xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq",
5670                    "spendable": true,
5671                    "confirmations": 10
5672                }
5673            ]
5674        });
5675
5676        let mock = server.mock(|when, then| {
5677            when.method(POST)
5678                .path("/")
5679                .header("authorization", "Basic dXNlcjpwYXNz")
5680                .json_body(serde_json::json!({
5681                    "jsonrpc": "1.0",
5682                    "id": "amp-client",
5683                    "method": "listunspent",
5684                    "params": [1, 9999999, [], true]
5685                }));
5686            then.status(200)
5687                .header("content-type", "application/json")
5688                .json_body(mock_response);
5689        });
5690
5691        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5692        let result = rpc.list_unspent(None).await;
5693
5694        assert!(result.is_ok());
5695        let utxos = result.unwrap();
5696        assert_eq!(utxos.len(), 1);
5697        assert_eq!(utxos[0].txid, "ghi789jkl012345");
5698        assert_eq!(utxos[0].amount, 25.0);
5699
5700        mock.assert();
5701    }
5702
5703    #[tokio::test]
5704    async fn test_create_raw_transaction_success() {
5705        let server = MockServer::start();
5706
5707        let mock_response = serde_json::json!({
5708            "jsonrpc": "1.0",
5709            "id": "amp-client",
5710            "result": "0200000000010abc123def456789abc123def456789abc123def456789abc123def456789abc123def456789000000006b483045022100..."
5711        });
5712
5713        let mock = server.mock(|when, then| {
5714            when.method(POST)
5715                .path("/")
5716                .header("authorization", "Basic dXNlcjpwYXNz")
5717                .json_body(serde_json::json!({
5718                    "jsonrpc": "1.0",
5719                    "id": "amp-client",
5720                    "method": "createrawtransaction",
5721                    "params": [
5722                        [
5723                            {
5724                                "txid": "input_txid_123",
5725                                "vout": 0,
5726                                "sequence": 4294967295u32
5727                            }
5728                        ],
5729                        {
5730                            "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq": 100.0
5731                        },
5732                        0,
5733                        false,
5734                        {
5735                            "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq": "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d"
5736                        }
5737                    ]
5738                }));
5739            then.status(200)
5740                .header("content-type", "application/json")
5741                .json_body(mock_response);
5742        });
5743
5744        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5745
5746        let inputs = vec![TxInput {
5747            txid: "input_txid_123".to_string(),
5748            vout: 0,
5749            sequence: Some(0xffffffff),
5750        }];
5751
5752        let mut outputs = HashMap::new();
5753        outputs.insert(
5754            "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
5755            100.0,
5756        );
5757
5758        let mut assets = HashMap::new();
5759        assets.insert(
5760            "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
5761            "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d".to_string(),
5762        );
5763
5764        let result = rpc.create_raw_transaction(inputs, outputs, assets).await;
5765
5766        assert!(result.is_ok());
5767        let raw_tx = result.unwrap();
5768        assert!(raw_tx.starts_with("0200000000010abc123def456789"));
5769
5770        mock.assert();
5771    }
5772
5773    #[tokio::test]
5774    async fn test_send_raw_transaction_success() {
5775        let server = MockServer::start();
5776
5777        let mock_response = serde_json::json!({
5778            "jsonrpc": "1.0",
5779            "id": "amp-client",
5780            "result": "abc123def456789abc123def456789abc123def456789abc123def456789abc123de"
5781        });
5782
5783        let signed_tx_hex = "0200000000010abc123def456789abc123def456789abc123def456789abc123def456789abc123def456789000000006b483045022100...";
5784
5785        let mock = server.mock(|when, then| {
5786            when.method(POST)
5787                .path("/")
5788                .header("authorization", "Basic dXNlcjpwYXNz")
5789                .json_body(serde_json::json!({
5790                    "jsonrpc": "1.0",
5791                    "id": "amp-client",
5792                    "method": "sendrawtransaction",
5793                    "params": [signed_tx_hex]
5794                }));
5795            then.status(200)
5796                .header("content-type", "application/json")
5797                .json_body(mock_response);
5798        });
5799
5800        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5801        let result = rpc.send_raw_transaction(signed_tx_hex).await;
5802
5803        assert!(result.is_ok());
5804        let txid = result.unwrap();
5805        assert_eq!(
5806            txid,
5807            "abc123def456789abc123def456789abc123def456789abc123def456789abc123de"
5808        );
5809
5810        mock.assert();
5811    }
5812
5813    #[tokio::test]
5814    async fn test_get_transaction_success() {
5815        let server = MockServer::start();
5816
5817        let mock_response = serde_json::json!({
5818            "jsonrpc": "1.0",
5819            "id": "amp-client",
5820            "result": {
5821                "txid": "abc123def456789abc123def456789abc123def456789abc123def456789abc123de",
5822                "confirmations": 6,
5823                "blockheight": 12345,
5824                "hex": "0200000000010abc123def456789...",
5825                "blockhash": "def456abc123789def456abc123789def456abc123789def456abc123789def456ab",
5826                "blocktime": 1640995200,
5827                "time": 1640995200,
5828                "timereceived": 1640995180
5829            }
5830        });
5831
5832        let txid = "abc123def456789abc123def456789abc123def456789abc123def456789abc123de";
5833
5834        let mock = server.mock(|when, then| {
5835            when.method(POST)
5836                .path("/")
5837                .header("authorization", "Basic dXNlcjpwYXNz")
5838                .json_body(serde_json::json!({
5839                    "jsonrpc": "1.0",
5840                    "id": "amp-client",
5841                    "method": "gettransaction",
5842                    "params": [txid, true]
5843                }));
5844            then.status(200)
5845                .header("content-type", "application/json")
5846                .json_body(mock_response);
5847        });
5848
5849        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5850        let result = rpc.get_transaction(txid).await;
5851
5852        assert!(result.is_ok());
5853        let tx_detail = result.unwrap();
5854        assert_eq!(tx_detail.txid, txid);
5855        assert_eq!(tx_detail.confirmations, 6);
5856        assert_eq!(tx_detail.blockheight, Some(12345));
5857        assert_eq!(tx_detail.blocktime, Some(1640995200));
5858
5859        mock.assert();
5860    }
5861
5862    // Error handling tests
5863
5864    #[tokio::test]
5865    async fn test_rpc_call_network_failure() {
5866        // Use an invalid URL to simulate network failure
5867        let rpc = ElementsRpc::new(
5868            "http://invalid-host:99999".to_string(),
5869            "user".to_string(),
5870            "pass".to_string(),
5871        );
5872
5873        let result = rpc.get_network_info().await;
5874        assert!(result.is_err());
5875
5876        match result.unwrap_err() {
5877            AmpError::Rpc(msg) => {
5878                assert!(msg.contains("Failed to send RPC request"));
5879            }
5880            _ => panic!("Expected RPC error for network failure"),
5881        }
5882    }
5883
5884    #[tokio::test]
5885    async fn test_rpc_call_http_error_status() {
5886        let server = MockServer::start();
5887
5888        let mock = server.mock(|when, then| {
5889            when.method(POST).path("/");
5890            then.status(500)
5891                .header("content-type", "application/json")
5892                .body("Internal Server Error");
5893        });
5894
5895        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5896        let result = rpc.get_network_info().await;
5897
5898        assert!(result.is_err());
5899        match result.unwrap_err() {
5900            AmpError::Rpc(msg) => {
5901                assert!(msg.contains("RPC request failed with status: 500"));
5902            }
5903            _ => panic!("Expected RPC error for HTTP error status"),
5904        }
5905
5906        mock.assert();
5907    }
5908
5909    #[tokio::test]
5910    async fn test_rpc_call_invalid_json_response() {
5911        let server = MockServer::start();
5912
5913        let mock = server.mock(|when, then| {
5914            when.method(POST).path("/");
5915            then.status(200)
5916                .header("content-type", "application/json")
5917                .body("invalid json response");
5918        });
5919
5920        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5921        let result = rpc.get_network_info().await;
5922
5923        assert!(result.is_err());
5924        match result.unwrap_err() {
5925            AmpError::Rpc(msg) => {
5926                assert!(msg.contains("Failed to parse RPC response"));
5927            }
5928            _ => panic!("Expected RPC error for invalid JSON"),
5929        }
5930
5931        mock.assert();
5932    }
5933
5934    #[tokio::test]
5935    async fn test_rpc_call_error_response() {
5936        let server = MockServer::start();
5937
5938        let mock_response = serde_json::json!({
5939            "jsonrpc": "1.0",
5940            "id": "amp-client",
5941            "result": null,
5942            "error": {
5943                "code": -32601,
5944                "message": "Method not found"
5945            }
5946        });
5947
5948        let mock = server.mock(|when, then| {
5949            when.method(POST).path("/");
5950            then.status(200)
5951                .header("content-type", "application/json")
5952                .json_body(mock_response);
5953        });
5954
5955        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5956        let result = rpc.get_network_info().await;
5957
5958        assert!(result.is_err());
5959        match result.unwrap_err() {
5960            AmpError::Rpc(msg) => {
5961                assert!(msg.contains("RPC error -32601: Method not found"));
5962            }
5963            _ => panic!("Expected RPC error for error response"),
5964        }
5965
5966        mock.assert();
5967    }
5968
5969    #[tokio::test]
5970    async fn test_rpc_call_missing_result() {
5971        let server = MockServer::start();
5972
5973        let mock_response = serde_json::json!({
5974            "jsonrpc": "1.0",
5975            "id": "amp-client",
5976            "result": null,
5977            "error": null
5978        });
5979
5980        let mock = server.mock(|when, then| {
5981            when.method(POST).path("/");
5982            then.status(200)
5983                .header("content-type", "application/json")
5984                .json_body(mock_response);
5985        });
5986
5987        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5988        let result = rpc.get_network_info().await;
5989
5990        assert!(result.is_err());
5991        match result.unwrap_err() {
5992            AmpError::Rpc(msg) => {
5993                assert!(msg.contains("RPC response missing result field"));
5994            }
5995            _ => panic!("Expected RPC error for missing result"),
5996        }
5997
5998        mock.assert();
5999    }
6000
6001    // Authentication tests
6002
6003    #[tokio::test]
6004    async fn test_rpc_authentication_headers() {
6005        let server = MockServer::start();
6006
6007        let mock_response = serde_json::json!({
6008            "jsonrpc": "1.0",
6009            "id": "amp-client",
6010            "result": {
6011                "version": 220000,
6012                "subversion": "/Liquid:22.0.0/",
6013                "protocolversion": 70016,
6014                "localservices": "0000000000000409",
6015                "localrelay": true,
6016                "timeoffset": 0,
6017                "networkactive": true,
6018                "connections": 8,
6019                "networks": [],
6020                "relayfee": 0.00001000,
6021                "incrementalfee": 0.00001000,
6022                "localaddresses": [],
6023                "warnings": ""
6024            }
6025        });
6026
6027        // Test with custom username and password
6028        let mock = server.mock(|when, then| {
6029            when.method(POST)
6030                .path("/")
6031                .header("authorization", "Basic dGVzdHVzZXI6dGVzdHBhc3M=") // base64 of "testuser:testpass"
6032                .json_body(serde_json::json!({
6033                    "jsonrpc": "1.0",
6034                    "id": "amp-client",
6035                    "method": "getnetworkinfo",
6036                    "params": []
6037                }));
6038            then.status(200)
6039                .header("content-type", "application/json")
6040                .json_body(mock_response);
6041        });
6042
6043        let rpc = ElementsRpc::new(
6044            server.url("/"),
6045            "testuser".to_string(),
6046            "testpass".to_string(),
6047        );
6048        let result = rpc.get_network_info().await;
6049
6050        assert!(result.is_ok());
6051        mock.assert();
6052    }
6053
6054    // Wallet passphrase tests
6055
6056    #[tokio::test]
6057    async fn test_wallet_passphrase_success() {
6058        let server = MockServer::start();
6059
6060        let mock_response = serde_json::json!({
6061            "jsonrpc": "1.0",
6062            "id": "amp-client",
6063            "result": null
6064        });
6065
6066        let mock = server.mock(|when, then| {
6067            when.method(POST)
6068                .path("/")
6069                .header("authorization", "Basic dXNlcjpwYXNz")
6070                .json_body(serde_json::json!({
6071                    "jsonrpc": "1.0",
6072                    "id": "amp-client",
6073                    "method": "walletpassphrase",
6074                    "params": ["my_passphrase", 300]
6075                }));
6076            then.status(200)
6077                .header("content-type", "application/json")
6078                .json_body(mock_response);
6079        });
6080
6081        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
6082        let result = rpc.wallet_passphrase("my_passphrase", 300).await;
6083
6084        assert!(result.is_ok());
6085        mock.assert();
6086    }
6087
6088    // Connection validation tests
6089
6090    #[tokio::test]
6091    async fn test_validate_connection_success() {
6092        let server = MockServer::start();
6093
6094        let mock_response = serde_json::json!({
6095            "jsonrpc": "1.0",
6096            "id": "amp-client",
6097            "result": {
6098                "version": 220000,
6099                "subversion": "/Liquid:22.0.0/",
6100                "protocolversion": 70016,
6101                "localservices": "0000000000000409",
6102                "localrelay": true,
6103                "timeoffset": 0,
6104                "networkactive": true,
6105                "connections": 8,
6106                "networks": [],
6107                "relayfee": 0.00001000,
6108                "incrementalfee": 0.00001000,
6109                "localaddresses": [],
6110                "warnings": ""
6111            }
6112        });
6113
6114        let mock = server.mock(|when, then| {
6115            when.method(POST).path("/");
6116            then.status(200)
6117                .header("content-type", "application/json")
6118                .json_body(mock_response);
6119        });
6120
6121        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
6122        let result = rpc.validate_connection().await;
6123
6124        assert!(result.is_ok());
6125        mock.assert();
6126    }
6127
6128    #[tokio::test]
6129    async fn test_get_node_status_success() {
6130        let server = MockServer::start();
6131
6132        let network_mock_response = serde_json::json!({
6133            "jsonrpc": "1.0",
6134            "id": "amp-client",
6135            "result": {
6136                "version": 220000,
6137                "subversion": "/Liquid:22.0.0/",
6138                "protocolversion": 70016,
6139                "localservices": "0000000000000409",
6140                "localrelay": true,
6141                "timeoffset": 0,
6142                "networkactive": true,
6143                "connections": 8,
6144                "networks": [],
6145                "relayfee": 0.00001000,
6146                "incrementalfee": 0.00001000,
6147                "localaddresses": [],
6148                "warnings": ""
6149            }
6150        });
6151
6152        let blockchain_mock_response = serde_json::json!({
6153            "jsonrpc": "1.0",
6154            "id": "amp-client",
6155            "result": {
6156                "chain": "liquidregtest",
6157                "blocks": 12345,
6158                "headers": 12345,
6159                "bestblockhash": "abc123def456789",
6160                "difficulty": 4.656542373906925e-10,
6161                "mediantime": 1640995200,
6162                "verificationprogress": 1.0,
6163                "initialblockdownload": false,
6164                "chainwork": "0000000000000000000000000000000000000000000000000000000000003039",
6165                "size_on_disk": 1234567,
6166                "pruned": false,
6167                "softforks": {},
6168                "warnings": ""
6169            }
6170        });
6171
6172        let network_mock = server.mock(|when, then| {
6173            when.method(POST).path("/").json_body(serde_json::json!({
6174                "jsonrpc": "1.0",
6175                "id": "amp-client",
6176                "method": "getnetworkinfo",
6177                "params": []
6178            }));
6179            then.status(200)
6180                .header("content-type", "application/json")
6181                .json_body(network_mock_response);
6182        });
6183
6184        let blockchain_mock = server.mock(|when, then| {
6185            when.method(POST).path("/").json_body(serde_json::json!({
6186                "jsonrpc": "1.0",
6187                "id": "amp-client",
6188                "method": "getblockchaininfo",
6189                "params": []
6190            }));
6191            then.status(200)
6192                .header("content-type", "application/json")
6193                .json_body(blockchain_mock_response);
6194        });
6195
6196        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
6197        let result = rpc.get_node_status().await;
6198
6199        assert!(result.is_ok());
6200        let (network_info, blockchain_info) = result.unwrap();
6201        assert_eq!(network_info.version, 220000);
6202        assert_eq!(blockchain_info.blocks, 12345);
6203
6204        network_mock.assert();
6205        blockchain_mock.assert();
6206    }
6207
6208    // Tests for UTXO selection and transaction building logic
6209
6210    #[tokio::test]
6211    async fn test_build_distribution_transaction_zero_amount() {
6212        let rpc = ElementsRpc::new(
6213            "http://localhost:18884".to_string(),
6214            "user".to_string(),
6215            "pass".to_string(),
6216        );
6217
6218        let address_amounts = HashMap::new(); // Empty distribution
6219
6220        let result = rpc
6221            .build_distribution_transaction(
6222                "test_wallet",
6223                "asset_id",
6224                address_amounts,
6225                "change_address",
6226                1.0,
6227            )
6228            .await;
6229
6230        assert!(result.is_err());
6231        match result.unwrap_err() {
6232            AmpError::Validation(msg) => {
6233                assert!(msg.contains("Total distribution amount must be greater than zero"));
6234            }
6235            _ => panic!("Expected validation error for zero distribution amount"),
6236        }
6237    }
6238
6239    #[tokio::test]
6240    async fn test_sign_transaction_validation() {
6241        let rpc = ElementsRpc::new(
6242            "http://localhost:18884".to_string(),
6243            "user".to_string(),
6244            "pass".to_string(),
6245        );
6246
6247        // Mock signer for testing
6248        struct MockSigner {
6249            should_fail: bool,
6250            return_value: String,
6251        }
6252
6253        #[async_trait::async_trait]
6254        impl crate::signer::Signer for MockSigner {
6255            async fn sign_transaction(
6256                &self,
6257                _unsigned_tx: &str,
6258            ) -> Result<String, crate::signer::SignerError> {
6259                if self.should_fail {
6260                    Err(crate::signer::SignerError::Lwk(
6261                        "Mock signing failure".to_string(),
6262                    ))
6263                } else {
6264                    // Return a longer hex string to simulate signed transaction (20+ bytes when decoded)
6265                    Ok(format!(
6266                        "{}deadbeefcafebabe1234567890abcdef",
6267                        self.return_value
6268                    ))
6269                }
6270            }
6271
6272            fn as_any(&self) -> &dyn std::any::Any {
6273                self
6274            }
6275        }
6276
6277        // Test empty transaction hex
6278        let mock_signer = MockSigner {
6279            should_fail: false,
6280            return_value: "".to_string(),
6281        };
6282        let result = rpc.sign_transaction("", &mock_signer).await;
6283        assert!(result.is_err());
6284        assert!(result.unwrap_err().to_string().contains("cannot be empty"));
6285
6286        // Test odd length hex
6287        let result = rpc.sign_transaction("abc", &mock_signer).await;
6288        assert!(result.is_err());
6289        assert!(result.unwrap_err().to_string().contains("even length"));
6290
6291        // Test invalid hex characters
6292        let result = rpc.sign_transaction("abcg", &mock_signer).await;
6293        assert!(result.is_err());
6294        assert!(result
6295            .unwrap_err()
6296            .to_string()
6297            .contains("invalid hex characters"));
6298
6299        // Test signer failure
6300        let mock_signer = MockSigner {
6301            should_fail: true,
6302            return_value: "".to_string(),
6303        };
6304        let result = rpc.sign_transaction("abcd", &mock_signer).await;
6305        assert!(result.is_err());
6306        assert!(result
6307            .unwrap_err()
6308            .to_string()
6309            .contains("Mock signing failure"));
6310
6311        // Test successful signing
6312        let mock_signer = MockSigner {
6313            should_fail: false,
6314            return_value: "abcd".to_string(),
6315        };
6316        let result = rpc.sign_transaction("abcd", &mock_signer).await;
6317        if result.is_err() {
6318            println!("Error: {}", result.as_ref().unwrap_err());
6319        }
6320        assert!(result.is_ok());
6321        assert_eq!(result.unwrap(), "abcddeadbeefcafebabe1234567890abcdef");
6322    }
6323
6324    #[tokio::test]
6325    async fn test_sign_transaction_validation_edge_cases() {
6326        let rpc = ElementsRpc::new(
6327            "http://localhost:18884".to_string(),
6328            "user".to_string(),
6329            "pass".to_string(),
6330        );
6331
6332        // Mock signer that returns invalid responses
6333        struct BadMockSigner {
6334            return_empty: bool,
6335            return_odd_length: bool,
6336            return_invalid_hex: bool,
6337            return_shorter: bool,
6338        }
6339
6340        #[async_trait::async_trait]
6341        impl crate::signer::Signer for BadMockSigner {
6342            async fn sign_transaction(
6343                &self,
6344                unsigned_tx: &str,
6345            ) -> Result<String, crate::signer::SignerError> {
6346                if self.return_empty {
6347                    Ok("".to_string())
6348                } else if self.return_odd_length {
6349                    Ok("abc".to_string())
6350                } else if self.return_invalid_hex {
6351                    Ok("abcg".to_string())
6352                } else if self.return_shorter {
6353                    Ok("ab".to_string()) // Shorter than input "abcd"
6354                } else {
6355                    Ok(format!("{}deadbeef", unsigned_tx))
6356                }
6357            }
6358
6359            fn as_any(&self) -> &dyn std::any::Any {
6360                self
6361            }
6362        }
6363
6364        // Test signer returning empty string
6365        let bad_signer = BadMockSigner {
6366            return_empty: true,
6367            return_odd_length: false,
6368            return_invalid_hex: false,
6369            return_shorter: false,
6370        };
6371        let result = rpc.sign_transaction("abcd", &bad_signer).await;
6372        assert!(result.is_err());
6373        assert!(result.unwrap_err().to_string().contains("cannot be empty"));
6374
6375        // Test signer returning odd length hex
6376        let bad_signer = BadMockSigner {
6377            return_empty: false,
6378            return_odd_length: true,
6379            return_invalid_hex: false,
6380            return_shorter: false,
6381        };
6382        let result = rpc.sign_transaction("abcd", &bad_signer).await;
6383        assert!(result.is_err());
6384        assert!(result.unwrap_err().to_string().contains("even length"));
6385
6386        // Test signer returning invalid hex
6387        let bad_signer = BadMockSigner {
6388            return_empty: false,
6389            return_odd_length: false,
6390            return_invalid_hex: true,
6391            return_shorter: false,
6392        };
6393        let result = rpc.sign_transaction("abcd", &bad_signer).await;
6394        assert!(result.is_err());
6395        assert!(result
6396            .unwrap_err()
6397            .to_string()
6398            .contains("invalid hex characters"));
6399
6400        // Test signer returning shorter transaction (invalid)
6401        let bad_signer = BadMockSigner {
6402            return_empty: false,
6403            return_odd_length: false,
6404            return_invalid_hex: false,
6405            return_shorter: true,
6406        };
6407        let result = rpc.sign_transaction("abcd", &bad_signer).await;
6408        assert!(result.is_err());
6409        assert!(result
6410            .unwrap_err()
6411            .to_string()
6412            .contains("shorter than unsigned transaction"));
6413    }
6414
6415    #[tokio::test]
6416    async fn test_sign_transaction_minimum_size_validation() {
6417        let rpc = ElementsRpc::new(
6418            "http://localhost:18884".to_string(),
6419            "user".to_string(),
6420            "pass".to_string(),
6421        );
6422
6423        // Mock signer that returns very small transactions
6424        struct TinyMockSigner;
6425
6426        #[async_trait::async_trait]
6427        impl crate::signer::Signer for TinyMockSigner {
6428            async fn sign_transaction(
6429                &self,
6430                _unsigned_tx: &str,
6431            ) -> Result<String, crate::signer::SignerError> {
6432                Ok("abcd".to_string()) // Only 2 bytes when decoded
6433            }
6434
6435            fn as_any(&self) -> &dyn std::any::Any {
6436                self
6437            }
6438        }
6439
6440        let tiny_signer = TinyMockSigner;
6441        let result = rpc.sign_transaction("abcd", &tiny_signer).await;
6442        assert!(result.is_err());
6443        let error_msg = result.unwrap_err().to_string();
6444        assert!(error_msg.contains("minimum size"));
6445        assert!(error_msg.contains("minimum is 10 bytes"));
6446    }
6447
6448    #[tokio::test]
6449    async fn test_sign_transaction_success_case() {
6450        let rpc = ElementsRpc::new(
6451            "http://localhost:18884".to_string(),
6452            "user".to_string(),
6453            "pass".to_string(),
6454        );
6455
6456        // Mock signer that returns a valid signed transaction
6457        struct GoodMockSigner;
6458
6459        #[async_trait::async_trait]
6460        impl crate::signer::Signer for GoodMockSigner {
6461            async fn sign_transaction(
6462                &self,
6463                unsigned_tx: &str,
6464            ) -> Result<String, crate::signer::SignerError> {
6465                // Return a longer valid hex string (20+ bytes when decoded)
6466                Ok(format!("{}deadbeefcafebabe1234567890abcdef", unsigned_tx))
6467            }
6468
6469            fn as_any(&self) -> &dyn std::any::Any {
6470                self
6471            }
6472        }
6473
6474        let good_signer = GoodMockSigner;
6475
6476        // Test with a reasonable sized unsigned transaction
6477        let unsigned_tx = "0200000000010123456789abcdef"; // 14 bytes when decoded
6478        let result = rpc.sign_transaction(unsigned_tx, &good_signer).await;
6479
6480        assert!(result.is_ok());
6481        let signed_tx = result.unwrap();
6482        assert!(signed_tx.starts_with(unsigned_tx));
6483        assert!(signed_tx.len() > unsigned_tx.len());
6484        assert!(signed_tx.contains("deadbeefcafebabe"));
6485    }
6486
6487    #[tokio::test]
6488    async fn test_sign_and_broadcast_transaction_mock() {
6489        // Create a mock server for testing the broadcast part
6490        let server = MockServer::start();
6491
6492        // Mock the RPC response for sendrawtransaction
6493        let mock = server.mock(|when, then| {
6494            when.method(POST).path("/").json_body(serde_json::json!({
6495                "jsonrpc": "1.0",
6496                "id": "amp-client",
6497                "method": "sendrawtransaction",
6498                "params": ["0200000000010123456789abcdefdeadbeefcafebabe1234567890abcdef"]
6499            }));
6500            then.status(200).json_body(serde_json::json!({
6501                "jsonrpc": "1.0",
6502                "id": "amp-client",
6503                "result": "abc123def456789",
6504                "error": null
6505            }));
6506        });
6507
6508        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
6509
6510        // Mock signer for testing
6511        struct TestMockSigner;
6512
6513        #[async_trait::async_trait]
6514        impl crate::signer::Signer for TestMockSigner {
6515            async fn sign_transaction(
6516                &self,
6517                unsigned_tx: &str,
6518            ) -> Result<String, crate::signer::SignerError> {
6519                Ok(format!("{}deadbeefcafebabe1234567890abcdef", unsigned_tx))
6520            }
6521
6522            fn as_any(&self) -> &dyn std::any::Any {
6523                self
6524            }
6525        }
6526
6527        let signer = TestMockSigner;
6528        let unsigned_tx = "0200000000010123456789abcdef";
6529
6530        let result = rpc
6531            .sign_and_broadcast_transaction(unsigned_tx, &signer)
6532            .await;
6533
6534        assert!(result.is_ok());
6535        assert_eq!(result.unwrap(), "abc123def456789");
6536
6537        // Verify the mock was called
6538        mock.assert();
6539    }
6540
6541    #[tokio::test]
6542    async fn test_sign_and_broadcast_transaction_signing_failure() {
6543        let rpc = ElementsRpc::new(
6544            "http://localhost:18884".to_string(),
6545            "user".to_string(),
6546            "pass".to_string(),
6547        );
6548
6549        // Mock signer that fails
6550        struct FailingSigner;
6551
6552        #[async_trait::async_trait]
6553        impl crate::signer::Signer for FailingSigner {
6554            async fn sign_transaction(
6555                &self,
6556                _unsigned_tx: &str,
6557            ) -> Result<String, crate::signer::SignerError> {
6558                Err(crate::signer::SignerError::Lwk(
6559                    "Signing failed".to_string(),
6560                ))
6561            }
6562
6563            fn as_any(&self) -> &dyn std::any::Any {
6564                self
6565            }
6566        }
6567
6568        let failing_signer = FailingSigner;
6569        let result = rpc
6570            .sign_and_broadcast_transaction("abcd", &failing_signer)
6571            .await;
6572
6573        assert!(result.is_err());
6574        let error_msg = result.unwrap_err().to_string();
6575        // The error should be a Signer error containing the original failure message
6576        assert!(error_msg.contains("Signer error"));
6577        assert!(error_msg.contains("Signing failed"));
6578    }
6579
6580    #[tokio::test]
6581    async fn test_sign_and_broadcast_transaction_broadcast_failure() {
6582        // Create a mock server that returns an error for broadcast
6583        let server = MockServer::start();
6584
6585        let mock = server.mock(|when, then| {
6586            when.method(POST).path("/");
6587            then.status(200).json_body(serde_json::json!({
6588                "jsonrpc": "1.0",
6589                "id": "amp-client",
6590                "result": null,
6591                "error": {
6592                    "code": -26,
6593                    "message": "Transaction rejected"
6594                }
6595            }));
6596        });
6597
6598        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
6599
6600        // Mock signer that succeeds
6601        struct WorkingSigner;
6602
6603        #[async_trait::async_trait]
6604        impl crate::signer::Signer for WorkingSigner {
6605            async fn sign_transaction(
6606                &self,
6607                unsigned_tx: &str,
6608            ) -> Result<String, crate::signer::SignerError> {
6609                Ok(format!("{}deadbeefcafebabe1234567890abcdef", unsigned_tx))
6610            }
6611
6612            fn as_any(&self) -> &dyn std::any::Any {
6613                self
6614            }
6615        }
6616
6617        let working_signer = WorkingSigner;
6618        let unsigned_tx = "0200000000010123456789abcdef";
6619
6620        let result = rpc
6621            .sign_and_broadcast_transaction(unsigned_tx, &working_signer)
6622            .await;
6623
6624        assert!(result.is_err());
6625        let error_msg = result.unwrap_err().to_string();
6626        assert!(error_msg.contains("Failed during transaction broadcast phase"));
6627        assert!(error_msg.contains("Transaction rejected"));
6628
6629        mock.assert();
6630    }
6631
6632    #[tokio::test]
6633    async fn test_wait_for_confirmations_success() {
6634        let server = MockServer::start();
6635
6636        let txid = "abc123def456789abc123def456789abc123def456789abc123def456789abc123de";
6637
6638        // First call returns 1 confirmation (not enough)
6639        let _mock_response_1 = serde_json::json!({
6640            "jsonrpc": "1.0",
6641            "id": "amp-client",
6642            "result": {
6643                "txid": txid,
6644                "confirmations": 1,
6645                "blockheight": 12345,
6646                "hex": "0200000000010abc123def456789...",
6647                "blockhash": "def456abc123789def456abc123789def456abc123789def456abc123789def456ab",
6648                "blocktime": 1640995200,
6649                "time": 1640995200,
6650                "timereceived": 1640995180
6651            }
6652        });
6653
6654        // Second call returns 2 confirmations (sufficient)
6655        let mock_response_2 = serde_json::json!({
6656            "jsonrpc": "1.0",
6657            "id": "amp-client",
6658            "result": {
6659                "txid": txid,
6660                "confirmations": 2,
6661                "blockheight": 12345,
6662                "hex": "0200000000010abc123def456789...",
6663                "blockhash": "def456abc123789def456abc123789def456abc123789def456abc123789def456ab",
6664                "blocktime": 1640995200,
6665                "time": 1640995200,
6666                "timereceived": 1640995180
6667            }
6668        });
6669
6670        // Create a mock that returns 2 confirmations immediately (simpler test)
6671        let mock = server.mock(|when, then| {
6672            when.method(POST)
6673                .path("/")
6674                .header("authorization", "Basic dXNlcjpwYXNz")
6675                .json_body(serde_json::json!({
6676                    "jsonrpc": "1.0",
6677                    "id": "amp-client",
6678                    "method": "gettransaction",
6679                    "params": [txid, true]
6680                }));
6681            then.status(200)
6682                .header("content-type", "application/json")
6683                .json_body(mock_response_2); // Return sufficient confirmations immediately
6684        });
6685
6686        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
6687
6688        // Use fast polling (1 second) for testing
6689        let result = rpc
6690            .wait_for_confirmations_with_interval(txid, Some(2), Some(1), Some(1))
6691            .await;
6692
6693        assert!(result.is_ok());
6694        let tx_detail = result.unwrap();
6695        assert_eq!(tx_detail.confirmations, 2);
6696        assert_eq!(tx_detail.txid, txid);
6697
6698        // Mock should have been called once
6699        mock.assert();
6700    }
6701
6702    #[tokio::test]
6703    async fn test_wait_for_confirmations_timeout() {
6704        let server = MockServer::start();
6705
6706        let txid = "abc123def456789abc123def456789abc123def456789abc123def456789abc123de";
6707
6708        // Always return insufficient confirmations
6709        let mock_response = serde_json::json!({
6710            "jsonrpc": "1.0",
6711            "id": "amp-client",
6712            "result": {
6713                "txid": txid,
6714                "confirmations": 1,
6715                "blockheight": 12345,
6716                "hex": "0200000000010abc123def456789...",
6717                "blockhash": null,
6718                "blocktime": null,
6719                "time": null,
6720                "timereceived": null
6721            }
6722        });
6723
6724        let _mock = server.mock(|when, then| {
6725            when.method(POST)
6726                .path("/")
6727                .header("authorization", "Basic dXNlcjpwYXNz")
6728                .json_body(serde_json::json!({
6729                    "jsonrpc": "1.0",
6730                    "id": "amp-client",
6731                    "method": "gettransaction",
6732                    "params": [txid, true]
6733                }));
6734            then.status(200)
6735                .header("content-type", "application/json")
6736                .json_body(mock_response);
6737        });
6738
6739        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
6740
6741        // Use a very short timeout for testing (0 = 3 seconds) and fast polling (1 second)
6742        let result = rpc
6743            .wait_for_confirmations_with_interval(txid, Some(2), Some(0), Some(1))
6744            .await;
6745
6746        assert!(result.is_err());
6747        match result.unwrap_err() {
6748            AmpError::Timeout(msg) => {
6749                assert!(msg.contains("Timeout waiting for confirmations"));
6750                assert!(msg.contains(txid));
6751                assert!(msg.contains("retry confirmation"));
6752            }
6753            _ => panic!("Expected timeout error"),
6754        }
6755
6756        // Mock will be called multiple times during the timeout period
6757        // We don't assert on the exact number since it depends on timing
6758    }
6759
6760    #[tokio::test]
6761    async fn test_wait_for_confirmations_immediate_success() {
6762        let server = MockServer::start();
6763
6764        let txid = "abc123def456789abc123def456789abc123def456789abc123def456789abc123de";
6765
6766        // Transaction already has sufficient confirmations
6767        let mock_response = serde_json::json!({
6768            "jsonrpc": "1.0",
6769            "id": "amp-client",
6770            "result": {
6771                "txid": txid,
6772                "confirmations": 5,
6773                "blockheight": 12345,
6774                "hex": "0200000000010abc123def456789...",
6775                "blockhash": "def456abc123789def456abc123789def456abc123789def456abc123789def456ab",
6776                "blocktime": 1640995200,
6777                "time": 1640995200,
6778                "timereceived": 1640995180
6779            }
6780        });
6781
6782        let mock = server.mock(|when, then| {
6783            when.method(POST)
6784                .path("/")
6785                .header("authorization", "Basic dXNlcjpwYXNz")
6786                .json_body(serde_json::json!({
6787                    "jsonrpc": "1.0",
6788                    "id": "amp-client",
6789                    "method": "gettransaction",
6790                    "params": [txid, true]
6791                }));
6792            then.status(200)
6793                .header("content-type", "application/json")
6794                .json_body(mock_response);
6795        });
6796
6797        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
6798
6799        let result = rpc.wait_for_confirmations(txid, Some(2), Some(10)).await;
6800
6801        assert!(result.is_ok());
6802        let tx_detail = result.unwrap();
6803        assert_eq!(tx_detail.confirmations, 5);
6804        assert_eq!(tx_detail.txid, txid);
6805
6806        // Should only need one call since confirmations are already sufficient
6807        mock.assert();
6808    }
6809}
6810
6811/// Configuration for retry behavior in API requests
6812#[derive(Debug, Clone)]
6813pub struct RetryConfig {
6814    /// Maximum number of retry attempts
6815    pub max_attempts: u32,
6816    /// Base delay in milliseconds for exponential backoff
6817    pub base_delay_ms: u64,
6818    /// Maximum delay in milliseconds to cap exponential backoff
6819    pub max_delay_ms: u64,
6820    /// Request timeout in seconds
6821    pub timeout_seconds: u64,
6822}
6823
6824impl Default for RetryConfig {
6825    fn default() -> Self {
6826        Self {
6827            max_attempts: 3,
6828            base_delay_ms: 1000,
6829            max_delay_ms: 30_000,
6830            timeout_seconds: 10,
6831        }
6832    }
6833}
6834
6835impl RetryConfig {
6836    /// Creates a `RetryConfig` from environment variables with default fallbacks
6837    ///
6838    /// Environment variables:
6839    /// - `API_RETRY_MAX_ATTEMPTS`: Maximum retry attempts (default: 3)
6840    /// - `API_RETRY_BASE_DELAY_MS`: Base delay in milliseconds (default: 1000)
6841    /// - `API_RETRY_MAX_DELAY_MS`: Maximum delay in milliseconds (default: 30000)
6842    /// - `API_REQUEST_TIMEOUT_SECONDS`: Request timeout in seconds (default: 10)
6843    ///
6844    /// # Errors
6845    ///
6846    /// Returns an error if any environment variable contains an invalid value
6847    pub fn from_env() -> Result<Self, Error> {
6848        let max_attempts = match env::var("API_RETRY_MAX_ATTEMPTS") {
6849            Ok(val) => val.parse::<u32>().map_err(|e| {
6850                Error::InvalidRetryConfig(format!("Invalid API_RETRY_MAX_ATTEMPTS: {e}"))
6851            })?,
6852            Err(_) => 3,
6853        };
6854
6855        let base_delay_ms = match env::var("API_RETRY_BASE_DELAY_MS") {
6856            Ok(val) => val.parse::<u64>().map_err(|e| {
6857                Error::InvalidRetryConfig(format!("Invalid API_RETRY_BASE_DELAY_MS: {e}"))
6858            })?,
6859            Err(_) => 1000,
6860        };
6861
6862        let max_delay_ms = match env::var("API_RETRY_MAX_DELAY_MS") {
6863            Ok(val) => val.parse::<u64>().map_err(|e| {
6864                Error::InvalidRetryConfig(format!("Invalid API_RETRY_MAX_DELAY_MS: {e}"))
6865            })?,
6866            Err(_) => 30_000,
6867        };
6868
6869        let timeout_seconds = match env::var("API_REQUEST_TIMEOUT_SECONDS") {
6870            Ok(val) => val.parse::<u64>().map_err(|e| {
6871                Error::InvalidRetryConfig(format!("Invalid API_REQUEST_TIMEOUT_SECONDS: {e}"))
6872            })?,
6873            Err(_) => 10,
6874        };
6875
6876        // Validate configuration
6877        if max_attempts == 0 {
6878            return Err(Error::InvalidRetryConfig(
6879                "max_attempts must be greater than 0".to_string(),
6880            ));
6881        }
6882        if base_delay_ms == 0 {
6883            return Err(Error::InvalidRetryConfig(
6884                "base_delay_ms must be greater than 0".to_string(),
6885            ));
6886        }
6887        if max_delay_ms < base_delay_ms {
6888            return Err(Error::InvalidRetryConfig(
6889                "max_delay_ms must be greater than or equal to base_delay_ms".to_string(),
6890            ));
6891        }
6892        if timeout_seconds == 0 {
6893            return Err(Error::InvalidRetryConfig(
6894                "timeout_seconds must be greater than 0".to_string(),
6895            ));
6896        }
6897
6898        Ok(Self {
6899            max_attempts,
6900            base_delay_ms,
6901            max_delay_ms,
6902            timeout_seconds,
6903        })
6904    }
6905
6906    /// Creates a `RetryConfig` optimized for test environments
6907    ///
6908    /// Uses reduced values for faster test execution:
6909    /// - 2 retry attempts
6910    /// - 500ms base delay
6911    /// - 5000ms max delay
6912    /// - 5 second timeout
6913    #[must_use]
6914    pub const fn for_tests() -> Self {
6915        Self {
6916            max_attempts: 2,
6917            base_delay_ms: 500,
6918            max_delay_ms: 5000,
6919            timeout_seconds: 5,
6920        }
6921    }
6922
6923    /// Sets a custom timeout value
6924    #[must_use]
6925    pub const fn with_timeout(mut self, timeout_seconds: u64) -> Self {
6926        self.timeout_seconds = timeout_seconds;
6927        self
6928    }
6929
6930    /// Sets custom max attempts
6931    #[must_use]
6932    pub const fn with_max_attempts(mut self, max_attempts: u32) -> Self {
6933        self.max_attempts = max_attempts;
6934        self
6935    }
6936
6937    /// Sets custom base delay
6938    #[must_use]
6939    pub const fn with_base_delay_ms(mut self, base_delay_ms: u64) -> Self {
6940        self.base_delay_ms = base_delay_ms;
6941        self
6942    }
6943
6944    /// Sets custom max delay
6945    #[must_use]
6946    pub const fn with_max_delay_ms(mut self, max_delay_ms: u64) -> Self {
6947        self.max_delay_ms = max_delay_ms;
6948        self
6949    }
6950}
6951
6952/// HTTP client with sophisticated retry logic and exponential backoff
6953#[derive(Debug, Clone)]
6954pub struct RetryClient {
6955    client: Client,
6956    config: RetryConfig,
6957}
6958
6959impl RetryClient {
6960    /// Creates a new `RetryClient` with the given configuration
6961    #[must_use]
6962    pub fn new(config: RetryConfig) -> Self {
6963        Self {
6964            client: Client::new(),
6965            config,
6966        }
6967    }
6968
6969    /// Creates a new `RetryClient` with default configuration
6970    #[must_use]
6971    pub fn with_default_config() -> Self {
6972        Self::new(RetryConfig::default())
6973    }
6974
6975    /// Creates a new `RetryClient` with test-optimized configuration
6976    #[must_use]
6977    pub fn for_tests() -> Self {
6978        Self::new(RetryConfig::for_tests())
6979    }
6980
6981    /// Executes an HTTP request with retry logic and exponential backoff
6982    ///
6983    /// # Arguments
6984    /// * `request_builder` - A function that creates the request builder
6985    ///
6986    /// # Returns
6987    /// The response if successful, or an error after all retries are exhausted
6988    ///
6989    /// # Errors
6990    /// Returns `TokenError::Timeout` if the request times out
6991    /// Returns `TokenError::RateLimited` if rate limited and retries are exhausted
6992    /// Returns `TokenError::ObtainFailed` if all retry attempts fail
6993    #[allow(clippy::cognitive_complexity)]
6994    pub async fn execute_with_retry<F>(
6995        &self,
6996        request_builder: F,
6997    ) -> Result<reqwest::Response, TokenError>
6998    where
6999        F: Fn() -> reqwest::RequestBuilder + Send + Sync,
7000    {
7001        let mut last_error = String::new();
7002        let mut attempt = 0;
7003
7004        while attempt < self.config.max_attempts {
7005            attempt += 1;
7006
7007            // Create the request with timeout
7008            let request =
7009                request_builder().timeout(StdDuration::from_secs(self.config.timeout_seconds));
7010
7011            // Execute the request
7012            match request.send().await {
7013                Ok(response) => {
7014                    let status = response.status();
7015
7016                    // Handle rate limiting (429 Too Many Requests)
7017                    if status == reqwest::StatusCode::TOO_MANY_REQUESTS {
7018                        let retry_after = Self::extract_retry_after(&response).unwrap_or(60);
7019
7020                        tracing::warn!(
7021                            "Rate limited (429) on attempt {}/{}. Retry after {} seconds",
7022                            attempt,
7023                            self.config.max_attempts,
7024                            retry_after
7025                        );
7026
7027                        // If this is our last attempt, return the rate limit error
7028                        if attempt >= self.config.max_attempts {
7029                            return Err(TokenError::rate_limited(retry_after));
7030                        }
7031
7032                        // Wait for the rate limit period (or our max delay, whichever is smaller)
7033                        let delay_ms = std::cmp::min(retry_after * 1000, self.config.max_delay_ms);
7034                        sleep(StdDuration::from_millis(delay_ms)).await;
7035                        continue;
7036                    }
7037
7038                    // Handle other client errors (4xx) - these are generally not retryable
7039                    if status.is_client_error() && status != reqwest::StatusCode::TOO_MANY_REQUESTS
7040                    {
7041                        last_error = format!("Client error: {status}");
7042                        tracing::error!("Non-retryable client error: {}", status);
7043                        break;
7044                    }
7045
7046                    // Handle server errors (5xx) - these are retryable
7047                    if status.is_server_error() {
7048                        last_error = format!("Server error: {status}");
7049                        tracing::warn!(
7050                            "Server error {} on attempt {}/{}",
7051                            status,
7052                            attempt,
7053                            self.config.max_attempts
7054                        );
7055
7056                        if attempt < self.config.max_attempts {
7057                            let delay = self.calculate_backoff_delay(attempt);
7058                            sleep(delay).await;
7059                            continue;
7060                        }
7061                        break;
7062                    }
7063
7064                    // Success case
7065                    return Ok(response);
7066                }
7067                Err(e) => {
7068                    last_error = e.to_string();
7069
7070                    // Check if this is a timeout error
7071                    if e.is_timeout() {
7072                        tracing::warn!(
7073                            "Request timeout on attempt {}/{}",
7074                            attempt,
7075                            self.config.max_attempts
7076                        );
7077
7078                        if attempt >= self.config.max_attempts {
7079                            return Err(TokenError::timeout(self.config.timeout_seconds));
7080                        }
7081                    } else {
7082                        tracing::warn!(
7083                            "Request failed on attempt {}/{}: {}",
7084                            attempt,
7085                            self.config.max_attempts,
7086                            e
7087                        );
7088                    }
7089
7090                    // If we have more attempts, wait and retry
7091                    if attempt < self.config.max_attempts {
7092                        let delay = self.calculate_backoff_delay(attempt);
7093                        sleep(delay).await;
7094                    }
7095                }
7096            }
7097        }
7098
7099        // All retries exhausted
7100        Err(TokenError::obtain_failed(attempt, last_error))
7101    }
7102
7103    /// Calculates the delay for exponential backoff with jitter
7104    ///
7105    /// Uses the formula: `min(base_delay * 2^(attempt-1) + jitter, max_delay)`
7106    /// where jitter is a random value between 0 and `base_delay/2`
7107    pub fn calculate_backoff_delay(&self, attempt: u32) -> StdDuration {
7108        use rand::Rng;
7109
7110        let base_delay = self.config.base_delay_ms;
7111        let max_delay = self.config.max_delay_ms;
7112
7113        // Calculate exponential backoff: base_delay * 2^(attempt-1)
7114        let exponential_delay = base_delay * 2_u64.pow(attempt.saturating_sub(1));
7115
7116        // Add jitter (random value between 0 and base_delay/2)
7117        let jitter = rand::thread_rng().gen_range(0..=base_delay / 2);
7118        let total_delay = exponential_delay + jitter;
7119
7120        // Cap at max_delay
7121        let final_delay = std::cmp::min(total_delay, max_delay);
7122
7123        tracing::debug!(
7124            "Calculated backoff delay for attempt {}: {}ms (exponential: {}ms, jitter: {}ms, capped at: {}ms)",
7125            attempt,
7126            final_delay,
7127            exponential_delay,
7128            jitter,
7129            max_delay
7130        );
7131
7132        StdDuration::from_millis(final_delay)
7133    }
7134
7135    /// Extracts the Retry-After header value from a 429 response
7136    ///
7137    /// Returns the number of seconds to wait, or None if the header is not present
7138    /// or cannot be parsed
7139    fn extract_retry_after(response: &reqwest::Response) -> Option<u64> {
7140        response
7141            .headers()
7142            .get("retry-after")
7143            .and_then(|value| value.to_str().ok())
7144            .and_then(|s| s.parse::<u64>().ok())
7145    }
7146
7147    /// Gets the underlying reqwest client
7148    #[must_use]
7149    pub const fn client(&self) -> &Client {
7150        &self.client
7151    }
7152
7153    /// Gets the retry configuration
7154    #[must_use]
7155    pub const fn config(&self) -> &RetryConfig {
7156        &self.config
7157    }
7158}
7159
7160/// Singleton instance of the `TokenManager` for shared token storage across all `ApiClient` instances
7161static GLOBAL_TOKEN_MANAGER: OnceCell<Arc<TokenManager>> = OnceCell::const_new();
7162
7163/// Core token manager with proactive refresh and secure storage
7164#[derive(Debug)]
7165pub struct TokenManager {
7166    pub token_data: Arc<Mutex<Option<TokenData>>>,
7167    pub retry_client: RetryClient,
7168    base_url: Url,
7169    /// Semaphore to ensure only one token operation (obtain/refresh) happens at a time
7170    /// This prevents race conditions where multiple threads try to refresh/obtain simultaneously
7171    token_operation_semaphore: Arc<Semaphore>,
7172}
7173
7174impl TokenManager {
7175    /// Gets the global singleton instance of `TokenManager`
7176    ///
7177    /// This ensures all `ApiClient` instances share the same token storage,
7178    /// preventing multiple token acquisition attempts in concurrent tests.
7179    ///
7180    /// # Errors
7181    /// Returns an error if the `TokenManager` cannot be initialized
7182    pub async fn get_global_instance() -> Result<Arc<Self>, Error> {
7183        let manager = GLOBAL_TOKEN_MANAGER
7184            .get_or_try_init(|| async {
7185                let config = RetryConfig::from_env()?;
7186                let base_url = get_amp_api_base_url()?;
7187                let manager = Self::with_config_and_base_url(config, base_url).await?;
7188                Ok::<Arc<Self>, Error>(Arc::new(manager))
7189            })
7190            .await?;
7191
7192        Ok(manager.clone())
7193    }
7194
7195    /// Creates a new `TokenManager` with default configuration
7196    ///
7197    /// # Errors
7198    /// Returns an error if the base URL cannot be obtained from environment variables
7199    pub async fn new() -> Result<Self, Error> {
7200        let config = RetryConfig::from_env()?;
7201        Self::with_config(config).await
7202    }
7203
7204    /// Creates a new `TokenManager` with the specified retry configuration
7205    ///
7206    /// # Errors
7207    /// Returns an error if the base URL cannot be obtained from environment variables
7208    pub async fn with_config(config: RetryConfig) -> Result<Self, Error> {
7209        let base_url = get_amp_api_base_url()?;
7210        Self::with_config_and_base_url(config, base_url).await
7211    }
7212
7213    /// Creates a new `TokenManager` with the specified configuration and base URL (for testing)
7214    ///
7215    /// # Errors
7216    /// This method is infallible but returns Result for API consistency
7217    pub async fn with_config_and_base_url(
7218        config: RetryConfig,
7219        base_url: Url,
7220    ) -> Result<Self, Error> {
7221        let manager = Self {
7222            token_data: Arc::new(Mutex::new(None)),
7223            retry_client: RetryClient::new(config),
7224            base_url,
7225            token_operation_semaphore: Arc::new(Semaphore::new(1)),
7226        };
7227
7228        // Load token from disk if persistence is enabled
7229        if Self::should_persist_tokens() {
7230            if let Ok(Some(token_data)) = manager.load_token_from_disk().await {
7231                *manager.token_data.lock().await = Some(token_data);
7232                tracing::info!("Token loaded from disk during initialization");
7233            }
7234        }
7235
7236        Ok(manager)
7237    }
7238
7239    /// Creates a new `TokenManager` with a pre-set mock token (for testing)
7240    ///
7241    /// # Errors
7242    /// This method is infallible but returns Result for API consistency
7243    pub fn with_mock_token(
7244        config: RetryConfig,
7245        base_url: Url,
7246        mock_token: String,
7247    ) -> Result<Self, Error> {
7248        let expires_at = Utc::now() + Duration::hours(24); // Mock token valid for 24 hours
7249        let token_data = TokenData::new(mock_token, expires_at);
7250
7251        let manager = Self {
7252            token_data: Arc::new(Mutex::new(Some(token_data))),
7253            retry_client: RetryClient::new(config),
7254            base_url,
7255            token_operation_semaphore: Arc::new(Semaphore::new(1)),
7256        };
7257
7258        Ok(manager)
7259    }
7260
7261    /// Gets a valid authentication token with proactive refresh logic
7262    ///
7263    /// This method implements thread-safe token management logic:
7264    /// 1. Check if a valid token exists and is not expiring soon (within 5 minutes)
7265    /// 2. If token needs refresh/obtain, acquire semaphore to prevent concurrent operations
7266    /// 3. Double-check token state after acquiring semaphore (another thread may have updated it)
7267    /// 4. Perform atomic token update operations
7268    /// 5. Return the valid token
7269    ///
7270    /// # Thread Safety
7271    /// This method is fully thread-safe and prevents race conditions by:
7272    /// - Using a semaphore to ensure only one token operation at a time
7273    /// - Double-checking token state after acquiring the semaphore
7274    /// - Performing atomic token updates within the critical section
7275    ///
7276    /// # Errors
7277    /// Returns a `TokenError` if token acquisition or refresh fails after all retries
7278    pub async fn get_token(&self) -> Result<String, Error> {
7279        // Fast path: check if we have a valid token without acquiring semaphore
7280        if let Some(token) = self.check_existing_token().await? {
7281            return Ok(token);
7282        }
7283
7284        // Slow path: token needs refresh/obtain, acquire semaphore for thread safety
7285        let _permit = self.acquire_token_semaphore().await?;
7286
7287        // Double-check token state after acquiring semaphore - another thread may have updated it
7288        if let Some(token) = self.check_existing_token().await? {
7289            tracing::debug!("Token was updated by another thread, using existing valid token");
7290            return Ok(token);
7291        }
7292
7293        // At this point, we need to refresh or obtain a new token
7294        self.handle_token_refresh_or_obtain().await
7295    }
7296
7297    /// Checks if we have a valid existing token that doesn't expire soon
7298    async fn check_existing_token(&self) -> Result<Option<String>, Error> {
7299        let token_guard = self.token_data.lock().await;
7300        if let Some(ref token_data) = *token_guard {
7301            if !token_data.expires_soon(Duration::minutes(5)) {
7302                tracing::debug!("Using existing valid token");
7303                let token = token_data.token.expose_secret().clone();
7304                drop(token_guard);
7305                return Ok(Some(token));
7306            }
7307        }
7308        drop(token_guard);
7309        Ok(None)
7310    }
7311
7312    /// Acquires the token operation semaphore for thread-safe operations
7313    async fn acquire_token_semaphore(&self) -> Result<tokio::sync::SemaphorePermit<'_>, Error> {
7314        let permit = self
7315            .token_operation_semaphore
7316            .acquire()
7317            .await
7318            .map_err(|e| {
7319                Error::Token(TokenError::storage(format!(
7320                    "Failed to acquire token operation semaphore: {e}"
7321                )))
7322            })?;
7323
7324        tracing::debug!("Acquired token operation semaphore for thread-safe token management");
7325        Ok(permit)
7326    }
7327
7328    /// Handles the token refresh or obtain logic
7329    async fn handle_token_refresh_or_obtain(&self) -> Result<String, Error> {
7330        let needs_refresh = self.determine_token_operation().await;
7331
7332        if needs_refresh {
7333            match self.refresh_token_internal().await {
7334                Ok(token) => {
7335                    tracing::info!("Token refreshed successfully");
7336                    return Ok(token);
7337                }
7338                Err(e) => {
7339                    tracing::warn!("Token refresh failed, falling back to obtain: {e}");
7340                    // Fall through to obtain new token
7341                }
7342            }
7343        }
7344
7345        // Either we needed to obtain from the start, or refresh failed
7346        self.obtain_token_internal().await
7347    }
7348
7349    /// Determines whether we need to refresh or obtain a new token
7350    async fn determine_token_operation(&self) -> bool {
7351        let token_guard = self.token_data.lock().await;
7352        token_guard.as_ref().map_or_else(
7353            || {
7354                tracing::info!("No token exists, will obtain new token");
7355                false
7356            },
7357            |token_data| {
7358                if token_data.is_expired() {
7359                    tracing::info!("Token is expired, will obtain new token");
7360                    false
7361                } else {
7362                    tracing::info!("Token expires soon, will attempt refresh");
7363                    true
7364                }
7365            },
7366        )
7367    }
7368
7369    /// Obtains a new authentication token using environment credentials with retry logic
7370    ///
7371    /// This method:
7372    /// 1. Reads credentials from environment variables
7373    /// 2. Makes a token request with retry logic
7374    /// 3. Stores the new token with 24-hour expiry
7375    /// 4. Returns the token string
7376    ///
7377    /// # Thread Safety
7378    /// This method acquires the token operation semaphore to ensure thread-safe operation.
7379    /// For internal use within already-synchronized contexts, use `obtain_token_internal()`.
7380    ///
7381    /// # Errors
7382    /// Returns an error if:
7383    /// - Environment variables are missing
7384    /// - All retry attempts fail
7385    /// - Response parsing fails
7386    pub async fn obtain_token(&self) -> Result<String, Error> {
7387        let _permit = self
7388            .token_operation_semaphore
7389            .acquire()
7390            .await
7391            .map_err(|e| {
7392                Error::Token(TokenError::storage(format!(
7393                    "Failed to acquire token operation semaphore: {e}"
7394                )))
7395            })?;
7396
7397        self.obtain_token_internal().await
7398    }
7399
7400    /// Internal method to obtain a new authentication token without acquiring semaphore
7401    ///
7402    /// This method should only be called from contexts where the token operation semaphore
7403    /// has already been acquired (e.g., from within `get_token()`).
7404    ///
7405    /// # Errors
7406    /// Returns an error if:
7407    /// - Environment variables are missing
7408    /// - All retry attempts fail
7409    /// - Response parsing fails
7410    async fn obtain_token_internal(&self) -> Result<String, Error> {
7411        tracing::debug!("Obtaining new authentication token");
7412
7413        let request_payload = Self::get_credentials_from_env()?;
7414        let url = self.build_obtain_token_url();
7415        let response = self.execute_token_request(&url, &request_payload).await?;
7416        let token_response = self.parse_token_response(response).await?;
7417
7418        self.store_token_data(&token_response.token).await;
7419
7420        tracing::info!("New authentication token obtained successfully");
7421        Ok(token_response.token)
7422    }
7423
7424    /// Gets credentials from environment variables
7425    fn get_credentials_from_env() -> Result<TokenRequest, Error> {
7426        let username = env::var("AMP_USERNAME")
7427            .map_err(|_| Error::MissingEnvVar("AMP_USERNAME".to_string()))?;
7428        let password = env::var("AMP_PASSWORD")
7429            .map_err(|_| Error::MissingEnvVar("AMP_PASSWORD".to_string()))?;
7430
7431        Ok(TokenRequest { username, password })
7432    }
7433
7434    /// Builds the URL for token obtain endpoint
7435    fn build_obtain_token_url(&self) -> Url {
7436        let mut url = self.base_url.clone();
7437        url.path_segments_mut()
7438            .unwrap()
7439            .push("user")
7440            .push("obtain_token");
7441        url
7442    }
7443
7444    /// Executes the token request with retry logic
7445    async fn execute_token_request(
7446        &self,
7447        url: &Url,
7448        request_payload: &TokenRequest,
7449    ) -> Result<reqwest::Response, Error> {
7450        let response = self
7451            .retry_client
7452            .execute_with_retry(|| {
7453                self.retry_client
7454                    .client()
7455                    .post(url.clone())
7456                    .json(request_payload)
7457            })
7458            .await
7459            .map_err(Error::Token)?;
7460
7461        if !response.status().is_success() {
7462            let status = response.status();
7463            let error_text = response
7464                .text()
7465                .await
7466                .unwrap_or_else(|_| "Unknown error".to_string());
7467            return Err(Error::TokenRequestFailed { status, error_text });
7468        }
7469
7470        Ok(response)
7471    }
7472
7473    /// Parses the token response from the API
7474    async fn parse_token_response(
7475        &self,
7476        response: reqwest::Response,
7477    ) -> Result<TokenResponse, Error> {
7478        response
7479            .json()
7480            .await
7481            .map_err(|e| Error::ResponseParsingFailed(e.to_string()))
7482    }
7483
7484    /// Stores the token data with 24-hour expiry and optional disk persistence
7485    async fn store_token_data(&self, token: &str) {
7486        let expires_at = Utc::now() + Duration::days(1);
7487        let token_data = TokenData::new(token.to_string(), expires_at);
7488
7489        // Atomic token update - hold the lock for the minimal time needed
7490        *self.token_data.lock().await = Some(token_data.clone());
7491        tracing::debug!("Token data updated atomically in storage");
7492
7493        // Save to disk if persistence is enabled
7494        if Self::should_persist_tokens() {
7495            if let Err(e) = self.save_token_to_disk(&token_data).await {
7496                tracing::warn!("Failed to save token to disk: {e}");
7497            }
7498        }
7499    }
7500
7501    /// Refreshes the current authentication token with fallback to obtain on failure
7502    ///
7503    /// This method:
7504    /// 1. Uses the existing token to request a refresh
7505    /// 2. Updates the stored token data on success
7506    /// 3. Falls back to obtaining a new token if refresh fails
7507    ///
7508    /// # Thread Safety
7509    /// This method acquires the token operation semaphore to ensure thread-safe operation.
7510    /// For internal use within already-synchronized contexts, use `refresh_token_internal()`.
7511    ///
7512    /// # Errors
7513    /// Returns an error if both refresh and obtain operations fail
7514    pub async fn refresh_token(&self) -> Result<String, Error> {
7515        let _permit = self
7516            .token_operation_semaphore
7517            .acquire()
7518            .await
7519            .map_err(|e| {
7520                Error::Token(TokenError::storage(format!(
7521                    "Failed to acquire token operation semaphore: {e}"
7522                )))
7523            })?;
7524
7525        self.refresh_token_internal().await
7526    }
7527
7528    /// Internal method to refresh the current authentication token without acquiring semaphore
7529    ///
7530    /// This method should only be called from contexts where the token operation semaphore
7531    /// has already been acquired (e.g., from within `get_token()`).
7532    ///
7533    /// # Errors
7534    /// Returns an error if both refresh and obtain operations fail
7535    #[allow(clippy::cognitive_complexity)]
7536    async fn refresh_token_internal(&self) -> Result<String, Error> {
7537        tracing::debug!("Refreshing authentication token");
7538
7539        let Some(current_token) = self.get_current_token_for_refresh().await else {
7540            tracing::warn!("No token available for refresh, obtaining new token");
7541            return self.obtain_token_internal().await;
7542        };
7543
7544        let url = self.build_refresh_token_url();
7545        let response = self.execute_refresh_request(&url, &current_token).await;
7546
7547        match response {
7548            Ok(resp) => self.handle_refresh_response(resp).await,
7549            Err(e) => {
7550                tracing::warn!("Token refresh request failed: {e}, falling back to obtain");
7551                self.obtain_token_internal().await
7552            }
7553        }
7554    }
7555
7556    /// Gets the current token for refresh operations
7557    async fn get_current_token_for_refresh(&self) -> Option<String> {
7558        let token_guard = self.token_data.lock().await;
7559        token_guard
7560            .as_ref()
7561            .map(|token_data| token_data.token.expose_secret().clone())
7562    }
7563
7564    /// Builds the URL for token refresh endpoint
7565    fn build_refresh_token_url(&self) -> Url {
7566        let mut url = self.base_url.clone();
7567        url.path_segments_mut()
7568            .unwrap()
7569            .push("user")
7570            .push("refresh_token");
7571        url
7572    }
7573
7574    /// Executes the refresh request with retry logic
7575    async fn execute_refresh_request(
7576        &self,
7577        url: &Url,
7578        current_token: &str,
7579    ) -> Result<reqwest::Response, TokenError> {
7580        self.retry_client
7581            .execute_with_retry(|| {
7582                self.retry_client
7583                    .client()
7584                    .post(url.clone())
7585                    .header(AUTHORIZATION, format!("token {current_token}"))
7586            })
7587            .await
7588    }
7589
7590    /// Handles the refresh response, either storing the new token or falling back to obtain
7591    async fn handle_refresh_response(&self, resp: reqwest::Response) -> Result<String, Error> {
7592        if !resp.status().is_success() {
7593            let status = resp.status();
7594            let error_text = resp
7595                .text()
7596                .await
7597                .unwrap_or_else(|_| "Unknown error".to_string());
7598
7599            tracing::warn!("Token refresh failed with status {status}: {error_text}");
7600            return self.obtain_token_internal().await;
7601        }
7602
7603        let token_response: TokenResponse = resp
7604            .json()
7605            .await
7606            .map_err(|e| Error::ResponseParsingFailed(e.to_string()))?;
7607
7608        self.store_token_data(&token_response.token).await;
7609        tracing::info!("Authentication token refreshed successfully");
7610        Ok(token_response.token)
7611    }
7612
7613    /// Gets current token information for debugging and monitoring
7614    ///
7615    /// Returns detailed information about the current token including:
7616    /// - Expiry time and remaining duration
7617    /// - Token age since acquisition
7618    /// - Expiry status flags
7619    ///
7620    /// # Returns
7621    /// `Some(TokenInfo)` if a token exists, `None` if no token is stored
7622    ///
7623    /// # Errors
7624    /// Returns an error if token information retrieval fails
7625    pub async fn get_token_info(&self) -> Result<Option<TokenInfo>, Error> {
7626        tracing::debug!("Retrieving token information for debugging");
7627
7628        let token_info = self.token_data.lock().await.as_ref().map(TokenInfo::from);
7629
7630        match &token_info {
7631            Some(info) => {
7632                tracing::debug!(
7633                    "Token info retrieved - expires_at: {}, age: {:?}, expires_in: {:?}, is_expired: {}, expires_soon: {}",
7634                    info.expires_at,
7635                    info.age,
7636                    info.expires_in,
7637                    info.is_expired,
7638                    info.expires_soon
7639                );
7640            }
7641            None => {
7642                tracing::debug!("No token information available - no token stored");
7643            }
7644        }
7645
7646        Ok(token_info)
7647    }
7648
7649    /// Clears the stored token (useful for testing scenarios)
7650    ///
7651    /// This method removes the current token from storage, forcing the next
7652    /// `get_token()` call to obtain a fresh token.
7653    ///
7654    /// # Errors
7655    /// Returns an error if token clearing fails
7656    pub async fn clear_token(&self) -> Result<(), Error> {
7657        tracing::debug!("Clearing stored token from memory and disk");
7658
7659        let had_token = self.clear_token_from_memory().await;
7660        self.clear_token_from_disk_if_enabled().await;
7661        Self::log_token_clear_result(had_token);
7662
7663        Ok(())
7664    }
7665
7666    /// Clears the token from memory and returns whether a token was present
7667    async fn clear_token_from_memory(&self) -> bool {
7668        let mut token_guard = self.token_data.lock().await;
7669        let had_token = token_guard.is_some();
7670        *token_guard = None;
7671        drop(token_guard);
7672        had_token
7673    }
7674
7675    /// Clears the token from disk if persistence is enabled
7676    async fn clear_token_from_disk_if_enabled(&self) {
7677        if Self::should_persist_tokens() {
7678            if let Err(e) = self.remove_token_from_disk().await {
7679                tracing::warn!("Failed to remove token from disk: {e}");
7680            }
7681        }
7682    }
7683
7684    /// Logs the result of the token clearing operation
7685    fn log_token_clear_result(had_token: bool) {
7686        if had_token {
7687            tracing::info!("Token successfully cleared from memory and disk - next get_token() will obtain fresh token");
7688        } else {
7689            tracing::debug!("No token was stored to clear");
7690        }
7691    }
7692
7693    /// Forces a token refresh regardless of current token status
7694    ///
7695    /// This method bypasses the normal proactive refresh logic and immediately
7696    /// attempts to refresh the current token. If no token exists or refresh fails,
7697    /// it falls back to obtaining a new token.
7698    ///
7699    /// # Thread Safety
7700    /// This method is fully thread-safe and uses the same semaphore-based synchronization
7701    /// as other token operations to prevent race conditions.
7702    ///
7703    /// # Errors
7704    /// Returns an error if both refresh and obtain operations fail
7705    pub async fn force_refresh(&self) -> Result<String, Error> {
7706        tracing::info!("Forcing token refresh - bypassing normal proactive refresh logic");
7707
7708        let _permit = self.acquire_token_semaphore().await?;
7709        self.log_token_status_for_refresh().await;
7710        self.execute_forced_refresh().await
7711    }
7712
7713    /// Logs the current token status for forced refresh operation
7714    async fn log_token_status_for_refresh(&self) {
7715        let has_token = {
7716            let token_guard = self.token_data.lock().await;
7717            token_guard.is_some()
7718        };
7719
7720        if has_token {
7721            tracing::debug!("Existing token found, attempting forced refresh");
7722        } else {
7723            tracing::debug!("No existing token found, will obtain new token");
7724        }
7725    }
7726
7727    /// Executes the forced refresh operation
7728    async fn execute_forced_refresh(&self) -> Result<String, Error> {
7729        match self.refresh_token_internal().await {
7730            Ok(token) => {
7731                tracing::info!("Forced token refresh completed successfully");
7732                Ok(token)
7733            }
7734            Err(e) => {
7735                tracing::error!("Forced token refresh failed: {e}");
7736                Err(e)
7737            }
7738        }
7739    }
7740
7741    /// Determines if token persistence is enabled based on environment variables
7742    ///
7743    /// Token persistence is enabled when:
7744    /// - `AMP_TESTS=live` (for live API testing)
7745    /// - `AMP_TOKEN_PERSISTENCE=true` is set
7746    /// - NOT in mock test environments (to prevent test pollution)
7747    fn should_persist_tokens() -> bool {
7748        // Use the new environment detection logic
7749        let environment = TokenEnvironment::detect();
7750
7751        // Never persist tokens in mock environments to prevent test pollution
7752        if environment.is_mock() {
7753            tracing::debug!("Token persistence disabled - mock environment detected");
7754            return false;
7755        }
7756
7757        // Check if explicitly enabled
7758        if env::var("AMP_TOKEN_PERSISTENCE").unwrap_or_default() == "true" {
7759            tracing::debug!("Token persistence enabled - AMP_TOKEN_PERSISTENCE=true");
7760            return true;
7761        }
7762
7763        // Use environment-based persistence setting
7764        let should_persist = environment.should_persist_tokens();
7765        tracing::debug!(
7766            "Token persistence setting from environment: {}",
7767            should_persist
7768        );
7769        should_persist
7770    }
7771
7772    /// Loads token data from disk if it exists and is valid
7773    async fn load_token_from_disk(&self) -> Result<Option<TokenData>, Error> {
7774        let token_file = "token.json";
7775
7776        if !self.token_file_exists(token_file).await {
7777            return Ok(None);
7778        }
7779
7780        let content = self.read_token_file(token_file).await?;
7781        self.parse_and_validate_token(token_file, &content).await
7782    }
7783
7784    /// Checks if the token file exists on disk
7785    async fn token_file_exists(&self, token_file: &str) -> bool {
7786        tokio::fs::try_exists(token_file).await.map_or_else(
7787            |_| {
7788                tracing::debug!("Error checking token file existence: {}", token_file);
7789                false
7790            },
7791            |exists| {
7792                if !exists {
7793                    tracing::debug!("Token file does not exist: {}", token_file);
7794                }
7795                exists
7796            },
7797        )
7798    }
7799
7800    /// Reads the token file content from disk
7801    async fn read_token_file(&self, token_file: &str) -> Result<String, Error> {
7802        use tokio::fs;
7803
7804        match fs::read_to_string(token_file).await {
7805            Ok(content) => Ok(content),
7806            Err(e) => {
7807                tracing::warn!("Failed to read token file: {e}");
7808                Err(Error::Token(TokenError::storage(format!(
7809                    "Failed to read token file: {e}"
7810                ))))
7811            }
7812        }
7813    }
7814
7815    /// Parses token content and validates expiration
7816    async fn parse_and_validate_token(
7817        &self,
7818        token_file: &str,
7819        content: &str,
7820    ) -> Result<Option<TokenData>, Error> {
7821        match serde_json::from_str::<TokenData>(content) {
7822            Ok(token_data) => self.handle_parsed_token(token_file, token_data).await,
7823            Err(e) => self.handle_parse_error(token_file, e).await,
7824        }
7825    }
7826
7827    /// Handles successfully parsed token data, checking expiration
7828    async fn handle_parsed_token(
7829        &self,
7830        token_file: &str,
7831        token_data: TokenData,
7832    ) -> Result<Option<TokenData>, Error> {
7833        if token_data.is_expired() {
7834            tracing::info!("Token loaded from disk is expired, removing file");
7835            let _ = tokio::fs::remove_file(token_file).await;
7836            Ok(None)
7837        } else {
7838            tracing::info!("Valid token loaded from disk");
7839            Ok(Some(token_data))
7840        }
7841    }
7842
7843    /// Handles token parsing errors by cleaning up the invalid file
7844    async fn handle_parse_error(
7845        &self,
7846        token_file: &str,
7847        e: serde_json::Error,
7848    ) -> Result<Option<TokenData>, Error> {
7849        tracing::warn!("Failed to parse token file, removing: {e}");
7850        let _ = tokio::fs::remove_file(token_file).await;
7851        Err(Error::Token(TokenError::serialization(format!(
7852            "Failed to parse token file: {e}"
7853        ))))
7854    }
7855
7856    /// Saves token data to disk
7857    async fn save_token_to_disk(&self, token_data: &TokenData) -> Result<(), Error> {
7858        use tokio::fs;
7859
7860        let token_file = "token.json";
7861
7862        match serde_json::to_string_pretty(token_data) {
7863            Ok(json) => match fs::write(token_file, json).await {
7864                Ok(()) => {
7865                    tracing::debug!("Token saved to disk: {}", token_file);
7866                    Ok(())
7867                }
7868                Err(e) => {
7869                    tracing::error!("Failed to write token file: {e}");
7870                    Err(Error::Token(TokenError::storage(format!(
7871                        "Failed to write token file: {e}"
7872                    ))))
7873                }
7874            },
7875            Err(e) => {
7876                tracing::error!("Failed to serialize token data: {e}");
7877                Err(Error::Token(TokenError::serialization(format!(
7878                    "Failed to serialize token data: {e}"
7879                ))))
7880            }
7881        }
7882    }
7883
7884    /// Removes the token file from disk
7885    async fn remove_token_from_disk(&self) -> Result<(), Error> {
7886        use tokio::fs;
7887
7888        let token_file = "token.json";
7889
7890        match fs::remove_file(token_file).await {
7891            Ok(()) => {
7892                tracing::debug!("Token file removed from disk: {}", token_file);
7893                Ok(())
7894            }
7895            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
7896                tracing::debug!("Token file does not exist, nothing to remove");
7897                Ok(())
7898            }
7899            Err(e) => {
7900                tracing::warn!("Failed to remove token file: {e}");
7901                Err(Error::Token(TokenError::storage(format!(
7902                    "Failed to remove token file: {e}"
7903                ))))
7904            }
7905        }
7906    }
7907
7908    /// Forces cleanup of token persistence files (useful for testing)
7909    /// This method removes token files regardless of persistence settings
7910    ///
7911    /// # Errors
7912    /// Returns an error if:
7913    /// - File system permissions prevent deletion of the token file
7914    /// - I/O errors occur during file deletion operations
7915    /// - The token file is locked by another process
7916    pub async fn force_cleanup_token_files() -> Result<(), Error> {
7917        use tokio::fs;
7918
7919        let token_file = "token.json";
7920
7921        match fs::remove_file(token_file).await {
7922            Ok(()) => {
7923                tracing::debug!("Token file forcefully removed: {}", token_file);
7924                Ok(())
7925            }
7926            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
7927                tracing::debug!("No token file to clean up");
7928                Ok(())
7929            }
7930            Err(e) => {
7931                tracing::warn!("Failed to force cleanup token file: {e}");
7932                Err(Error::Token(TokenError::storage(format!(
7933                    "Failed to force cleanup token file: {e}"
7934                ))))
7935            }
7936        }
7937    }
7938
7939    /// Resets the global `TokenManager` singleton (useful for testing)
7940    ///
7941    /// This method clears the global singleton instance, forcing the next
7942    /// call to `get_global_instance()` to create a fresh `TokenManager`.
7943    /// Primarily intended for test scenarios where a clean state is needed.
7944    ///
7945    /// # Errors
7946    /// Returns an error if:
7947    /// - Token clearing operations fail during the reset process
7948    /// - File system errors occur when clearing persistent token data
7949    /// - The global instance is in an invalid state that prevents cleanup
7950    pub async fn reset_global_instance() -> Result<(), Error> {
7951        // Clear any existing token from the current global instance
7952        if let Some(manager) = GLOBAL_TOKEN_MANAGER.get() {
7953            let _ = manager.clear_token().await;
7954        }
7955
7956        // Reset the OnceCell to allow a new instance to be created
7957        // Note: OnceCell doesn't have a reset method, so we can't actually reset it
7958        // The best we can do is clear the token from the existing instance
7959        tracing::debug!("Global TokenManager instance token cleared for testing");
7960        Ok(())
7961    }
7962}
7963
7964#[derive(Debug, Clone)]
7965pub struct ApiClient {
7966    client: Client,
7967    base_url: Url,
7968    token_strategy: Arc<Box<dyn TokenStrategy>>,
7969}
7970
7971#[allow(clippy::missing_errors_doc, clippy::missing_panics_doc)]
7972impl ApiClient {
7973    /// Creates a new API client with the base URL from environment variables.
7974    ///
7975    /// Automatically selects the appropriate token strategy based on environment detection:
7976    /// - Mock strategy for mock environments (no persistence, isolated tokens)
7977    /// - Live strategy for live environments (full token management with persistence)
7978    ///
7979    /// # Errors
7980    ///
7981    /// Returns an error if:
7982    /// - The `AMP_API_BASE_URL` environment variable contains an invalid URL
7983    /// - Token strategy initialization fails
7984    ///
7985    /// # Examples
7986    /// ```no_run
7987    /// # use amp_rs::ApiClient;
7988    /// # #[tokio::main]
7989    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
7990    /// // Create a new client - automatically detects environment
7991    /// let client = ApiClient::new().await?;
7992    ///
7993    /// // Client is ready to use
7994    /// let assets = client.get_assets().await?;
7995    /// println!("Found {} assets", assets.len());
7996    /// # Ok(())
7997    /// # }
7998    /// ```
7999    pub async fn new() -> Result<Self, Error> {
8000        let base_url = get_amp_api_base_url()?;
8001        let client = Client::new();
8002
8003        // Automatic strategy selection based on environment
8004        let token_strategy = TokenEnvironment::create_auto_strategy(None).await?;
8005
8006        tracing::info!(
8007            "Created ApiClient with {} strategy for base URL: {}",
8008            token_strategy.strategy_type(),
8009            base_url
8010        );
8011
8012        Ok(Self {
8013            client,
8014            base_url,
8015            token_strategy: Arc::new(token_strategy),
8016        })
8017    }
8018
8019    /// Creates a new API client with the specified base URL.
8020    ///
8021    /// Automatically selects the appropriate token strategy based on environment detection.
8022    ///
8023    /// # Errors
8024    ///
8025    /// Returns an error if token strategy initialization fails.
8026    ///
8027    /// # Examples
8028    /// ```no_run
8029    /// # use amp_rs::ApiClient;
8030    /// # use reqwest::Url;
8031    /// # #[tokio::main]
8032    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
8033    /// let base_url = Url::parse("https://amp-test.blockstream.com/api")?;
8034    /// let client = ApiClient::with_base_url(base_url).await?;
8035    ///
8036    /// // Client is ready to use with the specified URL
8037    /// let assets = client.get_assets().await?;
8038    /// # Ok(())
8039    /// # }
8040    /// ```
8041    pub async fn with_base_url(base_url: Url) -> Result<Self, Error> {
8042        let client = Client::new();
8043
8044        // Automatic strategy selection based on environment
8045        let token_strategy = TokenEnvironment::create_auto_strategy(None).await?;
8046
8047        tracing::info!(
8048            "Created ApiClient with {} strategy for base URL: {}",
8049            token_strategy.strategy_type(),
8050            base_url
8051        );
8052
8053        Ok(Self {
8054            client,
8055            base_url,
8056            token_strategy: Arc::new(token_strategy),
8057        })
8058    }
8059
8060    /// Creates a new API client with a custom token strategy (useful for testing).
8061    ///
8062    /// # Errors
8063    ///
8064    /// Returns an error if the base URL cannot be obtained from environment variables.
8065    pub fn with_token_strategy(token_strategy: Box<dyn TokenStrategy>) -> Result<Self, Error> {
8066        let base_url = get_amp_api_base_url()?;
8067
8068        tracing::info!(
8069            "Created ApiClient with explicit {} strategy for base URL: {}",
8070            token_strategy.strategy_type(),
8071            base_url
8072        );
8073
8074        Ok(Self {
8075            client: Client::new(),
8076            base_url,
8077            token_strategy: Arc::new(token_strategy),
8078        })
8079    }
8080
8081    /// Creates a new API client with a custom token manager (useful for testing).
8082    ///
8083    /// # Errors
8084    ///
8085    /// Returns an error if the base URL cannot be obtained from environment variables.
8086    pub fn with_token_manager(token_manager: Arc<TokenManager>) -> Result<Self, Error> {
8087        let base_url = get_amp_api_base_url()?;
8088        let token_strategy: Box<dyn TokenStrategy> =
8089            Box::new(LiveTokenStrategy::with_token_manager(token_manager));
8090
8091        tracing::info!(
8092            "Created ApiClient with custom token manager for base URL: {}",
8093            base_url
8094        );
8095
8096        Ok(Self {
8097            client: Client::new(),
8098            base_url,
8099            token_strategy: Arc::new(token_strategy),
8100        })
8101    }
8102
8103    /// Creates a new API client for testing with a mock token strategy that always returns a fixed token.
8104    /// This bypasses all token acquisition and management logic and uses complete isolation.
8105    ///
8106    /// # Errors
8107    ///
8108    /// This method is infallible but returns Result for API consistency.
8109    ///
8110    /// # Examples
8111    /// ```no_run
8112    /// # use amp_rs::ApiClient;
8113    /// # use reqwest::Url;
8114    /// # #[tokio::main]
8115    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
8116    /// let base_url = Url::parse("http://localhost:8080/api")?;
8117    /// let client = ApiClient::with_mock_token(base_url, "test_token".to_string())?;
8118    ///
8119    /// // Client will always use "test_token" for authentication
8120    /// let token = client.get_token().await?;
8121    /// assert_eq!(token, "test_token");
8122    /// # Ok(())
8123    /// # }
8124    /// ```
8125    pub fn with_mock_token(base_url: Url, mock_token: String) -> Result<Self, Error> {
8126        let client = Client::new();
8127        let token_strategy: Box<dyn TokenStrategy> = Box::new(MockTokenStrategy::new(mock_token));
8128
8129        tracing::info!(
8130            "Created ApiClient with explicit mock token strategy for base URL: {}",
8131            base_url
8132        );
8133
8134        Ok(Self {
8135            client,
8136            base_url,
8137            token_strategy: Arc::new(token_strategy),
8138        })
8139    }
8140
8141    /// Obtains a new authentication token from the AMP API.
8142    ///
8143    /// **Note**: This method is deprecated in favor of the automatic token management
8144    /// provided by `get_token()`. The `TokenManager` handles token acquisition internally
8145    /// with enhanced retry logic and error handling.
8146    ///
8147    /// # Errors
8148    ///
8149    /// Returns an error if:
8150    /// - The `AMP_USERNAME` or `AMP_PASSWORD` environment variables are not set
8151    /// - The HTTP request fails
8152    /// - The token request is rejected by the server
8153    /// - The response cannot be parsed
8154    #[deprecated(note = "Use get_token() instead - it provides automatic token management")]
8155    pub async fn obtain_amp_token(&self) -> Result<String, Error> {
8156        // Delegate to get_token for backward compatibility
8157        self.get_token().await
8158    }
8159
8160    /// Gets current token information for debugging and monitoring.
8161    ///
8162    /// Returns detailed information about the current token including:
8163    /// - Expiry time and remaining duration
8164    /// - Token age since acquisition
8165    /// - Expiry status flags
8166    ///
8167    /// Note: Mock strategies may return limited or no token information.
8168    ///
8169    /// # Returns
8170    /// `Some(TokenInfo)` if a token exists, `None` if no token is stored or strategy doesn't support info
8171    ///
8172    /// # Errors
8173    /// Returns an error if token information retrieval fails
8174    ///
8175    /// # Examples
8176    /// ```no_run
8177    /// # use amp_rs::ApiClient;
8178    /// # #[tokio::main]
8179    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
8180    /// let client = ApiClient::new().await?;
8181    ///
8182    /// if let Some(token_info) = client.get_token_info().await? {
8183    ///     println!("Token expires at: {}", token_info.expires_at);
8184    ///     println!("Token is expired: {}", token_info.is_expired);
8185    /// } else {
8186    ///     println!("No token stored or mock strategy in use");
8187    /// }
8188    /// # Ok(())
8189    /// # }
8190    /// ```
8191    pub async fn get_token_info(&self) -> Result<Option<TokenInfo>, Error> {
8192        // Only live strategies support detailed token information
8193        if let Some(live_strategy) = self
8194            .token_strategy
8195            .as_any()
8196            .downcast_ref::<LiveTokenStrategy>()
8197        {
8198            live_strategy.get_token_info().await
8199        } else {
8200            // Mock strategies don't provide detailed token information
8201            tracing::debug!(
8202                "Token info not available for {} strategy",
8203                self.token_strategy.strategy_type()
8204            );
8205            Ok(None)
8206        }
8207    }
8208
8209    /// Clears the stored token (useful for testing scenarios).
8210    ///
8211    /// This method removes the current token from storage, forcing the next
8212    /// `get_token()` call to obtain a fresh token.
8213    ///
8214    /// # Errors
8215    /// Returns an error if token clearing fails
8216    ///
8217    /// # Examples
8218    /// ```no_run
8219    /// # use amp_rs::ApiClient;
8220    /// # #[tokio::main]
8221    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
8222    /// let client = ApiClient::new().await?;
8223    ///
8224    /// // Clear any existing token
8225    /// client.clear_token().await?;
8226    ///
8227    /// // Next get_token() call will obtain a fresh token
8228    /// let token = client.get_token().await?;
8229    /// # Ok(())
8230    /// # }
8231    /// ```
8232    pub async fn clear_token(&self) -> Result<(), Error> {
8233        self.token_strategy.clear_token().await
8234    }
8235
8236    /// Forces a token refresh regardless of current token status.
8237    ///
8238    /// This method bypasses the normal proactive refresh logic and immediately
8239    /// attempts to refresh the current token. If no token exists or refresh fails,
8240    /// it falls back to obtaining a new token.
8241    ///
8242    /// # Errors
8243    /// Returns an error if both refresh and obtain operations fail
8244    pub async fn force_refresh(&self) -> Result<String, Error> {
8245        // Clear current token and get a fresh one
8246        self.token_strategy.clear_token().await?;
8247        self.token_strategy.get_token().await
8248    }
8249
8250    /// Resets the global `TokenManager` singleton (useful for testing).
8251    ///
8252    /// This method clears the token from the global `TokenManager` instance.
8253    /// Primarily intended for test scenarios where a clean token state is needed.
8254    ///
8255    /// # Errors
8256    /// Returns an error if the reset operation fails
8257    pub async fn reset_global_token_manager() -> Result<(), Error> {
8258        TokenManager::reset_global_instance().await
8259    }
8260
8261    /// Gets a valid authentication token with automatic token management.
8262    ///
8263    /// This method uses the integrated `TokenManager` to handle:
8264    /// - Proactive token refresh (5 minutes before expiry)
8265    /// - Automatic fallback from refresh to obtain on failure
8266    /// - Retry logic with exponential backoff
8267    /// - Thread-safe token storage
8268    ///
8269    /// # Errors
8270    ///
8271    /// Returns an error if token acquisition or refresh fails after all retries.
8272    ///
8273    /// # Examples
8274    /// ```no_run
8275    /// # use amp_rs::ApiClient;
8276    /// # #[tokio::main]
8277    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
8278    /// let client = ApiClient::new().await?;
8279    ///
8280    /// // Get a valid token - automatically handles refresh if needed
8281    /// let token = client.get_token().await?;
8282    /// println!("Got token: {}", &token[..10]); // Print first 10 chars
8283    /// # Ok(())
8284    /// # }
8285    /// ```
8286    pub async fn get_token(&self) -> Result<String, Error> {
8287        self.token_strategy.get_token().await
8288    }
8289
8290    /// Returns the type of token strategy currently in use
8291    ///
8292    /// This is useful for debugging and testing to verify the correct strategy is selected.
8293    ///
8294    /// # Returns
8295    /// A string indicating the strategy type: "mock" or "live"
8296    #[must_use]
8297    pub fn get_strategy_type(&self) -> &'static str {
8298        self.token_strategy.strategy_type()
8299    }
8300
8301    /// Returns whether the current strategy persists tokens
8302    ///
8303    /// This is useful for understanding the token management behavior.
8304    ///
8305    /// # Returns
8306    /// `true` if tokens are persisted to disk, `false` for in-memory only
8307    #[must_use]
8308    pub fn should_persist_tokens(&self) -> bool {
8309        self.token_strategy.should_persist()
8310    }
8311
8312    /// Force cleanup of token files (for test cleanup)
8313    ///
8314    /// This is a static method that can be used to cleanup token files
8315    /// without needing an `ApiClient` instance. Useful for test teardown.
8316    ///
8317    /// # Errors
8318    /// Returns an error if token file cleanup fails
8319    pub async fn force_cleanup_token_files() -> Result<(), Error> {
8320        // Only cleanup if we're not in a live test environment
8321        let environment = TokenEnvironment::detect();
8322        if !environment.is_live() || environment.is_mock() {
8323            TokenManager::force_cleanup_token_files().await?;
8324            tracing::debug!("Token files cleaned up for non-live environment");
8325        } else {
8326            tracing::debug!("Skipping token file cleanup in live environment");
8327        }
8328        Ok(())
8329    }
8330
8331    async fn request_raw(
8332        &self,
8333        method: Method,
8334        path: &[&str],
8335        body: Option<impl serde::Serialize>,
8336    ) -> Result<reqwest::Response, Error> {
8337        let debug_logging = std::env::var("AMP_DEBUG").is_ok();
8338
8339        if debug_logging {
8340            eprintln!("🌐 HTTP Request: {} /{}", method, path.join("/"));
8341        }
8342
8343        let token = self.get_token().await?;
8344        let mut url = self.base_url.clone();
8345        url.path_segments_mut().unwrap().extend(path);
8346
8347        if debug_logging {
8348            eprintln!("🔗 Full URL: {url}");
8349        }
8350
8351        // Retry logic for network issues
8352        let max_retries = 3;
8353        let mut last_error = None;
8354
8355        for attempt in 1..=max_retries {
8356            if debug_logging && attempt > 1 {
8357                eprintln!("🔄 Retry attempt {attempt} of {max_retries}");
8358            }
8359
8360            let mut request_builder = self
8361                .client
8362                .request(method.clone(), url.clone())
8363                .header(AUTHORIZATION, format!("token {token}"))
8364                .timeout(std::time::Duration::from_secs(60)); // Increase timeout to 60 seconds
8365
8366            if let Some(ref body) = body {
8367                if debug_logging && attempt == 1 {
8368                    if let Ok(json_body) = serde_json::to_string_pretty(&body) {
8369                        eprintln!(
8370                            "📤 Request body ({} bytes):\n{}",
8371                            json_body.len(),
8372                            json_body
8373                        );
8374                    } else {
8375                        eprintln!("📤 Request body: [serialization failed]");
8376                    }
8377                }
8378                request_builder = request_builder.json(&body);
8379            } else if debug_logging && attempt == 1 {
8380                eprintln!("📤 Request body: [empty]");
8381            }
8382
8383            if debug_logging {
8384                eprintln!("🚀 Sending HTTP request (attempt {attempt})...");
8385            }
8386
8387            match request_builder.send().await {
8388                Ok(response) => {
8389                    let status = response.status();
8390
8391                    if debug_logging {
8392                        eprintln!("📥 Response status: {status}");
8393                    }
8394
8395                    if !status.is_success() {
8396                        let error_text = response
8397                            .text()
8398                            .await
8399                            .unwrap_or_else(|_| "Unknown error".to_string());
8400
8401                        if debug_logging {
8402                            eprintln!("❌ Error response body: {error_text}");
8403                        }
8404
8405                        return Err(Error::RequestFailed(format!(
8406                            "Request to {path:?} failed with status {status}: {error_text}"
8407                        )));
8408                    }
8409
8410                    if debug_logging {
8411                        eprintln!("✅ HTTP request successful");
8412                    }
8413
8414                    return Ok(response);
8415                }
8416                Err(e) => {
8417                    if debug_logging {
8418                        eprintln!("❌ HTTP request failed (attempt {attempt}): {e:?}");
8419                        eprintln!("   Error kind: {:?}", e.is_timeout());
8420                        eprintln!("   Is connect error: {}", e.is_connect());
8421                        eprintln!("   Is request error: {}", e.is_request());
8422                    }
8423
8424                    last_error = Some(e);
8425
8426                    // Only retry on network/connection errors, not on client errors
8427                    if attempt < max_retries {
8428                        #[allow(clippy::cast_sign_loss)] // attempt is always positive (1-3)
8429                        let delay = std::time::Duration::from_millis((attempt as u64) * 1000);
8430                        if debug_logging {
8431                            eprintln!("⏳ Waiting {}ms before retry...", delay.as_millis());
8432                        }
8433                        tokio::time::sleep(delay).await;
8434                    }
8435                }
8436            }
8437        }
8438
8439        // If we get here, all retries failed
8440        if debug_logging {
8441            eprintln!("❌ All {max_retries} retry attempts failed");
8442        }
8443
8444        Err(Error::Reqwest(last_error.unwrap()))
8445    }
8446
8447    async fn request_json<T: DeserializeOwned>(
8448        &self,
8449        method: Method,
8450        path: &[&str],
8451        body: Option<impl serde::Serialize>,
8452    ) -> Result<T, Error> {
8453        // Capture request context for better error messages
8454        let method_str = method.to_string();
8455        let mut url = self.base_url.clone();
8456        url.path_segments_mut().unwrap().extend(path);
8457        let endpoint = url.to_string();
8458        let expected_type = std::any::type_name::<T>().to_string();
8459
8460        let response = self.request_raw(method, path, body).await?;
8461
8462        // Try to deserialize, capturing raw response on failure
8463        match response.text().await {
8464            Ok(raw_response) => serde_json::from_str(&raw_response).map_err(|e| {
8465                Error::ResponseDeserializationFailed {
8466                    method: method_str,
8467                    endpoint,
8468                    expected_type,
8469                    serde_error: e.to_string(),
8470                    raw_response,
8471                }
8472            }),
8473            Err(e) => Err(Error::ResponseParsingFailed(format!(
8474                "Failed to read response body: {e}"
8475            ))),
8476        }
8477    }
8478
8479    async fn request_empty(
8480        &self,
8481        method: Method,
8482        path: &[&str],
8483        body: Option<impl serde::Serialize>,
8484    ) -> Result<(), Error> {
8485        self.request_raw(method, path, body).await?;
8486        Ok(())
8487    }
8488
8489    /// Gets the API changelog.
8490    ///
8491    /// # Errors
8492    ///
8493    /// Returns an error if:
8494    /// - Authentication fails
8495    /// - The HTTP request fails
8496    /// - The server returns an error status
8497    /// - The response cannot be parsed as JSON
8498    pub async fn get_changelog(&self) -> Result<serde_json::Value, Error> {
8499        self.request_json(Method::GET, &["changelog"], None::<&()>)
8500            .await
8501    }
8502
8503    /// Changes the user's password.
8504    ///
8505    /// # Errors
8506    ///
8507    /// Returns an error if:
8508    /// - Authentication fails
8509    /// - The HTTP request fails
8510    /// - The server rejects the password change
8511    /// - The response cannot be parsed
8512    pub async fn user_change_password(
8513        &self,
8514        password: Secret<String>,
8515    ) -> Result<ChangePasswordResponse, Error> {
8516        let request = ChangePasswordRequest {
8517            password: Secret::new(Password(password.expose_secret().clone())),
8518        };
8519        self.request_json(Method::POST, &["user", "change_password"], Some(request))
8520            .await
8521    }
8522
8523    /// Gets a list of all assets.
8524    ///
8525    /// # Errors
8526    ///
8527    /// Returns an error if:
8528    /// - Authentication fails
8529    /// - The HTTP request fails
8530    /// - The server returns an error status
8531    /// - The response cannot be parsed
8532    ///
8533    /// # Examples
8534    /// ```no_run
8535    /// # use amp_rs::ApiClient;
8536    /// # #[tokio::main]
8537    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
8538    /// let client = ApiClient::new().await?;
8539    ///
8540    /// let assets = client.get_assets().await?;
8541    /// for asset in assets {
8542    ///     println!("Asset: {} ({})", asset.name, asset.ticker.unwrap_or_default());
8543    /// }
8544    /// # Ok(())
8545    /// # }
8546    /// ```
8547    pub async fn get_assets(&self) -> Result<Vec<Asset>, Error> {
8548        self.request_json(Method::GET, &["assets"], None::<&()>)
8549            .await
8550    }
8551
8552    /// Gets a specific asset by UUID.
8553    ///
8554    /// # Errors
8555    ///
8556    /// Returns an error if:
8557    /// - Authentication fails
8558    /// - The HTTP request fails
8559    /// - The asset does not exist
8560    /// - The response cannot be parsed
8561    ///
8562    /// # Examples
8563    /// ```no_run
8564    /// # use amp_rs::ApiClient;
8565    /// # #[tokio::main]
8566    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
8567    /// let client = ApiClient::new().await?;
8568    ///
8569    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
8570    /// let asset = client.get_asset(asset_uuid).await?;
8571    ///
8572    /// println!("Asset: {} ({})", asset.name, asset.ticker.unwrap_or_default());
8573    /// println!("Registered: {}, Locked: {}", asset.is_registered, asset.is_locked);
8574    /// # Ok(())
8575    /// # }
8576    /// ```
8577    pub async fn get_asset(&self, asset_uuid: &str) -> Result<Asset, Error> {
8578        self.request_json(Method::GET, &["assets", asset_uuid], None::<&()>)
8579            .await
8580    }
8581
8582    /// Issues a new asset.
8583    ///
8584    /// # Errors
8585    ///
8586    /// Returns an error if:
8587    /// - Authentication fails
8588    /// - The HTTP request fails
8589    /// - The issuance request is invalid
8590    /// - The response cannot be parsed
8591    ///
8592    /// # Examples
8593    /// ```no_run
8594    /// # use amp_rs::{ApiClient, model::IssuanceRequest};
8595    /// # #[tokio::main]
8596    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
8597    /// let client = ApiClient::new().await?;
8598    ///
8599    /// let issuance_request = IssuanceRequest {
8600    ///     name: "My Token".to_string(),
8601    ///     amount: 1000000,
8602    ///     destination_address: "vjU2i2EM2viGEzSywpStMPkTX9U9QSDsLSN63kJJYVpxKJZuxaph8v5r5Jf11aqnfBVdjSbrvcJ2pw26".to_string(),
8603    ///     domain: "example.com".to_string(),
8604    ///     ticker: "MYTKN".to_string(),
8605    ///     pubkey: "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798".to_string(),
8606    ///     precision: Some(8),
8607    ///     is_confidential: Some(true),
8608    ///     is_reissuable: Some(false),
8609    ///     reissuance_amount: None,
8610    ///     reissuance_address: None,
8611    ///     transfer_restricted: Some(false),
8612    /// };
8613    ///
8614    /// let response = client.issue_asset(&issuance_request).await?;
8615    /// println!("Issued asset with UUID: {}", response.asset_uuid);
8616    /// # Ok(())
8617    /// # }
8618    /// ```
8619    pub async fn issue_asset(
8620        &self,
8621        issuance_request: &IssuanceRequest,
8622    ) -> Result<IssuanceResponse, Error> {
8623        self.request_json(Method::POST, &["assets", "issue"], Some(issuance_request))
8624            .await
8625    }
8626
8627    /// Edits an existing asset.
8628    ///
8629    /// # Errors
8630    ///
8631    /// Returns an error if:
8632    /// - Authentication fails
8633    /// - The HTTP request fails
8634    /// - The asset does not exist
8635    /// - The edit request is invalid
8636    /// - The response cannot be parsed
8637    pub async fn edit_asset(
8638        &self,
8639        asset_uuid: &str,
8640        edit_asset_request: &EditAssetRequest,
8641    ) -> Result<Asset, Error> {
8642        self.request_json(
8643            Method::PUT,
8644            &["assets", asset_uuid, "edit"],
8645            Some(edit_asset_request),
8646        )
8647        .await
8648    }
8649
8650    /// Registers an asset with the Blockstream Asset Registry.
8651    ///
8652    /// This method publishes an asset to the public registry, making it discoverable
8653    /// and verifiable by other users and applications. The asset must already exist
8654    /// in the AMP system before it can be registered.
8655    ///
8656    /// # Arguments
8657    ///
8658    /// * `asset_uuid` - The unique identifier of the asset to register
8659    ///
8660    /// # Returns
8661    ///
8662    /// Returns a `RegisterAssetResponse` containing:
8663    /// - `success`: Boolean indicating whether the registration was successful
8664    /// - `message`: Optional status message from the API
8665    /// - `asset_id`: The registered asset identifier (hex string)
8666    ///
8667    /// # Errors
8668    ///
8669    /// Returns an error if:
8670    /// - The asset does not exist or cannot be found (404)
8671    /// - Authentication fails or token is invalid (401)
8672    /// - The asset is already registered (returns success with appropriate message)
8673    /// - Network connectivity issues occur
8674    /// - The server returns an error status (5xx)
8675    /// - The response cannot be parsed
8676    ///
8677    /// # Examples
8678    ///
8679    /// ```no_run
8680    /// # use amp_rs::ApiClient;
8681    /// # #[tokio::main]
8682    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
8683    /// let client = ApiClient::new().await?;
8684    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
8685    ///
8686    /// let response = client.register_asset(asset_uuid).await?;
8687    /// if response.success {
8688    ///     println!("Asset registered successfully!");
8689    ///     if let Some(asset) = response.asset_data {
8690    ///         println!("Asset ID: {}", asset.asset_id);
8691    ///     }
8692    ///     if let Some(message) = response.message {
8693    ///         println!("Message: {}", message);
8694    ///     }
8695    /// }
8696    /// # Ok(())
8697    /// # }
8698    /// ```
8699    pub async fn register_asset(&self, asset_uuid: &str) -> Result<RegisterAssetResponse, Error> {
8700        // Make HTTP request directly to handle both success and error responses
8701        let token = self.get_token().await?;
8702        let mut url = self.base_url.clone();
8703        url.path_segments_mut()
8704            .unwrap()
8705            .extend(&["assets", asset_uuid, "register"]);
8706
8707        let response = self
8708            .client
8709            .request(Method::GET, url)
8710            .header(AUTHORIZATION, format!("token {token}"))
8711            .timeout(std::time::Duration::from_secs(60))
8712            .send()
8713            .await
8714            .map_err(|e| Error::RequestFailed(format!("HTTP request failed: {e}")))?;
8715
8716        let status = response.status();
8717        let response_text = response.text().await.map_err(|e| {
8718            Error::ResponseParsingFailed(format!("Failed to read response body: {e}"))
8719        })?;
8720
8721        // Handle HTTP 200 - success case
8722        if status == reqwest::StatusCode::OK {
8723            // Try to parse as Asset (full registration response)
8724            if let Ok(asset) = serde_json::from_str::<Asset>(&response_text) {
8725                return Ok(RegisterAssetResponse {
8726                    success: true,
8727                    message: Some("Asset registered successfully".to_string()),
8728                    asset_data: Some(asset),
8729                });
8730            }
8731
8732            // If parsing as Asset fails, return success with raw message
8733            return Ok(RegisterAssetResponse {
8734                success: true,
8735                message: Some(response_text),
8736                asset_data: None,
8737            });
8738        }
8739
8740        // Handle error responses
8741        // Try to parse error response as JSON
8742        if let Ok(error_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
8743            // Check for "already registered" error
8744            if let Some(error_msg) = error_json.get("Error").and_then(|e| e.as_str()) {
8745                let error_msg_lower = error_msg.to_lowercase();
8746                if error_msg_lower.contains("already registered") {
8747                    return Ok(RegisterAssetResponse {
8748                        success: true,
8749                        message: Some("Asset is already registered".to_string()),
8750                        asset_data: None,
8751                    });
8752                }
8753
8754                // Other errors - return as error
8755                return Err(Error::RequestFailed(format!(
8756                    "Request to [\"assets\", \"{asset_uuid}\", \"register\"] failed with status {status}: {error_msg}"
8757                )));
8758            }
8759        }
8760
8761        // Fallback error for non-JSON or unexpected responses
8762        Err(Error::RequestFailed(format!(
8763            "Request to [\"assets\", \"{asset_uuid}\", \"register\"] failed with status {status}: {response_text}"
8764        )))
8765    }
8766
8767    /// # Errors
8768    /// Returns an error if:
8769    /// - The asset does not exist or cannot be found
8770    /// - Authentication fails or token is invalid
8771    /// - Network connectivity issues occur
8772    /// - The server returns an error status
8773    pub async fn delete_asset(&self, asset_uuid: &str) -> Result<(), Error> {
8774        self.request_empty(
8775            Method::DELETE,
8776            &["assets", asset_uuid, "delete"],
8777            None::<&()>,
8778        )
8779        .await
8780    }
8781
8782    /// # Errors
8783    /// Returns an error if:
8784    /// - The transaction ID is invalid or not found
8785    /// - Authentication fails or token is invalid
8786    /// - Network connectivity issues occur
8787    /// - The server returns an error status
8788    /// - The response cannot be parsed
8789    pub async fn get_broadcast_status(&self, txid: &str) -> Result<BroadcastResponse, Error> {
8790        self.request_json(Method::GET, &["tx", "broadcast", txid], None::<&()>)
8791            .await
8792    }
8793
8794    /// # Errors
8795    /// Returns an error if:
8796    /// - The transaction hex is invalid or malformed
8797    /// - The transaction is rejected by the network
8798    /// - Authentication fails or token is invalid
8799    /// - Network connectivity issues occur
8800    /// - The server returns an error status
8801    /// - The response cannot be parsed
8802    pub async fn broadcast_transaction(&self, tx_hex: &str) -> Result<BroadcastResponse, Error> {
8803        self.request_json(Method::POST, &["tx", "broadcast"], Some(tx_hex))
8804            .await
8805    }
8806
8807    /// # Errors
8808    /// Returns an error if:
8809    /// - The asset UUID is invalid or not found
8810    /// - The user lacks authorization to register the asset
8811    /// - The asset is already registered
8812    /// - Authentication fails or token is invalid
8813    /// - Network connectivity issues occur
8814    /// - The server returns an error status
8815    /// - The response cannot be parsed
8816    pub async fn register_asset_authorized(&self, asset_uuid: &str) -> Result<Asset, Error> {
8817        self.request_json(
8818            Method::GET,
8819            &["assets", asset_uuid, "register-authorized"],
8820            None::<&()>,
8821        )
8822        .await
8823    }
8824
8825    /// # Errors
8826    /// Returns an error if:
8827    /// - The asset UUID is invalid or not found
8828    /// - The asset is already locked
8829    /// - The user lacks permission to lock the asset
8830    /// - Authentication fails or token is invalid
8831    /// - Network connectivity issues occur
8832    /// - The server returns an error status
8833    /// - The response cannot be parsed
8834    pub async fn lock_asset(&self, asset_uuid: &str) -> Result<Asset, Error> {
8835        self.request_json(Method::PUT, &["assets", asset_uuid, "lock"], None::<&()>)
8836            .await
8837    }
8838
8839    /// # Errors
8840    /// Returns an error if:
8841    /// - The asset UUID is invalid or not found
8842    /// - The asset is not currently locked
8843    /// - The user lacks permission to unlock the asset
8844    /// - Authentication fails or token is invalid
8845    /// - Network connectivity issues occur
8846    /// - The server returns an error status
8847    /// - The response cannot be parsed
8848    pub async fn unlock_asset(&self, asset_uuid: &str) -> Result<Asset, Error> {
8849        self.request_json(Method::PUT, &["assets", asset_uuid, "unlock"], None::<&()>)
8850            .await
8851    }
8852
8853    /// # Errors
8854    /// Returns an error if:
8855    /// - The asset UUID is invalid or not found
8856    /// - The activity parameters are invalid
8857    /// - Authentication fails or token is invalid
8858    /// - Network connectivity issues occur
8859    /// - The server returns an error status
8860    /// - The response cannot be parsed
8861    pub async fn get_asset_activities(
8862        &self,
8863        asset_uuid: &str,
8864        params: &AssetActivityParams,
8865    ) -> Result<Vec<Activity>, Error> {
8866        self.request_json(
8867            Method::GET,
8868            &["assets", asset_uuid, "activities"],
8869            Some(params),
8870        )
8871        .await
8872    }
8873
8874    /// # Errors
8875    /// Returns an error if:
8876    /// - The asset UUID is invalid or not found
8877    /// - The specified height is invalid or out of range
8878    /// - Authentication fails or token is invalid
8879    /// - Network connectivity issues occur
8880    /// - The server returns an error status
8881    /// - The response cannot be parsed
8882    pub async fn get_asset_ownerships(
8883        &self,
8884        asset_uuid: &str,
8885        height: Option<i64>,
8886    ) -> Result<Vec<Ownership>, Error> {
8887        let mut path = vec!["assets", asset_uuid, "ownerships"];
8888        let height_str;
8889        if let Some(h) = height {
8890            height_str = h.to_string();
8891            path.push(&height_str);
8892        }
8893        self.request_json(Method::GET, &path, None::<&()>).await
8894    }
8895
8896    /// # Errors
8897    /// Returns an error if:
8898    /// - The asset UUID is invalid or not found
8899    /// - Authentication fails or token is invalid
8900    /// - Network connectivity issues occur
8901    /// - The server returns an error status
8902    /// - The response cannot be parsed
8903    pub async fn get_asset_balance(&self, asset_uuid: &str) -> Result<Balance, Error> {
8904        self.request_json(Method::GET, &["assets", asset_uuid, "balance"], None::<&()>)
8905            .await
8906    }
8907
8908    /// # Errors
8909    /// Returns an error if:
8910    /// - The asset UUID is invalid or not found
8911    /// - Authentication fails or token is invalid
8912    /// - Network connectivity issues occur
8913    /// - The server returns an error status
8914    /// - The response cannot be parsed
8915    pub async fn get_asset_summary(&self, asset_uuid: &str) -> Result<AssetSummary, Error> {
8916        self.request_json(Method::GET, &["assets", asset_uuid, "summary"], None::<&()>)
8917            .await
8918    }
8919
8920    /// # Errors
8921    /// Returns an error if:
8922    /// - The asset UUID is invalid or not found
8923    /// - Authentication fails or token is invalid
8924    /// - Network connectivity issues occur
8925    /// - The server returns an error status
8926    /// - The response cannot be parsed
8927    pub async fn get_asset_utxos(&self, asset_uuid: &str) -> Result<Vec<Utxo>, Error> {
8928        self.request_json(Method::GET, &["assets", asset_uuid, "utxos"], None::<&()>)
8929            .await
8930    }
8931
8932    /// Gets the memo for a specific asset.
8933    ///
8934    /// # Arguments
8935    /// * `asset_uuid` - The UUID of the asset to retrieve the memo for
8936    ///
8937    /// # Returns
8938    /// The memo string associated with the asset
8939    ///
8940    /// # Errors
8941    /// Returns an error if:
8942    /// - Authentication fails
8943    /// - The HTTP request fails
8944    /// - The server returns an error status
8945    /// - The asset does not exist
8946    /// - The response cannot be parsed
8947    pub async fn get_asset_memo(&self, asset_uuid: &str) -> Result<String, Error> {
8948        self.request_json(Method::GET, &["assets", asset_uuid, "memo"], None::<&()>)
8949            .await
8950    }
8951
8952    /// Sets a memo for the specified asset.
8953    ///
8954    /// # Arguments
8955    /// * `asset_uuid` - The UUID of the asset to set the memo for
8956    /// * `memo` - The memo string to associate with the asset
8957    ///
8958    /// # Returns
8959    /// Returns `Ok(())` on success.
8960    ///
8961    /// # Errors
8962    /// Returns an error if:
8963    /// - Authentication fails
8964    /// - The HTTP request fails
8965    /// - The server returns an error status
8966    /// - The asset does not exist
8967    /// - The memo cannot be set due to validation errors
8968    ///
8969    /// # Example
8970    /// ```no_run
8971    /// # use amp_rs::ApiClient;
8972    /// # #[tokio::main]
8973    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
8974    /// let client = ApiClient::new().await?;
8975    /// client.set_asset_memo("asset-uuid-123", "This is a memo for the asset").await?;
8976    /// # Ok(())
8977    /// # }
8978    /// ```
8979    pub async fn set_asset_memo(&self, asset_uuid: &str, memo: &str) -> Result<(), Error> {
8980        let token = self.get_token().await?;
8981        let mut url = self.base_url.clone();
8982        url.path_segments_mut()
8983            .unwrap()
8984            .extend(&["assets", asset_uuid, "memo", "set"]);
8985
8986        let response = self
8987            .client
8988            .request(Method::POST, url)
8989            .header(AUTHORIZATION, format!("token {token}"))
8990            .header("content-type", "application/json")
8991            .body(format!("\"{}\"", memo.replace('"', "\\\"")))
8992            .send()
8993            .await?;
8994
8995        if !response.status().is_success() {
8996            let status = response.status();
8997            let error_text = response
8998                .text()
8999                .await
9000                .unwrap_or_else(|_| "Unknown error".to_string());
9001            return Err(Error::RequestFailed(format!(
9002                "Request to [\"assets\", \"{asset_uuid}\", \"memo\", \"set\"] failed with status {status}: {error_text}"
9003            )));
9004        }
9005
9006        Ok(())
9007    }
9008
9009    /// Blacklists specific UTXOs for an asset to prevent them from being used in transactions.
9010    ///
9011    /// This method adds the specified UTXOs to the asset's blacklist, preventing them from being
9012    /// used in future transactions. This is typically used for security purposes when UTXOs are
9013    /// suspected to be compromised or need to be temporarily disabled.
9014    ///
9015    /// # Arguments
9016    /// * `asset_uuid` - The UUID of the asset to blacklist UTXOs for
9017    /// * `utxos` - A slice of `Outpoint` structs representing the UTXOs to blacklist
9018    ///
9019    /// # Returns
9020    /// Returns a vector of `Utxo` structs representing the blacklisted UTXOs with their updated status.
9021    ///
9022    /// # Errors
9023    /// Returns an error if:
9024    /// - Authentication fails or insufficient permissions
9025    /// - The asset UUID is invalid or does not exist
9026    /// - One or more UTXOs are invalid or already blacklisted
9027    /// - The HTTP request fails
9028    /// - The server returns an error status
9029    /// - The response cannot be parsed
9030    ///
9031    /// # Examples
9032    /// ```no_run
9033    /// # use amp_rs::{ApiClient, model::Outpoint};
9034    /// # #[tokio::main]
9035    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9036    /// let client = ApiClient::new().await?;
9037    ///
9038    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
9039    /// let utxos = vec![
9040    ///     Outpoint {
9041    ///         txid: "abc123...".to_string(),
9042    ///         vout: 0,
9043    ///     },
9044    ///     Outpoint {
9045    ///         txid: "def456...".to_string(),
9046    ///         vout: 1,
9047    ///     },
9048    /// ];
9049    ///
9050    /// let blacklisted_utxos = client.blacklist_asset_utxos(asset_uuid, &utxos).await?;
9051    /// println!("Blacklisted {} UTXOs", blacklisted_utxos.len());
9052    /// # Ok(())
9053    /// # }
9054    /// ```
9055    ///
9056    /// # Related Methods
9057    /// - [`whitelist_asset_utxos`](Self::whitelist_asset_utxos) - Remove UTXOs from blacklist
9058    /// - [`get_asset`](Self::get_asset) - Get asset information including UTXO status
9059    pub async fn blacklist_asset_utxos(
9060        &self,
9061        asset_uuid: &str,
9062        utxos: &[Outpoint],
9063    ) -> Result<Vec<Utxo>, Error> {
9064        self.request_json(
9065            Method::POST,
9066            &["assets", asset_uuid, "utxos", "blacklist"],
9067            Some(utxos),
9068        )
9069        .await
9070    }
9071
9072    /// Removes UTXOs from the asset's blacklist, allowing them to be used in transactions again.
9073    ///
9074    /// This method removes the specified UTXOs from the asset's blacklist, restoring their ability
9075    /// to be used in transactions. This is the reverse operation of blacklisting UTXOs.
9076    ///
9077    /// # Arguments
9078    /// * `asset_uuid` - The UUID of the asset to whitelist UTXOs for
9079    /// * `utxos` - A slice of `Outpoint` structs representing the UTXOs to remove from blacklist
9080    ///
9081    /// # Returns
9082    /// Returns a vector of `Utxo` structs representing the whitelisted UTXOs with their updated status.
9083    ///
9084    /// # Errors
9085    /// Returns an error if:
9086    /// - Authentication fails or insufficient permissions
9087    /// - The asset UUID is invalid or does not exist
9088    /// - One or more UTXOs are invalid or not currently blacklisted
9089    /// - The HTTP request fails
9090    /// - The server returns an error status
9091    /// - The response cannot be parsed
9092    ///
9093    /// # Examples
9094    /// ```no_run
9095    /// # use amp_rs::{ApiClient, model::Outpoint};
9096    /// # #[tokio::main]
9097    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9098    /// let client = ApiClient::new().await?;
9099    ///
9100    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
9101    /// let utxos = vec![
9102    ///     Outpoint {
9103    ///         txid: "abc123...".to_string(),
9104    ///         vout: 0,
9105    ///     },
9106    /// ];
9107    ///
9108    /// let whitelisted_utxos = client.whitelist_asset_utxos(asset_uuid, &utxos).await?;
9109    /// println!("Whitelisted {} UTXOs", whitelisted_utxos.len());
9110    /// # Ok(())
9111    /// # }
9112    /// ```
9113    ///
9114    /// # Related Methods
9115    /// - [`blacklist_asset_utxos`](Self::blacklist_asset_utxos) - Add UTXOs to blacklist
9116    /// - [`get_asset`](Self::get_asset) - Get asset information including UTXO status
9117    pub async fn whitelist_asset_utxos(
9118        &self,
9119        asset_uuid: &str,
9120        utxos: &[Outpoint],
9121    ) -> Result<Vec<Utxo>, Error> {
9122        self.request_json(
9123            Method::POST,
9124            &["assets", asset_uuid, "utxos", "whitelist"],
9125            Some(utxos),
9126        )
9127        .await
9128    }
9129
9130    /// Gets the treasury addresses for a specific asset
9131    ///
9132    /// # Arguments
9133    /// * `asset_uuid` - The UUID of the asset to get treasury addresses for
9134    ///
9135    /// # Returns
9136    /// A vector of treasury addresses as strings
9137    ///
9138    /// # Errors
9139    /// Returns an error if:
9140    /// - The asset does not exist
9141    /// - The request fails
9142    /// - The response cannot be parsed
9143    pub async fn get_asset_treasury_addresses(
9144        &self,
9145        asset_uuid: &str,
9146    ) -> Result<Vec<String>, Error> {
9147        self.request_json(
9148            Method::GET,
9149            &["assets", asset_uuid, "treasury-addresses"],
9150            None::<&()>,
9151        )
9152        .await
9153    }
9154
9155    /// Adds treasury addresses to a specific asset
9156    ///
9157    /// # Arguments
9158    /// * `asset_uuid` - The UUID of the asset to add treasury addresses to
9159    /// * `addresses` - A slice of address strings to add as treasury addresses
9160    ///
9161    /// # Returns
9162    /// Returns `Ok(())` on success
9163    ///
9164    /// # Errors
9165    /// Returns an error if:
9166    /// - The asset does not exist
9167    /// - The addresses are invalid
9168    /// - The request fails
9169    /// - Insufficient permissions
9170    pub async fn add_asset_treasury_addresses(
9171        &self,
9172        asset_uuid: &str,
9173        addresses: &[String],
9174    ) -> Result<(), Error> {
9175        self.request_empty(
9176            Method::POST,
9177            &["assets", asset_uuid, "treasury-addresses", "add"],
9178            Some(addresses),
9179        )
9180        .await
9181    }
9182
9183    /// Removes treasury addresses from a specific asset.
9184    ///
9185    /// This method removes the specified addresses from the asset's treasury address list.
9186    /// Treasury addresses are special addresses that can be used for asset management operations
9187    /// such as reissuance and burning.
9188    ///
9189    /// # Arguments
9190    /// * `asset_uuid` - The UUID of the asset to remove treasury addresses from
9191    /// * `addresses` - A slice of address strings to remove from the treasury addresses
9192    ///
9193    /// # Returns
9194    /// Returns `Ok(())` on successful removal.
9195    ///
9196    /// # Errors
9197    /// Returns an error if:
9198    /// - Authentication fails or insufficient permissions
9199    /// - The asset UUID is invalid or does not exist
9200    /// - One or more addresses are invalid or not currently treasury addresses
9201    /// - The HTTP request fails
9202    /// - The server returns an error status
9203    /// - Attempting to remove the last treasury address (if not allowed)
9204    ///
9205    /// # Examples
9206    /// ```no_run
9207    /// # use amp_rs::ApiClient;
9208    /// # #[tokio::main]
9209    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9210    /// let client = ApiClient::new().await?;
9211    ///
9212    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
9213    /// let addresses = vec![
9214    ///     "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh".to_string(),
9215    ///     "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4".to_string(),
9216    /// ];
9217    ///
9218    /// client.delete_asset_treasury_addresses(asset_uuid, &addresses).await?;
9219    /// println!("Removed {} treasury addresses", addresses.len());
9220    /// # Ok(())
9221    /// # }
9222    /// ```
9223    ///
9224    /// # Related Methods
9225    /// - [`add_asset_treasury_addresses`](Self::add_asset_treasury_addresses) - Add treasury addresses
9226    /// - [`get_asset_treasury_addresses`](Self::get_asset_treasury_addresses) - Get current treasury addresses
9227    /// - [`reissue_asset`](Self::reissue_asset) - Reissue assets using treasury addresses
9228    pub async fn delete_asset_treasury_addresses(
9229        &self,
9230        asset_uuid: &str,
9231        addresses: &[String],
9232    ) -> Result<(), Error> {
9233        self.request_empty(
9234            Method::DELETE,
9235            &["assets", asset_uuid, "treasury-addresses", "delete"],
9236            Some(addresses),
9237        )
9238        .await
9239    }
9240
9241    /// Gets a list of all registered users.
9242    ///
9243    /// # Errors
9244    /// Returns an error if:
9245    /// - Authentication fails
9246    /// - The HTTP request fails
9247    /// - The server returns an error status
9248    /// - The response cannot be parsed
9249    ///
9250    /// # Examples
9251    /// ```no_run
9252    /// # use amp_rs::ApiClient;
9253    /// # #[tokio::main]
9254    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9255    /// let client = ApiClient::new().await?;
9256    ///
9257    /// let users = client.get_registered_users().await?;
9258    /// for user in users {
9259    ///     println!("User: {} (ID: {})", user.name, user.id);
9260    /// }
9261    /// # Ok(())
9262    /// # }
9263    /// ```
9264    pub async fn get_registered_users(
9265        &self,
9266    ) -> Result<Vec<crate::model::RegisteredUserResponse>, Error> {
9267        self.request_json(Method::GET, &["registered_users"], None::<&()>)
9268            .await
9269    }
9270
9271    /// Gets a specific registered user by ID.
9272    ///
9273    /// # Arguments
9274    /// * `user_id` - The ID of the registered user to retrieve
9275    ///
9276    /// # Errors
9277    /// Returns an error if:
9278    /// - Authentication fails
9279    /// - The HTTP request fails
9280    /// - The user ID does not exist
9281    /// - The response cannot be parsed
9282    ///
9283    /// # Examples
9284    /// ```no_run
9285    /// # use amp_rs::ApiClient;
9286    /// # #[tokio::main]
9287    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9288    /// let client = ApiClient::new().await?;
9289    ///
9290    /// let user = client.get_registered_user(1).await?;
9291    /// println!("User: {} (ID: {})", user.name, user.id);
9292    /// # Ok(())
9293    /// # }
9294    /// ```
9295    pub async fn get_registered_user(
9296        &self,
9297        user_id: i64,
9298    ) -> Result<crate::model::RegisteredUserResponse, Error> {
9299        self.request_json(
9300            Method::GET,
9301            &["registered_users", &user_id.to_string()],
9302            None::<&()>,
9303        )
9304        .await
9305    }
9306
9307    /// Creates a new registered user in the AMP system.
9308    ///
9309    /// This method creates a new registered user with the provided information. Registered users
9310    /// can be associated with GAIDs, assigned to categories, and receive asset assignments.
9311    ///
9312    /// # Arguments
9313    /// * `new_user` - A `RegisteredUserAdd` struct containing the user information to create
9314    ///
9315    /// # Returns
9316    /// Returns a `RegisteredUserResponse` containing the created user's information including
9317    /// the assigned user ID.
9318    ///
9319    /// # Errors
9320    /// Returns an error if:
9321    /// - Authentication fails or insufficient permissions
9322    /// - The user data is invalid (e.g., missing required fields, invalid email format)
9323    /// - A user with the same identifier already exists
9324    /// - The HTTP request fails
9325    /// - The server returns an error status
9326    /// - The response cannot be parsed
9327    ///
9328    /// # Examples
9329    /// ```no_run
9330    /// # use amp_rs::{ApiClient, model::RegisteredUserAdd};
9331    /// # #[tokio::main]
9332    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9333    /// let client = ApiClient::new().await?;
9334    ///
9335    /// let new_user = RegisteredUserAdd {
9336    ///     name: "John Doe".to_string(),
9337    ///     gaid: Some("GAbYScu6jkWUND2jo3L4KJxyvo55d".to_string()),
9338    ///     is_company: false,
9339    /// };
9340    ///
9341    /// let created_user = client.add_registered_user(&new_user).await?;
9342    /// println!("Created user: {} with ID {}", created_user.name, created_user.id);
9343    /// # Ok(())
9344    /// # }
9345    /// ```
9346    ///
9347    /// # Related Methods
9348    /// - [`get_registered_users`](Self::get_registered_users) - List all registered users
9349    /// - [`edit_registered_user`](Self::edit_registered_user) - Update user information
9350    /// - [`delete_registered_user`](Self::delete_registered_user) - Remove a user
9351    pub async fn add_registered_user(
9352        &self,
9353        new_user: &crate::model::RegisteredUserAdd,
9354    ) -> Result<crate::model::RegisteredUserResponse, Error> {
9355        self.request_json(Method::POST, &["registered_users", "add"], Some(new_user))
9356            .await
9357    }
9358
9359    /// Removes a registered user from the AMP system.
9360    ///
9361    /// This method permanently deletes a registered user and all associated data. This operation
9362    /// cannot be undone. Any GAIDs associated with the user will be disassociated, and any
9363    /// pending assignments may be affected.
9364    ///
9365    /// # Arguments
9366    /// * `user_id` - The ID of the registered user to delete
9367    ///
9368    /// # Returns
9369    /// Returns `Ok(())` on successful deletion.
9370    ///
9371    /// # Errors
9372    /// Returns an error if:
9373    /// - Authentication fails or insufficient permissions
9374    /// - The user ID is invalid or does not exist
9375    /// - The user has active assignments that prevent deletion
9376    /// - The HTTP request fails
9377    /// - The server returns an error status
9378    ///
9379    /// # Examples
9380    /// ```no_run
9381    /// # use amp_rs::ApiClient;
9382    /// # #[tokio::main]
9383    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9384    /// let client = ApiClient::new().await?;
9385    ///
9386    /// let user_id = 123;
9387    /// client.delete_registered_user(user_id).await?;
9388    /// println!("Successfully deleted user with ID {}", user_id);
9389    /// # Ok(())
9390    /// # }
9391    /// ```
9392    ///
9393    /// # Related Methods
9394    /// - [`get_registered_user`](Self::get_registered_user) - Get user information before deletion
9395    /// - [`add_registered_user`](Self::add_registered_user) - Create a new user
9396    /// - [`get_registered_user_summary`](Self::get_registered_user_summary) - Check user's assignments
9397    pub async fn delete_registered_user(&self, user_id: i64) -> Result<(), Error> {
9398        self.request_empty(
9399            Method::DELETE,
9400            &["registered_users", &user_id.to_string(), "delete"],
9401            None::<&()>,
9402        )
9403        .await
9404    }
9405
9406    /// Updates registered user information.
9407    ///
9408    /// This method allows you to modify the information of an existing registered user.
9409    /// Only the fields provided in the edit data will be updated; other fields remain unchanged.
9410    ///
9411    /// # Arguments
9412    /// * `registered_user_id` - The ID of the registered user to update
9413    /// * `edit_data` - A `RegisteredUserEdit` struct containing the fields to update
9414    ///
9415    /// # Returns
9416    /// Returns a `RegisteredUserResponse` containing the updated user information.
9417    ///
9418    /// # Errors
9419    /// Returns an error if:
9420    /// - Authentication fails or insufficient permissions
9421    /// - The user ID is invalid or does not exist
9422    /// - The edit data contains invalid values (e.g., invalid email format)
9423    /// - The HTTP request fails
9424    /// - The server returns an error status
9425    /// - The response cannot be parsed
9426    ///
9427    /// # Examples
9428    /// ```no_run
9429    /// # use amp_rs::{ApiClient, model::RegisteredUserEdit};
9430    /// # #[tokio::main]
9431    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9432    /// let client = ApiClient::new().await?;
9433    ///
9434    /// let user_id = 123;
9435    /// let edit_data = RegisteredUserEdit {
9436    ///     name: Some("Jane Doe".to_string()),
9437    /// };
9438    ///
9439    /// let updated_user = client.edit_registered_user(user_id, &edit_data).await?;
9440    /// println!("Updated user: {}", updated_user.name);
9441    /// # Ok(())
9442    /// # }
9443    /// ```
9444    ///
9445    /// # Related Methods
9446    /// - [`get_registered_user`](Self::get_registered_user) - Get current user information
9447    /// - [`add_registered_user`](Self::add_registered_user) - Create a new user
9448    /// - [`delete_registered_user`](Self::delete_registered_user) - Remove a user
9449    pub async fn edit_registered_user(
9450        &self,
9451        registered_user_id: i64,
9452        edit_data: &crate::model::RegisteredUserEdit,
9453    ) -> Result<crate::model::RegisteredUserResponse, Error> {
9454        self.request_json(
9455            Method::PUT,
9456            &["registered_users", &registered_user_id.to_string(), "edit"],
9457            Some(edit_data),
9458        )
9459        .await
9460    }
9461
9462    /// Gets comprehensive summary information for a registered user including assets and distributions.
9463    ///
9464    /// This method retrieves detailed summary information about a registered user, including
9465    /// their basic information, associated assets, assignment history, and distribution records.
9466    /// This provides a complete overview of the user's activity and holdings in the system.
9467    ///
9468    /// # Arguments
9469    /// * `registered_user_id` - The ID of the registered user to get summary for
9470    ///
9471    /// # Returns
9472    /// Returns a `RegisteredUserSummary` containing:
9473    /// - Basic user information (name, email, etc.)
9474    /// - List of associated GAIDs
9475    /// - Asset assignments and their status
9476    /// - Distribution history
9477    /// - Balance information
9478    /// - Activity timestamps
9479    ///
9480    /// # Errors
9481    /// Returns an error if:
9482    /// - Authentication fails or insufficient permissions
9483    /// - The user ID is invalid or does not exist
9484    /// - The HTTP request fails
9485    /// - The server returns an error status
9486    /// - The response cannot be parsed
9487    ///
9488    /// # Examples
9489    /// ```no_run
9490    /// # use amp_rs::ApiClient;
9491    /// # #[tokio::main]
9492    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9493    /// let client = ApiClient::new().await?;
9494    ///
9495    /// let user_id = 123;
9496    /// let summary = client.get_registered_user_summary(user_id).await?;
9497    ///
9498    /// println!("Asset UUID: {}", summary.asset_uuid);
9499    /// println!("Asset ID: {}", summary.asset_id);
9500    /// println!("Asset assignments: {}", summary.assignments.len());
9501    /// println!("Distributions received: {}", summary.distributions.len());
9502    /// # Ok(())
9503    /// # }
9504    /// ```
9505    ///
9506    /// # Related Methods
9507    /// - [`get_registered_user`](Self::get_registered_user) - Get basic user information
9508    /// - [`get_registered_user_gaids`](Self::get_registered_user_gaids) - Get only GAIDs
9509    /// - [`get_asset_assignments`](Self::get_asset_assignments) - Get assignments for specific asset
9510    pub async fn get_registered_user_summary(
9511        &self,
9512        registered_user_id: i64,
9513    ) -> Result<crate::model::RegisteredUserSummary, Error> {
9514        self.request_json(
9515            Method::GET,
9516            &[
9517                "registered_users",
9518                &registered_user_id.to_string(),
9519                "summary",
9520            ],
9521            None::<&()>,
9522        )
9523        .await
9524    }
9525
9526    /// Gets all GAIDs (Green Address IDs) associated with a registered user.
9527    ///
9528    /// This method retrieves a list of all GAIDs that are currently associated with the specified
9529    /// registered user. GAIDs are unique identifiers that can be used to receive assets and
9530    /// track ownership.
9531    ///
9532    /// # Arguments
9533    /// * `registered_user_id` - The ID of the registered user to get GAIDs for
9534    ///
9535    /// # Returns
9536    /// Returns a vector of GAID strings associated with the user.
9537    ///
9538    /// # Errors
9539    /// Returns an error if:
9540    /// - Authentication fails or insufficient permissions
9541    /// - The user ID is invalid or does not exist
9542    /// - The HTTP request fails
9543    /// - The server returns an error status
9544    /// - The response cannot be parsed
9545    ///
9546    /// # Examples
9547    /// ```no_run
9548    /// # use amp_rs::ApiClient;
9549    /// # #[tokio::main]
9550    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9551    /// let client = ApiClient::new().await?;
9552    ///
9553    /// let user_id = 123;
9554    /// let gaids = client.get_registered_user_gaids(user_id).await?;
9555    ///
9556    /// println!("User {} has {} associated GAIDs:", user_id, gaids.len());
9557    /// for gaid in gaids {
9558    ///     println!("  - {}", gaid);
9559    /// }
9560    /// # Ok(())
9561    /// # }
9562    /// ```
9563    ///
9564    /// # Related Methods
9565    /// - [`add_gaid_to_registered_user`](Self::add_gaid_to_registered_user) - Associate a GAID with user
9566    /// - [`set_default_gaid_for_registered_user`](Self::set_default_gaid_for_registered_user) - Set default GAID
9567    /// - [`get_gaid_registered_user`](Self::get_gaid_registered_user) - Find user by GAID
9568    /// - [`validate_gaid`](Self::validate_gaid) - Validate GAID format
9569    pub async fn get_registered_user_gaids(
9570        &self,
9571        registered_user_id: i64,
9572    ) -> Result<Vec<String>, Error> {
9573        self.request_json(
9574            Method::GET,
9575            &["registered_users", &registered_user_id.to_string(), "gaids"],
9576            None::<&()>,
9577        )
9578        .await
9579    }
9580
9581    /// Associates a GAID with a registered user.
9582    ///
9583    /// # Arguments
9584    /// * `registered_user_id` - The ID of the registered user
9585    /// * `gaid` - The GAID to associate with the user
9586    ///
9587    /// # Errors
9588    ///
9589    /// Returns an error if:
9590    /// - Authentication fails
9591    /// - The HTTP request fails
9592    /// - The server returns an error status
9593    /// - The registered user ID is invalid
9594    /// - The GAID is invalid or already associated
9595    pub async fn add_gaid_to_registered_user(
9596        &self,
9597        registered_user_id: i64,
9598        gaid: &str,
9599    ) -> Result<(), Error> {
9600        // Send GAID as a plain string, not wrapped in an object
9601        self.request_empty(
9602            Method::POST,
9603            &[
9604                "registered_users",
9605                &registered_user_id.to_string(),
9606                "gaids",
9607                "add",
9608            ],
9609            Some(gaid),
9610        )
9611        .await
9612    }
9613
9614    /// Sets an existing GAID as the default for a registered user.
9615    ///
9616    /// This method allows you to designate a specific GAID as the primary/default
9617    /// GAID for a registered user. The GAID must already be associated with the user.
9618    ///
9619    /// # Arguments
9620    /// * `registered_user_id` - The ID of the registered user
9621    /// * `gaid` - The GAID to set as default
9622    ///
9623    /// # Returns
9624    /// Returns `Ok(())` if the operation is successful.
9625    ///
9626    /// # Errors
9627    /// Returns an error if:
9628    /// - Authentication fails
9629    /// - The HTTP request fails
9630    /// - The server returns an error status
9631    /// - The registered user ID is invalid
9632    /// - The GAID is not associated with the user
9633    pub async fn set_default_gaid_for_registered_user(
9634        &self,
9635        registered_user_id: i64,
9636        gaid: &str,
9637    ) -> Result<(), Error> {
9638        // Send GAID as a plain string, not wrapped in an object
9639        self.request_empty(
9640            Method::POST,
9641            &[
9642                "registered_users",
9643                &registered_user_id.to_string(),
9644                "gaids",
9645                "set-default",
9646            ],
9647            Some(gaid),
9648        )
9649        .await
9650    }
9651
9652    /// Retrieves the registered user associated with a GAID
9653    ///
9654    /// # Arguments
9655    /// * `gaid` - The GAID to look up
9656    ///
9657    /// # Returns
9658    /// Returns the registered user data if the GAID is associated with a user
9659    ///
9660    /// # Errors
9661    /// This function will return an error if:
9662    /// - The GAID has no associated user
9663    /// - The GAID is invalid
9664    /// - Network or authentication errors occur
9665    pub async fn get_gaid_registered_user(
9666        &self,
9667        gaid: &str,
9668    ) -> Result<crate::model::RegisteredUserResponse, Error> {
9669        self.request_json(
9670            Method::GET,
9671            &["gaids", gaid, "registered_user"],
9672            None::<&()>,
9673        )
9674        .await
9675    }
9676
9677    /// Gets the balance information for a specific GAID.
9678    ///
9679    /// This method retrieves all asset balances associated with the given GAID,
9680    /// including confirmed balances and any lost outputs.
9681    ///
9682    /// # Arguments
9683    /// * `gaid` - The GAID to query balance for
9684    ///
9685    /// # Returns
9686    /// Returns a `Balance` struct containing confirmed balances and lost outputs
9687    ///
9688    /// # Errors
9689    /// Returns an error if:
9690    /// - The GAID is invalid
9691    /// - Network or authentication errors occur
9692    /// - The response cannot be parsed
9693    ///
9694    /// # Examples
9695    /// ```no_run
9696    /// # use amp_rs::ApiClient;
9697    /// # #[tokio::main]
9698    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9699    /// let client = ApiClient::new().await?;
9700    ///
9701    /// let gaid = "GAbYScu6jkWUND2jo3L4KJxyvo55d";
9702    /// let balance = client.get_gaid_balance(gaid).await?;
9703    ///
9704    /// println!("GAID {} has {} balance entries", gaid, balance.len());
9705    /// for entry in balance {
9706    ///     println!("Asset {}: {} units", entry.asset_id, entry.balance);
9707    /// }
9708    /// # Ok(())
9709    /// # }
9710    /// ```
9711    pub async fn get_gaid_balance(&self, gaid: &str) -> Result<Balance, Error> {
9712        self.request_json(Method::GET, &["gaids", gaid, "balance"], None::<&()>)
9713            .await
9714    }
9715
9716    /// Retrieves the specific asset balance for a GAID
9717    ///
9718    /// # Arguments
9719    /// * `gaid` - The GAID to query
9720    /// * `asset_uuid` - The UUID of the asset to query
9721    ///
9722    /// # Returns
9723    /// Returns the specific asset balance information
9724    ///
9725    /// # Errors
9726    /// Returns an error if:
9727    /// - The GAID is invalid
9728    /// - The asset UUID is invalid
9729    /// - Network or authentication errors occur
9730    /// - The response cannot be parsed
9731    pub async fn get_gaid_asset_balance(
9732        &self,
9733        gaid: &str,
9734        asset_uuid: &str,
9735    ) -> Result<Ownership, Error> {
9736        // Try to get the response as a GaidBalanceEntry first, then convert to Ownership
9737        let balance_entry: GaidBalanceEntry = self
9738            .request_json(
9739                Method::GET,
9740                &["gaids", gaid, "balance", asset_uuid],
9741                None::<&()>,
9742            )
9743            .await?;
9744
9745        // Convert GaidBalanceEntry to Ownership format
9746        Ok(Ownership {
9747            owner: gaid.to_string(),
9748            amount: balance_entry.balance,
9749            gaid: Some(gaid.to_string()),
9750        })
9751    }
9752
9753    /// Gets a list of all categories.
9754    ///
9755    /// # Returns
9756    /// Returns a vector of `CategoryResponse` objects
9757    ///
9758    /// # Errors
9759    /// Returns an error if:
9760    /// - Authentication fails
9761    /// - The HTTP request fails
9762    /// - The response cannot be parsed
9763    ///
9764    /// # Examples
9765    /// ```no_run
9766    /// # use amp_rs::ApiClient;
9767    /// # #[tokio::main]
9768    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9769    /// let client = ApiClient::new().await?;
9770    ///
9771    /// let categories = client.get_categories().await?;
9772    /// for category in categories {
9773    ///     println!("Category: {} (ID: {})", category.name, category.id);
9774    ///     if let Some(desc) = category.description {
9775    ///         println!("  Description: {}", desc);
9776    ///     }
9777    /// }
9778    /// # Ok(())
9779    /// # }
9780    /// ```
9781    pub async fn get_categories(&self) -> Result<Vec<CategoryResponse>, Error> {
9782        self.request_json(Method::GET, &["categories"], None::<&()>)
9783            .await
9784    }
9785
9786    /// Creates a new category for organizing users and assets.
9787    ///
9788    /// This method creates a new category that can be used to group registered users and assets
9789    /// for organizational purposes. Categories help manage permissions and provide logical
9790    /// groupings for assets and users.
9791    ///
9792    /// # Arguments
9793    /// * `new_category` - A `CategoryAdd` struct containing the category information to create
9794    ///
9795    /// # Returns
9796    /// Returns a `CategoryResponse` containing the created category information including
9797    /// the assigned category ID.
9798    ///
9799    /// # Errors
9800    /// Returns an error if:
9801    /// - Authentication fails or insufficient permissions
9802    /// - The category data is invalid (e.g., missing name, invalid characters)
9803    /// - A category with the same name already exists
9804    /// - The HTTP request fails
9805    /// - The server returns an error status
9806    /// - The response cannot be parsed
9807    ///
9808    /// # Examples
9809    /// ```no_run
9810    /// # use amp_rs::{ApiClient, model::CategoryAdd};
9811    /// # #[tokio::main]
9812    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9813    /// let client = ApiClient::new().await?;
9814    ///
9815    /// let new_category = CategoryAdd {
9816    ///     name: "Premium Users".to_string(),
9817    ///     description: Some("High-value users with special privileges".to_string()),
9818    /// };
9819    ///
9820    /// let created_category = client.add_category(&new_category).await?;
9821    /// println!("Created category: {} with ID {}", created_category.name, created_category.id);
9822    /// # Ok(())
9823    /// # }
9824    /// ```
9825    ///
9826    /// # Related Methods
9827    /// - [`get_categories`](Self::get_categories) - List all categories
9828    /// - [`edit_category`](Self::edit_category) - Update category information
9829    /// - [`delete_category`](Self::delete_category) - Remove a category
9830    /// - [`add_registered_user_to_category`](Self::add_registered_user_to_category) - Add users to category
9831    pub async fn add_category(
9832        &self,
9833        new_category: &CategoryAdd,
9834    ) -> Result<CategoryResponse, Error> {
9835        self.request_json(Method::POST, &["categories", "add"], Some(new_category))
9836            .await
9837    }
9838
9839    /// Gets a specific category by ID.
9840    ///
9841    /// This method retrieves detailed information about a specific category, including
9842    /// its name, description, and associated users and assets.
9843    ///
9844    /// # Arguments
9845    /// * `category_id` - The ID of the category to retrieve
9846    ///
9847    /// # Returns
9848    /// Returns a `CategoryResponse` containing the category information including:
9849    /// - Category ID, name, and description
9850    /// - List of associated registered users
9851    /// - List of associated assets
9852    /// - Creation and modification timestamps
9853    ///
9854    /// # Errors
9855    /// Returns an error if:
9856    /// - Authentication fails or insufficient permissions
9857    /// - The category ID is invalid or does not exist
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 category = client.get_category(category_id).await?;
9871    ///
9872    /// println!("Category: {} (ID: {})", category.name, category.id);
9873    /// if let Some(desc) = category.description {
9874    ///     println!("Description: {}", desc);
9875    /// }
9876    /// println!("Users: {}, Assets: {}", category.registered_users.len(), category.assets.len());
9877    /// # Ok(())
9878    /// # }
9879    /// ```
9880    ///
9881    /// # Related Methods
9882    /// - [`get_categories`](Self::get_categories) - List all categories
9883    /// - [`add_category`](Self::add_category) - Create a new category
9884    /// - [`edit_category`](Self::edit_category) - Update category information
9885    /// - [`delete_category`](Self::delete_category) - Remove a category
9886    pub async fn get_category(&self, category_id: i64) -> Result<CategoryResponse, Error> {
9887        self.request_json(
9888            Method::GET,
9889            &["categories", &category_id.to_string()],
9890            None::<&()>,
9891        )
9892        .await
9893    }
9894
9895    /// Updates category information.
9896    ///
9897    /// This method allows you to modify the information of an existing category.
9898    /// Only the fields provided in the edit data will be updated; other fields remain unchanged.
9899    ///
9900    /// # Arguments
9901    /// * `category_id` - The ID of the category to update
9902    /// * `edit_category` - A `CategoryEdit` struct containing the fields to update
9903    ///
9904    /// # Returns
9905    /// Returns a `CategoryResponse` containing the updated category information.
9906    ///
9907    /// # Errors
9908    /// Returns an error if:
9909    /// - Authentication fails or insufficient permissions
9910    /// - The category ID is invalid or does not exist
9911    /// - The edit data contains invalid values (e.g., empty name, invalid characters)
9912    /// - A category with the new name already exists (if name is being changed)
9913    /// - The HTTP request fails
9914    /// - The server returns an error status
9915    /// - The response cannot be parsed
9916    ///
9917    /// # Examples
9918    /// ```no_run
9919    /// # use amp_rs::{ApiClient, model::CategoryEdit};
9920    /// # #[tokio::main]
9921    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9922    /// let client = ApiClient::new().await?;
9923    ///
9924    /// let category_id = 1;
9925    /// let edit_data = CategoryEdit {
9926    ///     name: Some("VIP Users".to_string()),
9927    ///     description: Some("Very important users with premium access".to_string()),
9928    /// };
9929    ///
9930    /// let updated_category = client.edit_category(category_id, &edit_data).await?;
9931    /// println!("Updated category: {}", updated_category.name);
9932    /// # Ok(())
9933    /// # }
9934    /// ```
9935    ///
9936    /// # Related Methods
9937    /// - [`get_category`](Self::get_category) - Get current category information
9938    /// - [`add_category`](Self::add_category) - Create a new category
9939    /// - [`delete_category`](Self::delete_category) - Remove a category
9940    pub async fn edit_category(
9941        &self,
9942        category_id: i64,
9943        edit_category: &CategoryEdit,
9944    ) -> Result<CategoryResponse, Error> {
9945        self.request_json(
9946            Method::PUT,
9947            &["categories", &category_id.to_string(), "edit"],
9948            Some(edit_category),
9949        )
9950        .await
9951    }
9952
9953    /// Removes a category from the system.
9954    ///
9955    /// This method permanently deletes a category. All users and assets associated with the
9956    /// category will be disassociated, but the users and assets themselves are not deleted.
9957    /// This operation cannot be undone.
9958    ///
9959    /// # Arguments
9960    /// * `category_id` - The ID of the category to delete
9961    ///
9962    /// # Returns
9963    /// Returns `Ok(())` on successful deletion.
9964    ///
9965    /// # Errors
9966    /// Returns an error if:
9967    /// - Authentication fails or insufficient permissions
9968    /// - The category ID is invalid or does not exist
9969    /// - The category is still in use and cannot be deleted (depending on system configuration)
9970    /// - The HTTP request fails
9971    /// - The server returns an error status
9972    ///
9973    /// # Examples
9974    /// ```no_run
9975    /// # use amp_rs::ApiClient;
9976    /// # #[tokio::main]
9977    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9978    /// let client = ApiClient::new().await?;
9979    ///
9980    /// let category_id = 1;
9981    /// client.delete_category(category_id).await?;
9982    /// println!("Successfully deleted category with ID {}", category_id);
9983    /// # Ok(())
9984    /// # }
9985    /// ```
9986    ///
9987    /// # Related Methods
9988    /// - [`get_category`](Self::get_category) - Get category information before deletion
9989    /// - [`add_category`](Self::add_category) - Create a new category
9990    /// - [`remove_registered_user_from_category`](Self::remove_registered_user_from_category) - Remove users first
9991    /// - [`remove_asset_from_category`](Self::remove_asset_from_category) - Remove assets first
9992    pub async fn delete_category(&self, category_id: i64) -> Result<(), Error> {
9993        self.request_empty(
9994            Method::DELETE,
9995            &["categories", &category_id.to_string(), "delete"],
9996            None::<&()>,
9997        )
9998        .await
9999    }
10000
10001    /// Associates a registered user with a category.
10002    ///
10003    /// This method adds a registered user to a category, allowing for organized grouping
10004    /// of users. Users can belong to multiple categories, and categories can contain
10005    /// multiple users.
10006    ///
10007    /// # Arguments
10008    /// * `category_id` - The ID of the category to add the user to
10009    /// * `user_id` - The ID of the registered user to add to the category
10010    ///
10011    /// # Returns
10012    /// Returns a `CategoryResponse` containing the updated category information including
10013    /// the newly added user.
10014    ///
10015    /// # Errors
10016    /// Returns an error if:
10017    /// - Authentication fails or insufficient permissions
10018    /// - The category ID is invalid or does not exist
10019    /// - The user ID is invalid or does not exist
10020    /// - The user is already associated with the category
10021    /// - The HTTP request fails
10022    /// - The server returns an error status
10023    /// - The response cannot be parsed
10024    ///
10025    /// # Examples
10026    /// ```no_run
10027    /// # use amp_rs::ApiClient;
10028    /// # #[tokio::main]
10029    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
10030    /// let client = ApiClient::new().await?;
10031    ///
10032    /// let category_id = 1;
10033    /// let user_id = 123;
10034    ///
10035    /// let updated_category = client.add_registered_user_to_category(category_id, user_id).await?;
10036    /// println!("Added user {} to category '{}'", user_id, updated_category.name);
10037    /// println!("Category now has {} users", updated_category.registered_users.len());
10038    /// # Ok(())
10039    /// # }
10040    /// ```
10041    ///
10042    /// # Related Methods
10043    /// - [`remove_registered_user_from_category`](Self::remove_registered_user_from_category) - Remove user from category
10044    /// - [`get_category`](Self::get_category) - Get category information including users
10045    /// - [`get_registered_user`](Self::get_registered_user) - Get user information
10046    pub async fn add_registered_user_to_category(
10047        &self,
10048        category_id: i64,
10049        user_id: i64,
10050    ) -> Result<CategoryResponse, Error> {
10051        self.request_json(
10052            Method::PUT,
10053            &[
10054                "categories",
10055                &category_id.to_string(),
10056                "registered_users",
10057                &user_id.to_string(),
10058                "add",
10059            ],
10060            None::<&()>,
10061        )
10062        .await
10063    }
10064
10065    /// Removes a registered user from a category.
10066    ///
10067    /// This method disassociates a registered user from a category. The user remains in the
10068    /// system but is no longer part of the specified category. This does not affect the user's
10069    /// association with other categories.
10070    ///
10071    /// # Arguments
10072    /// * `category_id` - The ID of the category to remove the user from
10073    /// * `user_id` - The ID of the registered user to remove from the category
10074    ///
10075    /// # Returns
10076    /// Returns a `CategoryResponse` containing the updated category information without
10077    /// the removed user.
10078    ///
10079    /// # Errors
10080    /// Returns an error if:
10081    /// - Authentication fails or insufficient permissions
10082    /// - The category ID is invalid or does not exist
10083    /// - The user ID is invalid or does not exist
10084    /// - The user is not currently associated with the category
10085    /// - The HTTP request fails
10086    /// - The server returns an error status
10087    /// - The response cannot be parsed
10088    ///
10089    /// # Examples
10090    /// ```no_run
10091    /// # use amp_rs::ApiClient;
10092    /// # #[tokio::main]
10093    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
10094    /// let client = ApiClient::new().await?;
10095    ///
10096    /// let category_id = 1;
10097    /// let user_id = 123;
10098    ///
10099    /// let updated_category = client.remove_registered_user_from_category(category_id, user_id).await?;
10100    /// println!("Removed user {} from category '{}'", user_id, updated_category.name);
10101    /// println!("Category now has {} users", updated_category.registered_users.len());
10102    /// # Ok(())
10103    /// # }
10104    /// ```
10105    ///
10106    /// # Related Methods
10107    /// - [`add_registered_user_to_category`](Self::add_registered_user_to_category) - Add user to category
10108    /// - [`get_category`](Self::get_category) - Get category information including users
10109    /// - [`get_registered_user`](Self::get_registered_user) - Get user information
10110    pub async fn remove_registered_user_from_category(
10111        &self,
10112        category_id: i64,
10113        user_id: i64,
10114    ) -> Result<CategoryResponse, Error> {
10115        self.request_json(
10116            Method::PUT,
10117            &[
10118                "categories",
10119                &category_id.to_string(),
10120                "registered_users",
10121                &user_id.to_string(),
10122                "remove",
10123            ],
10124            None::<&()>,
10125        )
10126        .await
10127    }
10128
10129    /// Associates an asset with a category.
10130    ///
10131    /// This method adds an asset to a category, allowing for organized grouping of assets.
10132    /// Assets can belong to multiple categories, and categories can contain multiple assets.
10133    /// This helps with asset management and permission organization.
10134    ///
10135    /// # Arguments
10136    /// * `category_id` - The ID of the category to add the asset to
10137    /// * `asset_uuid` - The UUID of the asset to add to the category
10138    ///
10139    /// # Returns
10140    /// Returns a `CategoryResponse` containing the updated category information including
10141    /// the newly added asset.
10142    ///
10143    /// # Errors
10144    /// Returns an error if:
10145    /// - Authentication fails or insufficient permissions
10146    /// - The category ID is invalid or does not exist
10147    /// - The asset UUID is invalid or does not exist
10148    /// - The asset is already associated with the category
10149    /// - The HTTP request fails
10150    /// - The server returns an error status
10151    /// - The response cannot be parsed
10152    ///
10153    /// # Examples
10154    /// ```no_run
10155    /// # use amp_rs::ApiClient;
10156    /// # #[tokio::main]
10157    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
10158    /// let client = ApiClient::new().await?;
10159    ///
10160    /// let category_id = 1;
10161    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
10162    ///
10163    /// let updated_category = client.add_asset_to_category(category_id, asset_uuid).await?;
10164    /// println!("Added asset {} to category '{}'", asset_uuid, updated_category.name);
10165    /// println!("Category now has {} assets", updated_category.assets.len());
10166    /// # Ok(())
10167    /// # }
10168    /// ```
10169    ///
10170    /// # Related Methods
10171    /// - [`remove_asset_from_category`](Self::remove_asset_from_category) - Remove asset from category
10172    /// - [`get_category`](Self::get_category) - Get category information including assets
10173    /// - [`get_asset`](Self::get_asset) - Get asset information
10174    pub async fn add_asset_to_category(
10175        &self,
10176        category_id: i64,
10177        asset_uuid: &str,
10178    ) -> Result<CategoryResponse, Error> {
10179        self.request_json(
10180            Method::PUT,
10181            &[
10182                "categories",
10183                &category_id.to_string(),
10184                "assets",
10185                asset_uuid,
10186                "add",
10187            ],
10188            None::<&()>,
10189        )
10190        .await
10191    }
10192
10193    /// Removes an asset from a category.
10194    ///
10195    /// This method disassociates an asset from a category. The asset remains in the system
10196    /// but is no longer part of the specified category. This does not affect the asset's
10197    /// association with other categories.
10198    ///
10199    /// # Arguments
10200    /// * `category_id` - The ID of the category to remove the asset from
10201    /// * `asset_uuid` - The UUID of the asset to remove from the category
10202    ///
10203    /// # Returns
10204    /// Returns a `CategoryResponse` containing the updated category information without
10205    /// the removed asset.
10206    ///
10207    /// # Errors
10208    /// Returns an error if:
10209    /// - Authentication fails or insufficient permissions
10210    /// - The category ID is invalid or does not exist
10211    /// - The asset UUID is invalid or does not exist
10212    /// - The asset is not currently associated with the category
10213    /// - The HTTP request fails
10214    /// - The server returns an error status
10215    /// - The response cannot be parsed
10216    ///
10217    /// # Examples
10218    /// ```no_run
10219    /// # use amp_rs::ApiClient;
10220    /// # #[tokio::main]
10221    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
10222    /// let client = ApiClient::new().await?;
10223    ///
10224    /// let category_id = 1;
10225    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
10226    ///
10227    /// let updated_category = client.remove_asset_from_category(category_id, asset_uuid).await?;
10228    /// println!("Removed asset {} from category '{}'", asset_uuid, updated_category.name);
10229    /// println!("Category now has {} assets", updated_category.assets.len());
10230    /// # Ok(())
10231    /// # }
10232    /// ```
10233    ///
10234    /// # Related Methods
10235    /// - [`add_asset_to_category`](Self::add_asset_to_category) - Add asset to category
10236    /// - [`get_category`](Self::get_category) - Get category information including assets
10237    /// - [`get_asset`](Self::get_asset) - Get asset information
10238    pub async fn remove_asset_from_category(
10239        &self,
10240        category_id: i64,
10241        asset_uuid: &str,
10242    ) -> Result<CategoryResponse, Error> {
10243        self.request_json(
10244            Method::PUT,
10245            &[
10246                "categories",
10247                &category_id.to_string(),
10248                "assets",
10249                asset_uuid,
10250                "remove",
10251            ],
10252            None::<&()>,
10253        )
10254        .await
10255    }
10256
10257    /// Validates a GAID (Green Address ID).
10258    ///
10259    /// # Arguments
10260    /// * `gaid` - The GAID string to validate
10261    ///
10262    /// # Returns
10263    /// Returns a `ValidateGaidResponse` indicating whether the GAID is valid
10264    ///
10265    /// # Errors
10266    /// Returns an error if:
10267    /// - Authentication fails
10268    /// - The HTTP request fails
10269    /// - The response cannot be parsed
10270    ///
10271    /// # Examples
10272    /// ```no_run
10273    /// # use amp_rs::ApiClient;
10274    /// # #[tokio::main]
10275    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
10276    /// let client = ApiClient::new().await?;
10277    ///
10278    /// let gaid = "GAbYScu6jkWUND2jo3L4KJxyvo55d";
10279    /// let validation = client.validate_gaid(gaid).await?;
10280    ///
10281    /// if validation.is_valid {
10282    ///     println!("GAID {} is valid", gaid);
10283    /// } else {
10284    ///     println!("GAID {} is invalid: {:?}", gaid, validation.error);
10285    /// }
10286    /// # Ok(())
10287    /// # }
10288    /// ```
10289    pub async fn validate_gaid(
10290        &self,
10291        gaid: &str,
10292    ) -> Result<crate::model::ValidateGaidResponse, Error> {
10293        self.request_json(Method::GET, &["gaids", gaid, "validate"], None::<&()>)
10294            .await
10295    }
10296
10297    /// Gets the address associated with a GAID.
10298    ///
10299    /// # Arguments
10300    /// * `gaid` - The GAID to get the address for
10301    ///
10302    /// # Returns
10303    /// Returns an `AddressGaidResponse` containing the address
10304    ///
10305    /// # Errors
10306    /// Returns an error if:
10307    /// - The GAID is invalid
10308    /// - Authentication fails
10309    /// - The HTTP request fails
10310    /// - The response cannot be parsed
10311    ///
10312    /// # Examples
10313    /// ```no_run
10314    /// # use amp_rs::ApiClient;
10315    /// # #[tokio::main]
10316    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
10317    /// let client = ApiClient::new().await?;
10318    ///
10319    /// let gaid = "GAbYScu6jkWUND2jo3L4KJxyvo55d";
10320    /// let address_response = client.get_gaid_address(gaid).await?;
10321    ///
10322    /// println!("Address for GAID {}: {}", gaid, address_response.address);
10323    /// # Ok(())
10324    /// # }
10325    /// ```
10326    pub async fn get_gaid_address(
10327        &self,
10328        gaid: &str,
10329    ) -> Result<crate::model::AddressGaidResponse, Error> {
10330        self.request_json(Method::GET, &["gaids", gaid, "address"], None::<&()>)
10331            .await
10332    }
10333
10334    /// Gets a list of all managers.
10335    ///
10336    /// # Returns
10337    /// Returns a vector of `Manager` objects
10338    ///
10339    /// # Errors
10340    /// Returns an error if:
10341    /// - Authentication fails
10342    /// - The HTTP request fails
10343    /// - The response cannot be parsed
10344    ///
10345    /// # Examples
10346    /// ```no_run
10347    /// # use amp_rs::ApiClient;
10348    /// # #[tokio::main]
10349    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
10350    /// let client = ApiClient::new().await?;
10351    ///
10352    /// let managers = client.get_managers().await?;
10353    /// for manager in managers {
10354    ///     println!("Manager: {} (ID: {})", manager.username, manager.id);
10355    /// }
10356    /// # Ok(())
10357    /// # }
10358    /// ```
10359    pub async fn get_managers(&self) -> Result<Vec<crate::model::Manager>, Error> {
10360        self.request_json(Method::GET, &["managers"], None::<&()>)
10361            .await
10362    }
10363
10364    /// Creates a new manager.
10365    ///
10366    /// # Arguments
10367    /// * `new_manager` - The manager creation request containing username and password
10368    ///
10369    /// # Returns
10370    /// Returns the created `Manager` object
10371    ///
10372    /// # Errors
10373    /// Returns an error if:
10374    /// - Authentication fails
10375    /// - The HTTP request fails
10376    /// - The manager creation request is invalid
10377    /// - The response cannot be parsed
10378    ///
10379    /// # Examples
10380    /// ```no_run
10381    /// # use amp_rs::{ApiClient, model::ManagerCreate};
10382    /// # #[tokio::main]
10383    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
10384    /// let client = ApiClient::new().await?;
10385    ///
10386    /// let new_manager = ManagerCreate {
10387    ///     username: "new_manager".to_string(),
10388    ///     password: "secure_password".to_string(),
10389    /// };
10390    ///
10391    /// let manager = client.create_manager(&new_manager).await?;
10392    /// println!("Created manager: {} (ID: {})", manager.username, manager.id);
10393    /// # Ok(())
10394    /// # }
10395    /// ```
10396    pub async fn create_manager(
10397        &self,
10398        new_manager: &crate::model::ManagerCreate,
10399    ) -> Result<crate::model::Manager, Error> {
10400        self.request_json(Method::POST, &["managers", "create"], Some(new_manager))
10401            .await
10402    }
10403
10404    /// Gets all assignments for a specific asset.
10405    ///
10406    /// # Arguments
10407    /// * `asset_uuid` - The UUID of the asset to get assignments for
10408    ///
10409    /// # Returns
10410    /// Returns a vector of `Assignment` objects
10411    ///
10412    /// # Errors
10413    /// Returns an error if:
10414    /// - Authentication fails
10415    /// - The HTTP request fails
10416    /// - The asset UUID is invalid
10417    /// - The response cannot be parsed
10418    ///
10419    /// # Examples
10420    /// ```no_run
10421    /// # use amp_rs::ApiClient;
10422    /// # #[tokio::main]
10423    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
10424    /// let client = ApiClient::new().await?;
10425    ///
10426    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
10427    /// let assignments = client.get_asset_assignments(asset_uuid).await?;
10428    ///
10429    /// for assignment in assignments {
10430    ///     println!("Assignment ID: {}, Amount: {}", assignment.id, assignment.amount);
10431    /// }
10432    /// # Ok(())
10433    /// # }
10434    /// ```
10435    pub async fn get_asset_assignments(&self, asset_uuid: &str) -> Result<Vec<Assignment>, Error> {
10436        self.request_json(
10437            Method::GET,
10438            &["assets", asset_uuid, "assignments"],
10439            None::<&()>,
10440        )
10441        .await
10442    }
10443
10444    /// Creates multiple asset assignments in batch.
10445    ///
10446    /// This method creates multiple asset assignments for the specified asset. Each assignment
10447    /// allocates a specific amount of the asset to a registered user. The assignments are
10448    /// created individually due to API limitations, but this method handles the batch processing
10449    /// automatically.
10450    ///
10451    /// # Arguments
10452    /// * `asset_uuid` - The UUID of the asset to create assignments for
10453    /// * `requests` - A slice of `CreateAssetAssignmentRequest` structs containing assignment details
10454    ///
10455    /// # Returns
10456    /// Returns a vector of `Assignment` structs representing the created assignments with their
10457    /// assigned IDs and status information.
10458    ///
10459    /// # Errors
10460    /// Returns an error if:
10461    /// - Authentication fails or insufficient permissions
10462    /// - The asset UUID is invalid or does not exist
10463    /// - Any assignment request contains invalid data (e.g., invalid user ID, negative amount)
10464    /// - Insufficient asset balance for the total requested assignments
10465    /// - Any individual assignment creation fails
10466    /// - The HTTP request fails
10467    /// - The server returns an error status
10468    /// - The response cannot be parsed
10469    ///
10470    /// # Examples
10471    /// ```no_run
10472    /// # use amp_rs::{ApiClient, model::CreateAssetAssignmentRequest};
10473    /// # #[tokio::main]
10474    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
10475    /// let client = ApiClient::new().await?;
10476    ///
10477    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
10478    /// let requests = vec![
10479    ///     CreateAssetAssignmentRequest {
10480    ///         registered_user: 123,
10481    ///         amount: 1000,
10482    ///         vesting_timestamp: None,
10483    ///         ready_for_distribution: false,
10484    ///     },
10485    ///     CreateAssetAssignmentRequest {
10486    ///         registered_user: 456,
10487    ///         amount: 500,
10488    ///         vesting_timestamp: None,
10489    ///         ready_for_distribution: true,
10490    ///     },
10491    /// ];
10492    ///
10493    /// let assignments = client.create_asset_assignments(asset_uuid, &requests).await?;
10494    /// println!("Created {} assignments", assignments.len());
10495    /// for assignment in assignments {
10496    ///     println!("Assignment {}: {} units to user {}",
10497    ///              assignment.id, assignment.amount, assignment.registered_user);
10498    /// }
10499    /// # Ok(())
10500    /// # }
10501    /// ```
10502    ///
10503    /// # Related Methods
10504    /// - [`get_asset_assignments`](Self::get_asset_assignments) - List all assignments for an asset
10505    /// - [`delete_asset_assignment`](Self::delete_asset_assignment) - Remove an assignment
10506    /// - [`edit_asset_assignment`](Self::edit_asset_assignment) - Update assignment details
10507    /// - [`set_assignment_ready_for_distribution`](Self::set_assignment_ready_for_distribution) - Mark for distribution
10508    pub async fn create_asset_assignments(
10509        &self,
10510        asset_uuid: &str,
10511        requests: &[CreateAssetAssignmentRequest],
10512    ) -> Result<Vec<Assignment>, Error> {
10513        use crate::model::CreateAssetAssignmentRequestWrapper;
10514
10515        // The API only supports maximum length 1 per request, so we need to break
10516        // multiple assignments into separate CreateAssetAssignmentRequestWrapper instances
10517        let mut all_assignments = Vec::new();
10518
10519        for request in requests {
10520            let wrapper = CreateAssetAssignmentRequestWrapper {
10521                assignments: vec![request.clone()],
10522            };
10523
10524            let assignments: Vec<Assignment> = self
10525                .request_json(
10526                    Method::POST,
10527                    &["assets", asset_uuid, "assignments", "create"],
10528                    Some(&wrapper),
10529                )
10530                .await?;
10531
10532            all_assignments.extend(assignments);
10533        }
10534
10535        Ok(all_assignments)
10536    }
10537
10538    /// Gets a specific asset assignment by asset UUID and assignment ID.
10539    ///
10540    /// This method sends a GET request to retrieve detailed information about a specific asset
10541    /// assignment. Asset assignments represent the allocation of assets to users or entities,
10542    /// including information such as the assigned amount, recipient details, and assignment status.
10543    ///
10544    /// # Arguments
10545    /// * `asset_uuid` - The UUID of the asset for which to retrieve the assignment
10546    /// * `assignment_id` - The ID of the specific assignment to retrieve
10547    ///
10548    /// # Returns
10549    /// Returns an `Assignment` struct containing the assignment details including:
10550    /// - Assignment ID and amount
10551    /// - Recipient information
10552    /// - Assignment status and metadata
10553    /// - Creation and modification timestamps
10554    ///
10555    /// # Errors
10556    /// Returns an error if:
10557    /// - Authentication fails
10558    /// - The HTTP request fails
10559    /// - The server returns an error status
10560    /// - The asset UUID is invalid or does not exist
10561    /// - The assignment ID is invalid or does not exist
10562    /// - The assignment is not accessible to the current user
10563    /// - The response cannot be parsed as a valid Assignment
10564    ///
10565    /// # Example
10566    /// ```no_run
10567    /// # use amp_rs::ApiClient;
10568    /// # #[tokio::main]
10569    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
10570    /// let client = ApiClient::new().await?;
10571    ///
10572    /// // Retrieve assignment with ID "123" for asset "550e8400-e29b-41d4-a716-446655440000"
10573    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
10574    /// let assignment_id = "123";
10575    ///
10576    /// let assignment = client.get_asset_assignment(asset_uuid, assignment_id).await?;
10577    ///
10578    /// println!("Assignment ID: {}", assignment.id);
10579    /// println!("Assigned amount: {}", assignment.amount);
10580    /// println!("Registered user: {}", assignment.registered_user);
10581    /// # Ok(())
10582    /// # }
10583    /// ```
10584    pub async fn get_asset_assignment(
10585        &self,
10586        asset_uuid: &str,
10587        assignment_id: &str,
10588    ) -> Result<Assignment, Error> {
10589        self.request_json(
10590            Method::GET,
10591            &["assets", asset_uuid, "assignments", assignment_id],
10592            None::<&()>,
10593        )
10594        .await
10595    }
10596
10597    /// Creates a distribution for an asset with the specified assignments.
10598    ///
10599    /// This method initiates the distribution creation process by sending assignment details
10600    /// to the AMP API. The API will return a distribution UUID and address mappings that
10601    /// can be used for subsequent transaction creation and confirmation steps.
10602    ///
10603    /// # Arguments
10604    /// * `asset_uuid` - The UUID of the asset to distribute
10605    /// * `assignments` - A vector of `AssetDistributionAssignment` structs containing user IDs, addresses, and amounts
10606    ///
10607    /// # Returns
10608    /// Returns a `DistributionResponse` containing:
10609    /// - `distribution_uuid` - Unique identifier for the created distribution
10610    /// - `map_address_amount` - Mapping of addresses to amounts to be distributed
10611    /// - `map_address_asset` - Mapping of addresses to asset IDs
10612    /// - `asset_id` - The asset ID for the distribution
10613    ///
10614    /// # Errors
10615    /// Returns an `AmpError` if:
10616    /// - Authentication fails or insufficient permissions
10617    /// - The asset UUID is invalid or does not exist
10618    /// - Assignment data is invalid (e.g., invalid user IDs, negative amounts, invalid addresses)
10619    /// - Insufficient asset balance for the requested distribution
10620    /// - The HTTP request fails
10621    /// - The server returns an error status
10622    /// - The response cannot be parsed
10623    ///
10624    /// # Examples
10625    /// ```no_run
10626    /// # use amp_rs::{ApiClient, model::AssetDistributionAssignment, AmpError};
10627    /// # #[tokio::main]
10628    /// # async fn main() -> Result<(), AmpError> {
10629    /// let client = ApiClient::new().await.map_err(AmpError::from)?;
10630    ///
10631    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
10632    /// let assignments = vec![
10633    ///     AssetDistributionAssignment {
10634    ///         user_id: "user123".to_string(),
10635    ///         address: "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
10636    ///         amount: 100.0,
10637    ///     },
10638    ///     AssetDistributionAssignment {
10639    ///         user_id: "user456".to_string(),
10640    ///         address: "lq1qq3xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
10641    ///         amount: 50.0,
10642    ///     },
10643    /// ];
10644    ///
10645    /// let distribution_response = client.create_distribution(asset_uuid, assignments).await?;
10646    /// println!("Created distribution: {}", distribution_response.distribution_uuid);
10647    /// println!("Asset ID: {}", distribution_response.asset_id);
10648    /// # Ok(())
10649    /// # }
10650    /// ```
10651    ///
10652    /// # Related Methods
10653    /// - [`get_asset_assignments`](Self::get_asset_assignments) - List assignments for an asset
10654    /// - [`create_asset_assignments`](Self::create_asset_assignments) - Create new assignments
10655    #[allow(clippy::cognitive_complexity, clippy::too_many_lines)]
10656    pub async fn create_distribution(
10657        &self,
10658        asset_uuid: &str,
10659        assignments: Vec<crate::model::AssetDistributionAssignment>,
10660    ) -> Result<crate::model::DistributionResponse, AmpError> {
10661        use crate::model::{CreateDistributionRequest, DistributionAssignmentRequest};
10662
10663        let create_span = tracing::debug_span!(
10664            "create_distribution",
10665            asset_uuid = %asset_uuid,
10666            assignment_count = assignments.len()
10667        );
10668        let _enter = create_span.enter();
10669
10670        tracing::debug!(
10671            "Creating distribution for asset {} with {} assignments",
10672            asset_uuid,
10673            assignments.len()
10674        );
10675
10676        // Validate inputs
10677        if asset_uuid.is_empty() {
10678            tracing::error!("Distribution creation failed: empty asset UUID");
10679            return Err(AmpError::validation("Asset UUID cannot be empty"));
10680        }
10681
10682        if assignments.is_empty() {
10683            tracing::error!("Distribution creation failed: empty assignments");
10684            return Err(AmpError::validation("Assignments cannot be empty"));
10685        }
10686
10687        // Convert AssetDistributionAssignment to DistributionAssignmentRequest
10688        // The API expects user_uuid field, but our input uses user_id
10689        tracing::trace!("Converting {} assignments to API format", assignments.len());
10690        let mut total_amount = 0.0;
10691        let api_assignments: Vec<DistributionAssignmentRequest> = assignments
10692            .into_iter()
10693            .enumerate()
10694            .map(
10695                #[allow(clippy::cognitive_complexity)]
10696                |(index, assignment)| {
10697                    tracing::trace!(
10698                        "Converting assignment {}: user_id={}, address={}, amount={}",
10699                        index,
10700                        assignment.user_id,
10701                        assignment.address,
10702                        assignment.amount
10703                    );
10704
10705                    // Validate assignment data
10706                    if assignment.user_id.is_empty() {
10707                        tracing::error!("Assignment {} has empty user_id", index);
10708                        return Err(AmpError::validation(format!(
10709                            "Assignment {index} has empty user_id"
10710                        )));
10711                    }
10712                    if assignment.address.is_empty() {
10713                        tracing::error!("Assignment {} has empty address", index);
10714                        return Err(AmpError::validation(format!(
10715                            "Assignment {index} has empty address"
10716                        )));
10717                    }
10718                    if assignment.amount <= 0.0 {
10719                        tracing::error!(
10720                            "Assignment {} has non-positive amount: {}",
10721                            index,
10722                            assignment.amount
10723                        );
10724                        return Err(AmpError::validation(format!(
10725                            "Assignment {} has non-positive amount: {}",
10726                            index, assignment.amount
10727                        )));
10728                    }
10729
10730                    total_amount += assignment.amount;
10731
10732                    Ok(DistributionAssignmentRequest {
10733                        user_uuid: assignment.user_id, // Map user_id to user_uuid for API
10734                        amount: assignment.amount,
10735                        address: assignment.address,
10736                    })
10737                },
10738            )
10739            .collect::<Result<Vec<_>, AmpError>>()?;
10740
10741        tracing::debug!(
10742            "Converted {} assignments successfully, total amount: {}",
10743            api_assignments.len(),
10744            total_amount
10745        );
10746
10747        let request = CreateDistributionRequest {
10748            assignments: api_assignments,
10749        };
10750
10751        tracing::debug!("Sending distribution creation request to AMP API");
10752        let api_call_start = std::time::Instant::now();
10753
10754        // Make the API call
10755        let response: crate::model::DistributionResponse = self
10756            .request_json(
10757                Method::GET,
10758                &["assets", asset_uuid, "distributions", "create"],
10759                Some(&request),
10760            )
10761            .await
10762            .map_err(
10763                #[allow(clippy::cognitive_complexity)]
10764                |e| {
10765                    let api_call_duration = api_call_start.elapsed();
10766                    let error_msg =
10767                        format!("Failed to create distribution after {api_call_duration:?}: {e}");
10768                    tracing::error!("{}", error_msg);
10769
10770                    // Check for specific API error patterns
10771                    let error_str = e.to_string();
10772                    if error_str.contains("404") || error_str.contains("not found") {
10773                        tracing::error!(
10774                            "Asset {} not found - verify asset UUID is correct",
10775                            asset_uuid
10776                        );
10777                    } else if error_str.contains("400") || error_str.contains("bad request") {
10778                        tracing::error!("Bad request - check assignment data format and values");
10779                    } else if error_str.contains("401") || error_str.contains("unauthorized") {
10780                        tracing::error!("Unauthorized - check API credentials and token validity");
10781                    } else if error_str.contains("403") || error_str.contains("forbidden") {
10782                        tracing::error!("Forbidden - check permissions for asset distribution");
10783                    } else if error_str.contains("429") || error_str.contains("rate limit") {
10784                        tracing::error!("Rate limited - wait before retrying");
10785                    } else if error_str.contains("500") || error_str.contains("internal server") {
10786                        tracing::error!(
10787                            "Server error - this may be a temporary issue, retry may help"
10788                        );
10789                    }
10790
10791                    AmpError::api(error_msg)
10792                },
10793            )?;
10794
10795        let api_call_duration = api_call_start.elapsed();
10796        tracing::info!(
10797            "Successfully created distribution: {} (took {:?})",
10798            response.distribution_uuid,
10799            api_call_duration
10800        );
10801
10802        // Validate response data
10803        if response.distribution_uuid.is_empty() {
10804            tracing::error!("API returned empty distribution UUID");
10805            return Err(AmpError::api("API returned empty distribution UUID"));
10806        }
10807
10808        if response.asset_id.is_empty() {
10809            tracing::error!("API returned empty asset ID");
10810            return Err(AmpError::api("API returned empty asset ID"));
10811        }
10812
10813        if response.map_address_amount.is_empty() {
10814            tracing::error!("API returned empty address mapping");
10815            return Err(AmpError::api("API returned empty address mapping"));
10816        }
10817
10818        tracing::debug!(
10819            "Distribution response validated - {} addresses mapped, asset_id: {}",
10820            response.map_address_amount.len(),
10821            response.asset_id
10822        );
10823
10824        Ok(response)
10825    }
10826
10827    /// Confirms a distribution with transaction and change data.
10828    ///
10829    /// This method submits the final confirmation for a distribution by providing
10830    /// the transaction details and any change UTXOs to the AMP API. This completes
10831    /// the distribution workflow after the transaction has been broadcast and confirmed
10832    /// on the blockchain.
10833    ///
10834    /// # Arguments
10835    /// * `asset_uuid` - The UUID of the asset being distributed
10836    /// * `distribution_uuid` - The UUID of the distribution to confirm (from `create_distribution` response)
10837    /// * `tx_data` - Transaction data containing details and txid from the blockchain
10838    /// * `change_data` - Vector of change UTXOs from the transaction
10839    ///
10840    /// # Errors
10841    /// Returns an error if:
10842    /// - Authentication fails
10843    /// - The asset UUID or distribution UUID is invalid
10844    /// - The transaction data is invalid or incomplete
10845    /// - The HTTP request fails
10846    /// - The server returns an error status
10847    /// - The response cannot be parsed
10848    ///
10849    /// # Examples
10850    /// ```no_run
10851    /// # use amp_rs::{ApiClient, model::{AmpTxData, Unspent}, AmpError};
10852    /// # #[tokio::main]
10853    /// # async fn main() -> Result<(), AmpError> {
10854    /// # let client = ApiClient::new().await?;
10855    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
10856    /// let distribution_uuid = "dist-550e8400-e29b-41d4-a716-446655440000";
10857    ///
10858    /// // Transaction data for AMP API confirmation
10859    /// let tx_data = AmpTxData {
10860    ///     details: serde_json::json!([{
10861    ///         "account": "",
10862    ///         "address": "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq",
10863    ///         "category": "send",
10864    ///         "amount": -100.0,
10865    ///         "vout": 0,
10866    ///         "fee": -0.001
10867    ///     }]),
10868    ///     txid: "abc123def456...".to_string(),
10869    /// };
10870    ///
10871    /// // Change UTXOs from Elements node listunspent call
10872    /// let change_data = vec![
10873    ///     Unspent {
10874    ///         txid: "abc123def456...".to_string(),
10875    ///         vout: 1,
10876    ///         amount: 25.0,
10877    ///         asset: "asset_id_hex".to_string(),
10878    ///         address: "change_address".to_string(),
10879    ///         spendable: true,
10880    ///         confirmations: Some(2),
10881    ///         scriptpubkey: Some("76a914...88ac".to_string()),
10882    ///         redeemscript: None,
10883    ///         witnessscript: None,
10884    ///         amountblinder: None,
10885    ///         assetblinder: None,
10886    ///     }
10887    /// ];
10888    ///
10889    /// client.confirm_distribution(asset_uuid, distribution_uuid, tx_data, change_data).await?;
10890    /// println!("Distribution confirmed successfully");
10891    /// # Ok(())
10892    /// # }
10893    /// ```
10894    ///
10895    /// # Related Methods
10896    /// - [`create_distribution`](Self::create_distribution) - Create a new distribution
10897    /// - [`get_asset_assignments`](Self::get_asset_assignments) - List assignments for an asset
10898    #[allow(clippy::cognitive_complexity, clippy::too_many_lines)]
10899    pub async fn confirm_distribution(
10900        &self,
10901        asset_uuid: &str,
10902        distribution_uuid: &str,
10903        tx_data: crate::model::AmpTxData,
10904        change_data: Vec<crate::model::Unspent>,
10905    ) -> Result<(), AmpError> {
10906        use crate::model::ConfirmDistributionRequest;
10907
10908        let confirm_span = tracing::debug_span!(
10909            "confirm_distribution",
10910            asset_uuid = %asset_uuid,
10911            distribution_uuid = %distribution_uuid,
10912            txid = %tx_data.txid,
10913            change_count = change_data.len()
10914        );
10915        let _enter = confirm_span.enter();
10916
10917        tracing::debug!(
10918            "Confirming distribution {} for asset {} with txid {} ({} change UTXOs)",
10919            distribution_uuid,
10920            asset_uuid,
10921            tx_data.txid,
10922            change_data.len()
10923        );
10924
10925        // Validate inputs
10926        if asset_uuid.is_empty() {
10927            tracing::error!("Distribution confirmation failed: empty asset UUID");
10928            return Err(AmpError::validation("Asset UUID cannot be empty"));
10929        }
10930
10931        if distribution_uuid.is_empty() {
10932            tracing::error!("Distribution confirmation failed: empty distribution UUID");
10933            return Err(AmpError::validation("Distribution UUID cannot be empty"));
10934        }
10935
10936        if tx_data.txid.is_empty() {
10937            tracing::error!("Distribution confirmation failed: empty transaction ID");
10938            return Err(AmpError::validation("Transaction ID cannot be empty"));
10939        }
10940
10941        // Log transaction details for debugging
10942        tracing::debug!("Transaction details array: {:?}", tx_data.details);
10943
10944        // Log change data details
10945        if change_data.is_empty() {
10946            tracing::debug!("No change UTXOs to include in confirmation");
10947        } else {
10948            let total_change: f64 = change_data.iter().map(|utxo| utxo.amount).sum();
10949            tracing::debug!(
10950                "Change data - {} UTXOs, total amount: {}",
10951                change_data.len(),
10952                total_change
10953            );
10954
10955            for (i, utxo) in change_data.iter().enumerate() {
10956                tracing::trace!(
10957                    "Change UTXO {}: txid={}, vout={}, amount={}, spendable={}",
10958                    i,
10959                    utxo.txid,
10960                    utxo.vout,
10961                    utxo.amount,
10962                    utxo.spendable
10963                );
10964            }
10965        }
10966
10967        let request = ConfirmDistributionRequest {
10968            tx_data: tx_data.clone(),
10969            change_data: change_data.clone(),
10970        };
10971
10972        tracing::debug!("Sending distribution confirmation request to AMP API");
10973        let api_call_start = std::time::Instant::now();
10974
10975        // Make the API call
10976        self.request_empty(
10977            Method::POST,
10978            &["assets", asset_uuid, "distributions", distribution_uuid, "confirm"],
10979            Some(&request),
10980        )
10981        .await
10982        .map_err(#[allow(clippy::cognitive_complexity)] |e| {
10983            let api_call_duration = api_call_start.elapsed();
10984            let error_msg = format!(
10985                "Failed to confirm distribution {} after {:?}: {}. IMPORTANT: Transaction {} was successful on blockchain. Use this txid to manually retry confirmation.",
10986                distribution_uuid, api_call_duration, e, tx_data.txid
10987            );
10988            tracing::error!("{}", error_msg);
10989
10990            // Check for specific API error patterns
10991            let error_str = e.to_string();
10992            if error_str.contains("404") || error_str.contains("not found") {
10993                tracing::error!("Distribution {} not found - verify distribution UUID is correct", distribution_uuid);
10994            } else if error_str.contains("400") || error_str.contains("bad request") {
10995                tracing::error!("Bad request - check transaction data format and change data");
10996            } else if error_str.contains("409") || error_str.contains("conflict") {
10997                tracing::error!("Conflict - distribution may already be confirmed");
10998            } else if error_str.contains("422") || error_str.contains("unprocessable") {
10999                tracing::error!("Unprocessable entity - check transaction confirmations and data validity");
11000            } else if error_str.contains("500") || error_str.contains("internal server") {
11001                tracing::error!("Server error - this may be a temporary issue, retry with txid: {}", tx_data.txid);
11002            }
11003
11004            AmpError::api(error_msg)
11005        })?;
11006
11007        let api_call_duration = api_call_start.elapsed();
11008        tracing::info!(
11009            "Successfully confirmed distribution: {} for asset: {} with txid: {} (took {:?})",
11010            distribution_uuid,
11011            asset_uuid,
11012            tx_data.txid,
11013            api_call_duration
11014        );
11015
11016        Ok(())
11017    }
11018
11019    /// Cancels an in-progress distribution for an asset.
11020    ///
11021    /// This method cancels a distribution that is currently in progress (unconfirmed status).
11022    /// Once a distribution is cancelled, it cannot be confirmed and the assigned amounts
11023    /// become available for new distributions.
11024    ///
11025    /// # Arguments
11026    /// * `asset_uuid` - The UUID of the asset
11027    /// * `distribution_uuid` - The UUID of the distribution to cancel
11028    ///
11029    /// # Returns
11030    /// Returns `Ok(())` if the distribution was successfully cancelled.
11031    ///
11032    /// # Errors
11033    /// Returns an error if:
11034    /// - Authentication fails
11035    /// - The HTTP request fails
11036    /// - The server returns an error status
11037    /// - The distribution is not found
11038    /// - The distribution is already confirmed and cannot be cancelled
11039    ///
11040    /// # Examples
11041    /// ```no_run
11042    /// use amp_rs::ApiClient;
11043    ///
11044    /// #[tokio::main]
11045    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
11046    ///     let client = ApiClient::new().await?;
11047    ///     
11048    ///     client.cancel_distribution(
11049    ///         "asset-uuid-123",
11050    ///         "distribution-uuid-456"
11051    ///     ).await?;
11052    ///     
11053    ///     println!("Distribution cancelled successfully");
11054    ///     Ok(())
11055    /// # }
11056    /// ```
11057    #[allow(clippy::cognitive_complexity)]
11058    pub async fn cancel_distribution(
11059        &self,
11060        asset_uuid: &str,
11061        distribution_uuid: &str,
11062    ) -> Result<(), AmpError> {
11063        let cancel_span = tracing::debug_span!(
11064            "cancel_distribution",
11065            asset_uuid = %asset_uuid,
11066            distribution_uuid = %distribution_uuid
11067        );
11068        let _enter = cancel_span.enter();
11069
11070        tracing::debug!(
11071            "Cancelling distribution {} for asset {}",
11072            distribution_uuid,
11073            asset_uuid
11074        );
11075
11076        // Validate inputs
11077        if asset_uuid.is_empty() {
11078            tracing::error!("Distribution cancellation failed: empty asset UUID");
11079            return Err(AmpError::validation("Asset UUID cannot be empty"));
11080        }
11081
11082        if distribution_uuid.is_empty() {
11083            tracing::error!("Distribution cancellation failed: empty distribution UUID");
11084            return Err(AmpError::validation("Distribution UUID cannot be empty"));
11085        }
11086
11087        let api_call_start = std::time::Instant::now();
11088
11089        self.request_empty(
11090            Method::DELETE,
11091            &[
11092                "assets",
11093                asset_uuid,
11094                "distributions",
11095                distribution_uuid,
11096                "cancel",
11097            ],
11098            None::<&()>,
11099        )
11100        .await
11101        .map_err(|e| {
11102            let api_call_duration = api_call_start.elapsed();
11103            let error_msg = format!(
11104                "Failed to cancel distribution {distribution_uuid} for asset {asset_uuid} after {api_call_duration:?}: {e}"
11105            );
11106            tracing::error!("{}", error_msg);
11107
11108            // Check for specific API error patterns
11109            let error_str = e.to_string();
11110            if error_str.contains("404") || error_str.contains("not found") {
11111                tracing::error!(
11112                    "Distribution {} not found - verify distribution UUID is correct",
11113                    distribution_uuid
11114                );
11115            } else if error_str.contains("400") || error_str.contains("bad request") {
11116                tracing::error!("Bad request - distribution may already be confirmed or invalid");
11117            } else if error_str.contains("409") || error_str.contains("conflict") {
11118                tracing::error!(
11119                    "Conflict - distribution may already be confirmed and cannot be cancelled"
11120                );
11121            } else if error_str.contains("422") || error_str.contains("unprocessable") {
11122                tracing::error!(
11123                    "Unprocessable entity - distribution is in a state that cannot be cancelled"
11124                );
11125            }
11126
11127            AmpError::api(error_msg)
11128        })?;
11129
11130        let api_call_duration = api_call_start.elapsed();
11131        tracing::info!(
11132            "Successfully cancelled distribution: {} for asset: {} (took {:?})",
11133            distribution_uuid,
11134            asset_uuid,
11135            api_call_duration
11136        );
11137
11138        Ok(())
11139    }
11140
11141    /// Gets all distributions for a specific asset.
11142    ///
11143    /// This method retrieves all distributions (both confirmed and unconfirmed) for the specified asset.
11144    /// This is useful for checking if there are any in-progress distributions before deleting an asset.
11145    ///
11146    /// # Arguments
11147    /// * `asset_uuid` - The UUID of the asset to get distributions for
11148    ///
11149    /// # Returns
11150    /// Returns a vector of `Distribution` objects for the asset.
11151    ///
11152    /// # Errors
11153    /// Returns an error if:
11154    /// - Authentication fails
11155    /// - The HTTP request fails
11156    /// - The server returns an error status
11157    /// - The response cannot be parsed
11158    ///
11159    /// # Examples
11160    /// ```no_run
11161    /// use amp_rs::ApiClient;
11162    ///
11163    /// #[tokio::main]
11164    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
11165    ///     let client = ApiClient::new().await?;
11166    ///     
11167    ///     let distributions = client.get_asset_distributions("asset-uuid-123").await?;
11168    ///     
11169    ///     for distribution in distributions {
11170    ///         println!("Distribution: {} - Status: {:?}",
11171    ///                  distribution.distribution_uuid,
11172    ///                  distribution.distribution_status);
11173    ///     }
11174    ///     Ok(())
11175    /// }
11176    /// ```
11177    pub async fn get_asset_distributions(
11178        &self,
11179        asset_uuid: &str,
11180    ) -> Result<Vec<crate::model::Distribution>, Error> {
11181        let distributions_span = tracing::debug_span!(
11182            "get_asset_distributions",
11183            asset_uuid = %asset_uuid
11184        );
11185        let _enter = distributions_span.enter();
11186
11187        tracing::debug!("Getting distributions for asset {}", asset_uuid);
11188
11189        // Validate input
11190        if asset_uuid.is_empty() {
11191            tracing::error!("Get distributions failed: empty asset UUID");
11192            return Err(Error::RequestFailed(
11193                "Asset UUID cannot be empty".to_string(),
11194            ));
11195        }
11196
11197        self.request_json(
11198            Method::GET,
11199            &["assets", asset_uuid, "distributions"],
11200            None::<&()>,
11201        )
11202        .await
11203    }
11204
11205    /// Gets a specific distribution by UUID for an asset.
11206    ///
11207    /// This method retrieves detailed information about a specific distribution,
11208    /// including its status, UUID, and associated transactions.
11209    ///
11210    /// # Arguments
11211    /// * `asset_uuid` - The UUID of the asset
11212    /// * `distribution_uuid` - The UUID of the distribution to retrieve
11213    ///
11214    /// # Returns
11215    /// Returns a `Distribution` struct containing:
11216    /// - `distribution_uuid` - The unique identifier for the distribution
11217    /// - `distribution_status` - Current status of the distribution
11218    /// - `transactions` - List of transactions associated with the distribution
11219    ///
11220    /// # Errors
11221    /// Returns an error if:
11222    /// - Authentication fails
11223    /// - The HTTP request fails
11224    /// - The server returns an error status
11225    /// - The response cannot be parsed as JSON
11226    /// - The asset UUID or distribution UUID is empty
11227    ///
11228    /// # Examples
11229    /// ```no_run
11230    /// # use amp_rs::ApiClient;
11231    /// # #[tokio::main]
11232    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
11233    /// let client = ApiClient::new().await?;
11234    ///
11235    /// let distribution = client.get_asset_distribution(
11236    ///     "asset-uuid-123",
11237    ///     "distribution-uuid-456"
11238    /// ).await?;
11239    ///
11240    /// println!("Distribution: {} - Status: {:?}",
11241    ///          distribution.distribution_uuid,
11242    ///          distribution.distribution_status);
11243    /// # Ok(())
11244    /// # }
11245    /// ```
11246    ///
11247    /// # Related Methods
11248    /// - [`get_asset_distributions`](Self::get_asset_distributions) - List all distributions for an asset
11249    /// - [`create_distribution`](Self::create_distribution) - Create a new distribution
11250    /// - [`confirm_distribution`](Self::confirm_distribution) - Confirm a distribution
11251    /// - [`cancel_distribution`](Self::cancel_distribution) - Cancel a distribution
11252    #[allow(clippy::cognitive_complexity)]
11253    pub async fn get_asset_distribution(
11254        &self,
11255        asset_uuid: &str,
11256        distribution_uuid: &str,
11257    ) -> Result<crate::model::Distribution, Error> {
11258        let distribution_span = tracing::debug_span!(
11259            "get_asset_distribution",
11260            asset_uuid = %asset_uuid,
11261            distribution_uuid = %distribution_uuid
11262        );
11263        let _enter = distribution_span.enter();
11264
11265        tracing::debug!(
11266            "Getting distribution {} for asset {}",
11267            distribution_uuid,
11268            asset_uuid
11269        );
11270
11271        // Validate inputs
11272        if asset_uuid.is_empty() {
11273            tracing::error!("Get distribution failed: empty asset UUID");
11274            return Err(Error::RequestFailed(
11275                "Asset UUID cannot be empty".to_string(),
11276            ));
11277        }
11278
11279        if distribution_uuid.is_empty() {
11280            tracing::error!("Get distribution failed: empty distribution UUID");
11281            return Err(Error::RequestFailed(
11282                "Distribution UUID cannot be empty".to_string(),
11283            ));
11284        }
11285
11286        self.request_json(
11287            Method::GET,
11288            &["assets", asset_uuid, "distributions", distribution_uuid],
11289            None::<&()>,
11290        )
11291        .await
11292    }
11293
11294    /// Requests reissuance data for an asset
11295    ///
11296    /// This method creates a reissuance request with the AMP API and returns
11297    /// the necessary data to execute the reissuance transaction, including
11298    /// asset information, amount, and required UTXOs.
11299    ///
11300    /// # Arguments
11301    /// * `asset_uuid` - The UUID of the asset to reissue
11302    /// * `amount_to_reissue` - The amount to reissue (in satoshis for the asset)
11303    ///
11304    /// # Returns
11305    /// Returns a `ReissueRequestResponse` containing:
11306    /// - `command` - The command type ("reissue")
11307    /// - `min_supported_client_script_version` - Minimum script version required
11308    /// - `base_url` - Base URL for the AMP API
11309    /// - `asset_uuid` - The asset UUID
11310    /// - `asset_id` - The asset ID (hex string)
11311    /// - `amount` - The amount to reissue
11312    /// - `reissuance_utxos` - List of required reissuance token UTXOs
11313    ///
11314    /// # Errors
11315    /// Returns an `AmpError` if:
11316    /// - Authentication fails or insufficient permissions
11317    /// - The asset UUID is invalid or does not exist
11318    /// - The asset is not reissuable
11319    /// - Insufficient reissuance tokens are available
11320    /// - The HTTP request fails
11321    /// - The server returns an error status
11322    /// - The response cannot be parsed
11323    ///
11324    /// # Examples
11325    /// ```no_run
11326    /// # use amp_rs::{ApiClient, AmpError};
11327    /// # #[tokio::main]
11328    /// # async fn main() -> Result<(), AmpError> {
11329    /// let client = ApiClient::new().await.map_err(AmpError::from)?;
11330    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
11331    /// let amount = 1000000; // 0.01 of an asset with 8 decimals
11332    ///
11333    /// let response = client.reissue_request(asset_uuid, amount).await?;
11334    /// println!("Reissuance request created for asset: {}", response.asset_id);
11335    /// println!("Amount to reissue: {}", response.amount);
11336    /// # Ok(())
11337    /// # }
11338    /// ```
11339    ///
11340    /// # Related Methods
11341    /// - [`reissue_confirm`](Self::reissue_confirm) - Confirm a completed reissuance
11342    /// - [`reissue_asset`](Self::reissue_asset) - Complete reissuance workflow
11343    #[allow(clippy::cognitive_complexity)]
11344    pub async fn reissue_request(
11345        &self,
11346        asset_uuid: &str,
11347        amount_to_reissue: i64,
11348    ) -> Result<crate::model::ReissueRequestResponse, AmpError> {
11349        use crate::model::ReissueRequest;
11350
11351        let request_span = tracing::debug_span!("reissue_request", asset_uuid = %asset_uuid);
11352        let _enter = request_span.enter();
11353
11354        tracing::debug!(
11355            "Creating reissuance request for asset {} with amount {}",
11356            asset_uuid,
11357            amount_to_reissue
11358        );
11359
11360        // Validate inputs
11361        if asset_uuid.is_empty() {
11362            tracing::error!("Reissuance request failed: empty asset UUID");
11363            return Err(AmpError::validation("Asset UUID cannot be empty"));
11364        }
11365
11366        if amount_to_reissue <= 0 {
11367            tracing::error!(
11368                "Reissuance request failed: invalid amount {}",
11369                amount_to_reissue
11370            );
11371            return Err(AmpError::validation("Amount to reissue must be positive"));
11372        }
11373
11374        let request = ReissueRequest { amount_to_reissue };
11375
11376        let response: crate::model::ReissueRequestResponse = self
11377            .request_json(
11378                Method::POST,
11379                &["assets", asset_uuid, "reissue-request"],
11380                Some(&request),
11381            )
11382            .await
11383            .map_err(|e| {
11384                tracing::error!("Reissuance request failed: {}", e);
11385                AmpError::api(format!("Failed to create reissuance request: {e}"))
11386                    .with_context("Reissuance request creation")
11387            })?;
11388
11389        tracing::info!(
11390            "Reissuance request created successfully: asset_id={}, amount={}",
11391            response.asset_id,
11392            response.amount
11393        );
11394
11395        Ok(response)
11396    }
11397
11398    /// Confirms a completed reissuance transaction
11399    ///
11400    /// This method confirms a reissuance transaction that has been broadcast
11401    /// to the Elements network. It provides the transaction details and issuance
11402    /// information to the AMP API to register the reissuance.
11403    ///
11404    /// # Arguments
11405    /// * `asset_uuid` - The UUID of the asset that was reissued
11406    /// * `details` - Transaction details from `gettransaction` RPC call (as JSON Value)
11407    /// * `listissuances` - List of issuances from `listissuances` RPC call for this transaction
11408    /// * `reissuance_output` - Reissuance output containing txid and vin (as JSON Value)
11409    ///
11410    /// # Returns
11411    /// Returns a `ReissueResponse` containing:
11412    /// - `txid` - The transaction ID
11413    /// - `vin` - The input index of the reissuance
11414    /// - `reissuance_amount` - The amount that was reissued
11415    ///
11416    /// # Errors
11417    /// Returns an `AmpError` if:
11418    /// - Authentication fails or insufficient permissions
11419    /// - The asset UUID is invalid or does not exist
11420    /// - The transaction data is invalid or incomplete
11421    /// - The reissuance transaction is not valid
11422    /// - The HTTP request fails
11423    /// - The server returns an error status
11424    /// - The response cannot be parsed
11425    ///
11426    /// # Examples
11427    /// ```no_run
11428    /// # use amp_rs::{ApiClient, AmpError, ElementsRpc};
11429    /// # use serde_json::json;
11430    /// # #[tokio::main]
11431    /// # async fn main() -> Result<(), AmpError> {
11432    /// let client = ApiClient::new().await.map_err(AmpError::from)?;
11433    /// let rpc = ElementsRpc::from_env()?;
11434    ///
11435    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
11436    /// let txid = "abc123...";
11437    ///
11438    /// // Get transaction details
11439    /// let tx_detail = rpc.get_transaction(txid).await?;
11440    /// let details = serde_json::to_value(&tx_detail.details).unwrap();
11441    ///
11442    /// // Get issuances for this transaction
11443    /// let issuances = rpc.list_issuances(None).await?;
11444    /// let listissuances: Vec<_> = issuances
11445    ///     .into_iter()
11446    ///     .filter(|i| i.get("txid").and_then(|v| v.as_str()) == Some(txid))
11447    ///     .collect();
11448    ///
11449    /// let reissuance_output = json!({"txid": txid, "vin": 0});
11450    ///
11451    /// let response = client.reissue_confirm(
11452    ///     asset_uuid,
11453    ///     details,
11454    ///     listissuances,
11455    ///     reissuance_output,
11456    /// ).await?;
11457    ///
11458    /// println!("Reissuance confirmed: txid={}, vin={}", response.txid, response.vin);
11459    /// # Ok(())
11460    /// # }
11461    /// ```
11462    ///
11463    /// # Related Methods
11464    /// - [`reissue_request`](Self::reissue_request) - Create a reissuance request
11465    /// - [`reissue_asset`](Self::reissue_asset) - Complete reissuance workflow
11466    #[allow(clippy::cognitive_complexity)]
11467    pub async fn reissue_confirm(
11468        &self,
11469        asset_uuid: &str,
11470        details: serde_json::Value,
11471        listissuances: Vec<serde_json::Value>,
11472        reissuance_output: serde_json::Value,
11473    ) -> Result<crate::model::ReissueResponse, AmpError> {
11474        use crate::model::ReissueConfirmRequest;
11475
11476        let confirm_span = tracing::debug_span!("reissue_confirm", asset_uuid = %asset_uuid);
11477        let _enter = confirm_span.enter();
11478
11479        // Extract txid for logging (clone to avoid borrow checker issue)
11480        let txid = reissuance_output
11481            .get("txid")
11482            .and_then(serde_json::Value::as_str)
11483            .map_or_else(|| "unknown".to_string(), std::string::ToString::to_string);
11484        let vin = reissuance_output
11485            .get("vin")
11486            .and_then(serde_json::Value::as_u64)
11487            .unwrap_or(0);
11488
11489        tracing::debug!(
11490            "Confirming reissuance for asset {} with txid {} vin {} ({} issuances)",
11491            asset_uuid,
11492            txid,
11493            vin,
11494            listissuances.len()
11495        );
11496
11497        // Validate inputs
11498        if asset_uuid.is_empty() {
11499            tracing::error!("Reissuance confirmation failed: empty asset UUID");
11500            return Err(AmpError::validation("Asset UUID cannot be empty"));
11501        }
11502
11503        let request = ReissueConfirmRequest {
11504            details,
11505            listissuances,
11506            reissuance_output,
11507        };
11508
11509        let response: crate::model::ReissueResponse = self
11510            .request_json(
11511                Method::POST,
11512                &["assets", asset_uuid, "reissue-confirm"],
11513                Some(&request),
11514            )
11515            .await
11516            .map_err(|e| {
11517                tracing::error!("Reissuance confirmation failed: {}", e);
11518                AmpError::api(format!(
11519                    "Failed to confirm reissuance for txid {}: {}. \
11520                    IMPORTANT: Transaction {} was successful on blockchain. \
11521                    You may need to retry confirmation with this txid.",
11522                    &txid, e, &txid
11523                ))
11524                .with_context("Reissuance confirmation")
11525            })?;
11526
11527        tracing::info!(
11528            "Reissuance confirmed successfully: txid={}, vin={}, amount={}",
11529            response.txid,
11530            response.vin,
11531            response.reissuance_amount
11532        );
11533
11534        Ok(response)
11535    }
11536
11537    /// Creates a burn request for an asset
11538    ///
11539    /// This method requests the data needed to burn (destroy) a specific amount of an asset.
11540    /// The response contains UTXOs that need to be available in the wallet for the burn operation.
11541    ///
11542    /// # Arguments
11543    /// * `asset_uuid` - The UUID of the asset to burn
11544    /// * `amount` - The amount to burn (in satoshis for the asset)
11545    ///
11546    /// # Returns
11547    /// Returns a `BurnCreate` containing:
11548    /// - Asset information (UUID, asset ID)
11549    /// - Amount to burn
11550    /// - Required UTXOs that must be available in the wallet
11551    ///
11552    /// # Errors
11553    /// Returns an `AmpError` if:
11554    /// - The asset UUID is invalid or empty
11555    /// - The amount is invalid (non-positive)
11556    /// - Authentication fails or insufficient permissions
11557    /// - The asset does not exist
11558    /// - The HTTP request fails
11559    /// - The server returns an error status
11560    ///
11561    /// # Examples
11562    /// ```no_run
11563    /// # use amp_rs::{ApiClient, AmpError};
11564    /// # #[tokio::main]
11565    /// # async fn main() -> Result<(), AmpError> {
11566    /// let client = ApiClient::new().await?;
11567    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
11568    /// let amount = 1000000; // 0.01 of an asset with 8 decimals
11569    ///
11570    /// let response = client.burn_request(asset_uuid, amount).await?;
11571    /// println!("Burn request created for asset: {}", response.asset_id);
11572    /// println!("Amount to burn: {}", response.amount);
11573    /// println!("Required UTXOs: {:?}", response.utxos);
11574    /// # Ok(())
11575    /// # }
11576    /// ```
11577    ///
11578    /// # Related Methods
11579    /// - [`burn_confirm`](Self::burn_confirm) - Confirm a burn transaction
11580    /// - [`burn_asset`](Self::burn_asset) - Complete burn workflow
11581    #[allow(clippy::cognitive_complexity)]
11582    pub async fn burn_request(
11583        &self,
11584        asset_uuid: &str,
11585        amount: i64,
11586    ) -> Result<crate::model::BurnCreate, AmpError> {
11587        use crate::model::BurnRequest;
11588
11589        let request_span = tracing::debug_span!("burn_request", asset_uuid = %asset_uuid);
11590        let _enter = request_span.enter();
11591
11592        tracing::debug!(
11593            "Creating burn request for asset {} with amount {}",
11594            asset_uuid,
11595            amount
11596        );
11597
11598        // Validate inputs
11599        if asset_uuid.is_empty() {
11600            tracing::error!("Burn request failed: empty asset UUID");
11601            return Err(AmpError::validation("Asset UUID cannot be empty"));
11602        }
11603
11604        if amount <= 0 {
11605            tracing::error!("Burn request failed: invalid amount {}", amount);
11606            return Err(AmpError::validation("Amount to burn must be positive"));
11607        }
11608
11609        let request = BurnRequest { amount };
11610
11611        let response: crate::model::BurnCreate = self
11612            .request_json(
11613                Method::POST,
11614                &["assets", asset_uuid, "burn-request"],
11615                Some(&request),
11616            )
11617            .await
11618            .map_err(|e| {
11619                tracing::error!("Burn request failed: {}", e);
11620                AmpError::api(format!("Failed to create burn request: {e}"))
11621                    .with_context("Burn request creation")
11622            })?;
11623
11624        tracing::info!(
11625            "Burn request created successfully: asset_id={}, amount={}",
11626            response.asset_id,
11627            response.amount
11628        );
11629
11630        Ok(response)
11631    }
11632
11633    /// Confirms a completed burn transaction
11634    ///
11635    /// This method confirms a burn transaction that has been broadcast
11636    /// to the Elements network. It provides the transaction details and
11637    /// change data to complete the burn registration with the AMP API.
11638    ///
11639    /// # Arguments
11640    /// * `asset_uuid` - The UUID of the asset that was burned
11641    /// * `tx_data` - Transaction data from `gettransaction` RPC call (as JSON Value, containing at least txid)
11642    /// * `change_data` - Change data from `listunspent` RPC call filtered by `asset_id` and txid (as JSON Values)
11643    ///
11644    /// # Returns
11645    /// Returns `Ok(())` on success (the API returns an empty response with status 200)
11646    ///
11647    /// # Errors
11648    /// Returns an `AmpError` if:
11649    /// - Authentication fails or insufficient permissions
11650    /// - The asset UUID is invalid or does not exist
11651    /// - The transaction data is invalid or incomplete
11652    /// - The burn transaction is not valid
11653    /// - The HTTP request fails
11654    /// - The server returns an error status
11655    ///
11656    /// # Examples
11657    /// ```no_run
11658    /// # use amp_rs::{ApiClient, AmpError};
11659    /// # #[tokio::main]
11660    /// # async fn main() -> Result<(), AmpError> {
11661    /// let client = ApiClient::new().await?;
11662    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
11663    ///
11664    /// let tx_data = serde_json::json!({
11665    ///     "txid": "abc123def456..."
11666    /// });
11667    ///
11668    /// let change_data = vec![serde_json::json!({
11669    ///     "txid": "abc123def456...",
11670    ///     "vout": 0,
11671    ///     "address": "tlq1qq...",
11672    ///     "amount": 100.0,
11673    ///     "asset": "asset_id_here",
11674    ///     "spendable": true
11675    /// })];
11676    ///
11677    /// client.burn_confirm(asset_uuid, tx_data, change_data).await?;
11678    /// println!("Burn confirmed successfully");
11679    /// # Ok(())
11680    /// # }
11681    /// ```
11682    ///
11683    /// # Related Methods
11684    /// - [`burn_request`](Self::burn_request) - Create a burn request
11685    /// - [`burn_asset`](Self::burn_asset) - Complete burn workflow
11686    #[allow(clippy::cognitive_complexity)]
11687    pub async fn burn_confirm(
11688        &self,
11689        asset_uuid: &str,
11690        tx_data: serde_json::Value,
11691        change_data: Vec<serde_json::Value>,
11692    ) -> Result<(), AmpError> {
11693        use crate::model::BurnConfirmRequest;
11694
11695        let confirm_span = tracing::debug_span!("burn_confirm", asset_uuid = %asset_uuid);
11696        let _enter = confirm_span.enter();
11697
11698        // Extract txid for logging
11699        let txid = tx_data
11700            .get("txid")
11701            .and_then(serde_json::Value::as_str)
11702            .map_or_else(|| "unknown".to_string(), std::string::ToString::to_string);
11703
11704        tracing::debug!(
11705            "Confirming burn for asset {} with txid {} ({} change outputs)",
11706            asset_uuid,
11707            txid,
11708            change_data.len()
11709        );
11710
11711        // Validate inputs
11712        if asset_uuid.is_empty() {
11713            tracing::error!("Burn confirmation failed: empty asset UUID");
11714            return Err(AmpError::validation("Asset UUID cannot be empty"));
11715        }
11716
11717        let request = BurnConfirmRequest {
11718            tx_data,
11719            change_data,
11720        };
11721
11722        // The burn-confirm endpoint returns 200 with empty body (no JSON response)
11723        self.request_empty(
11724            Method::POST,
11725            &["assets", asset_uuid, "burn-confirm"],
11726            Some(&request),
11727        )
11728        .await
11729        .map_err(|e| {
11730            tracing::error!("Burn confirmation failed: {}", e);
11731            AmpError::api(format!(
11732                "Failed to confirm burn for txid {}: {}. \
11733                IMPORTANT: Transaction {} was successful on blockchain. \
11734                You may need to retry confirmation with this txid.",
11735                &txid, e, &txid
11736            ))
11737            .with_context("Burn confirmation")
11738        })?;
11739
11740        tracing::info!("Burn confirmed successfully: txid={}", txid);
11741
11742        Ok(())
11743    }
11744
11745    /// Gets a specific manager by ID.
11746    ///
11747    /// # Arguments
11748    /// * `manager_id` - The ID of the manager to retrieve
11749    ///
11750    /// # Errors
11751    /// Returns an error if:
11752    /// - Authentication fails
11753    /// - The HTTP request fails
11754    /// - The server returns an error status
11755    /// - The response cannot be parsed as JSON
11756    pub async fn get_manager(&self, manager_id: i64) -> Result<crate::model::Manager, Error> {
11757        self.request_json(
11758            Method::GET,
11759            &["managers", &manager_id.to_string()],
11760            None::<&()>,
11761        )
11762        .await
11763    }
11764
11765    /// Removes a manager's permissions to modify a specific asset.
11766    ///
11767    /// This method revokes a manager's access to a specific asset, preventing them from
11768    /// performing asset management operations such as creating assignments, managing ownership,
11769    /// or modifying asset properties. The manager will no longer be able to access this asset
11770    /// through their management interface.
11771    ///
11772    /// # Arguments
11773    /// * `manager_id` - The ID of the manager to remove permissions from
11774    /// * `asset_uuid` - The UUID of the asset to remove permissions for
11775    ///
11776    /// # Returns
11777    /// Returns `Ok(())` on successful permission removal.
11778    ///
11779    /// # Errors
11780    /// Returns an error if:
11781    /// - Authentication fails or insufficient permissions
11782    /// - The manager ID is invalid or does not exist
11783    /// - The asset UUID is invalid or does not exist
11784    /// - The manager does not currently have permissions for this asset
11785    /// - The HTTP request fails
11786    /// - The server returns an error status
11787    ///
11788    /// # Examples
11789    /// ```no_run
11790    /// # use amp_rs::ApiClient;
11791    /// # #[tokio::main]
11792    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
11793    /// let client = ApiClient::new().await?;
11794    ///
11795    /// let manager_id = 123;
11796    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
11797    ///
11798    /// client.manager_remove_asset(manager_id, asset_uuid).await?;
11799    /// println!("Removed asset {} from manager {}", asset_uuid, manager_id);
11800    /// # Ok(())
11801    /// # }
11802    /// ```
11803    ///
11804    /// # Related Methods
11805    /// - [`add_asset_to_manager`](Self::add_asset_to_manager) - Grant manager permissions for an asset
11806    /// - [`get_manager`](Self::get_manager) - Get manager information including current assets
11807    /// - [`revoke_manager`](Self::revoke_manager) - Remove all asset permissions from manager
11808    /// - [`lock_manager`](Self::lock_manager) - Lock manager account
11809    pub async fn manager_remove_asset(
11810        &self,
11811        manager_id: i64,
11812        asset_uuid: &str,
11813    ) -> Result<(), Error> {
11814        self.request_empty(
11815            Method::POST,
11816            &[
11817                "managers",
11818                &manager_id.to_string(),
11819                "assets",
11820                asset_uuid,
11821                "remove",
11822            ],
11823            None::<&()>,
11824        )
11825        .await
11826    }
11827
11828    /// Revokes all asset permissions for a manager.
11829    ///
11830    /// This method first retrieves the manager's current asset permissions,
11831    /// then removes the manager's access to each asset they currently have access to.
11832    ///
11833    /// # Arguments
11834    /// * `manager_id` - The ID of the manager to revoke permissions for
11835    ///
11836    /// # Errors
11837    /// Returns an error if:
11838    /// - Authentication fails
11839    /// - The HTTP request fails
11840    /// - The server returns an error status
11841    /// - Any individual asset removal fails
11842    pub async fn revoke_manager(&self, manager_id: i64) -> Result<(), Error> {
11843        // First, get the manager to see which assets they have access to
11844        let manager = self.get_manager(manager_id).await?;
11845
11846        // Remove the manager's access to each asset
11847        for asset_uuid in &manager.assets {
11848            self.manager_remove_asset(manager_id, asset_uuid).await?;
11849        }
11850
11851        Ok(())
11852    }
11853
11854    /// Gets the current manager information as raw JSON.
11855    ///
11856    /// This method calls the `/managers/me` endpoint to retrieve information
11857    /// about the currently authenticated manager.
11858    ///
11859    /// # Errors
11860    /// Returns an error if:
11861    /// - Authentication fails
11862    /// - The HTTP request fails
11863    /// - The server returns an error status
11864    /// - The response cannot be parsed as JSON
11865    pub async fn get_current_manager_raw(&self) -> Result<serde_json::Value, Error> {
11866        self.request_json(Method::GET, &["managers", "me"], None::<&()>)
11867            .await
11868    }
11869
11870    /// Locks a manager account to prevent further operations.
11871    ///
11872    /// This method sends a PUT request to lock the specified manager, preventing any further
11873    /// operations on that manager account. This is typically used for security purposes or
11874    /// when a manager needs to be temporarily disabled.
11875    ///
11876    /// # Arguments
11877    /// * `manager_id` - The ID of the manager to lock
11878    ///
11879    /// # Returns
11880    /// Returns `Ok(())` if the manager was successfully locked.
11881    ///
11882    /// # Errors
11883    /// Returns an error if:
11884    /// - Authentication fails
11885    /// - The HTTP request fails
11886    /// - The server returns an error status
11887    /// - The manager ID is invalid or does not exist
11888    /// - The manager is already locked
11889    ///
11890    /// # Example
11891    /// ```no_run
11892    /// # use amp_rs::ApiClient;
11893    /// # #[tokio::main]
11894    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
11895    /// let client = ApiClient::new().await?;
11896    ///
11897    /// // Lock manager with ID 123
11898    /// client.lock_manager(123).await?;
11899    /// println!("Manager 123 has been locked successfully");
11900    /// # Ok(())
11901    /// # }
11902    /// ```
11903    pub async fn lock_manager(&self, manager_id: i64) -> Result<(), Error> {
11904        self.request_empty(
11905            Method::PUT,
11906            &["managers", &manager_id.to_string(), "lock"],
11907            None::<&()>,
11908        )
11909        .await
11910    }
11911
11912    /// Unlocks a manager account.
11913    ///
11914    /// # Arguments
11915    /// * `manager_id` - The ID of the manager to unlock
11916    ///
11917    /// # Errors
11918    /// Returns an error if:
11919    /// - Authentication fails
11920    /// - The HTTP request fails
11921    /// - The server returns an error status
11922    pub async fn unlock_manager(&self, manager_id: i64) -> Result<(), Error> {
11923        self.request_empty(
11924            Method::PUT,
11925            &["managers", &manager_id.to_string(), "unlock"],
11926            None::<&()>,
11927        )
11928        .await
11929    }
11930
11931    /// Authorizes a manager to manage a specific asset.
11932    ///
11933    /// This method sends a PUT request to authorize the specified manager to manage the given asset.
11934    /// Once authorized, the manager will have permissions to perform operations on the asset such as
11935    /// creating assignments, managing ownership, and other asset-related operations.
11936    ///
11937    /// # Arguments
11938    /// * `manager_id` - The ID of the manager to authorize
11939    /// * `asset_uuid` - The UUID of the asset to add to the manager's authorized assets
11940    ///
11941    /// # Returns
11942    /// Returns `Ok(())` if the manager was successfully authorized for the asset.
11943    ///
11944    /// # Errors
11945    /// Returns an error if:
11946    /// - Authentication fails or insufficient permissions
11947    /// - The HTTP request fails
11948    /// - The server returns an error status
11949    /// - The manager ID is invalid or does not exist
11950    /// - The asset UUID is invalid or does not exist
11951    /// - The manager is already authorized for this asset
11952    /// - The manager is locked and cannot be modified
11953    ///
11954    /// # Examples
11955    /// ```no_run
11956    /// # use amp_rs::ApiClient;
11957    /// # #[tokio::main]
11958    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
11959    /// let client = ApiClient::new().await?;
11960    ///
11961    /// // Authorize manager 123 to manage asset with UUID "550e8400-e29b-41d4-a716-446655440000"
11962    /// let manager_id = 123;
11963    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
11964    ///
11965    /// client.add_asset_to_manager(manager_id, asset_uuid).await?;
11966    /// println!("Manager {} is now authorized to manage asset {}", manager_id, asset_uuid);
11967    /// # Ok(())
11968    /// # }
11969    /// ```
11970    ///
11971    /// # Related Methods
11972    /// - [`manager_remove_asset`](Self::manager_remove_asset) - Remove manager permissions for an asset
11973    /// - [`get_manager`](Self::get_manager) - Get manager information including current assets
11974    /// - [`get_manager_permissions`](Self::get_manager_permissions) - Get manager's current permissions
11975    /// - [`lock_manager`](Self::lock_manager) - Lock manager account
11976    pub async fn add_asset_to_manager(
11977        &self,
11978        manager_id: i64,
11979        asset_uuid: &str,
11980    ) -> Result<(), Error> {
11981        self.request_empty(
11982            Method::PUT,
11983            &[
11984                "managers",
11985                &manager_id.to_string(),
11986                "assets",
11987                asset_uuid,
11988                "add",
11989            ],
11990            None::<&()>,
11991        )
11992        .await
11993    }
11994
11995    /// Deletes a specific asset assignment.
11996    ///
11997    /// # Arguments
11998    /// * `asset_uuid` - The UUID of the asset
11999    /// * `assignment_id` - The ID of the assignment to delete
12000    ///
12001    /// # Errors
12002    /// Returns an error if:
12003    /// - Authentication fails
12004    /// - The HTTP request fails
12005    /// - The server returns an error status
12006    ///   Removes an asset assignment.
12007    ///
12008    /// This method permanently deletes an asset assignment, returning the allocated assets
12009    /// back to the available pool. This operation cannot be undone. If the assignment has
12010    /// already been distributed, this operation may fail.
12011    ///
12012    /// # Arguments
12013    /// * `asset_uuid` - The UUID of the asset containing the assignment
12014    /// * `assignment_id` - The ID of the assignment to delete
12015    ///
12016    /// # Returns
12017    /// Returns `Ok(())` on successful deletion.
12018    ///
12019    /// # Errors
12020    /// Returns an error if:
12021    /// - Authentication fails or insufficient permissions
12022    /// - The asset UUID is invalid or does not exist
12023    /// - The assignment ID is invalid or does not exist
12024    /// - The assignment has already been distributed and cannot be deleted
12025    /// - The assignment is locked and cannot be modified
12026    /// - The HTTP request fails
12027    /// - The server returns an error status
12028    ///
12029    /// # Examples
12030    /// ```no_run
12031    /// # use amp_rs::ApiClient;
12032    /// # #[tokio::main]
12033    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
12034    /// let client = ApiClient::new().await?;
12035    ///
12036    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
12037    /// let assignment_id = "123";
12038    ///
12039    /// client.delete_asset_assignment(asset_uuid, assignment_id).await?;
12040    /// println!("Successfully deleted assignment {}", assignment_id);
12041    /// # Ok(())
12042    /// # }
12043    /// ```
12044    ///
12045    /// # Related Methods
12046    /// - [`get_asset_assignment`](Self::get_asset_assignment) - Get assignment details before deletion
12047    /// - [`create_asset_assignments`](Self::create_asset_assignments) - Create new assignments
12048    /// - [`edit_asset_assignment`](Self::edit_asset_assignment) - Update assignment instead of deleting
12049    /// - [`lock_asset_assignment`](Self::lock_asset_assignment) - Lock assignment to prevent changes
12050    pub async fn delete_asset_assignment(
12051        &self,
12052        asset_uuid: &str,
12053        assignment_id: &str,
12054    ) -> Result<(), Error> {
12055        self.request_empty(
12056            Method::DELETE,
12057            &["assets", asset_uuid, "assignments", assignment_id, "delete"],
12058            None::<&()>,
12059        )
12060        .await
12061    }
12062
12063    /// Locks a specific asset assignment.
12064    ///
12065    /// # Arguments
12066    /// * `asset_uuid` - The UUID of the asset
12067    /// * `assignment_id` - The ID of the assignment to lock
12068    ///
12069    /// # Errors
12070    /// Returns an error if:
12071    /// - Authentication fails
12072    /// - The HTTP request fails
12073    /// - The server returns an error status
12074    pub async fn lock_asset_assignment(
12075        &self,
12076        asset_uuid: &str,
12077        assignment_id: &str,
12078    ) -> Result<Assignment, Error> {
12079        self.request_json(
12080            Method::PUT,
12081            &["assets", asset_uuid, "assignments", assignment_id, "lock"],
12082            None::<&()>,
12083        )
12084        .await
12085    }
12086
12087    /// Unlocks a specific asset assignment.
12088    ///
12089    /// # Arguments
12090    /// * `asset_uuid` - The UUID of the asset
12091    /// * `assignment_id` - The ID of the assignment to unlock
12092    ///
12093    /// # Errors
12094    /// Returns an error if:
12095    /// - Authentication fails
12096    /// - The HTTP request fails
12097    /// - The server returns an error status
12098    pub async fn unlock_asset_assignment(
12099        &self,
12100        asset_uuid: &str,
12101        assignment_id: &str,
12102    ) -> Result<Assignment, Error> {
12103        self.request_json(
12104            Method::PUT,
12105            &["assets", asset_uuid, "assignments", assignment_id, "unlock"],
12106            None::<&()>,
12107        )
12108        .await
12109    }
12110
12111    /// Adds categories to a registered user.
12112    ///
12113    /// # Arguments
12114    /// * `registered_user_id` - The ID of the registered user
12115    /// * `categories` - A slice of category IDs to add to the user
12116    ///
12117    /// # Errors
12118    /// Returns an error if:
12119    /// - Authentication fails
12120    /// - The HTTP request fails
12121    /// - The server returns an error status
12122    /// - The registered user ID is invalid
12123    /// - Any category ID is invalid
12124    pub async fn add_categories_to_registered_user(
12125        &self,
12126        registered_user_id: i64,
12127        categories: &[i64],
12128    ) -> Result<(), Error> {
12129        let request_body = CategoriesRequest {
12130            categories: categories.to_vec(),
12131        };
12132
12133        self.request_empty(
12134            Method::PUT,
12135            &[
12136                "registered_users",
12137                &registered_user_id.to_string(),
12138                "categories",
12139                "add",
12140            ],
12141            Some(request_body),
12142        )
12143        .await
12144    }
12145
12146    /// Removes categories from a registered user
12147    ///
12148    /// # Arguments
12149    /// * `registered_user_id` - The ID of the registered user
12150    /// * `categories` - A slice of category IDs to remove from the user
12151    ///
12152    /// # Returns
12153    /// Returns `Ok(())` if the categories are successfully removed, or an error if:
12154    /// - Authentication fails
12155    /// - The HTTP request fails
12156    /// - The server returns an error status
12157    /// - The registered user ID is invalid
12158    /// - Any category ID is not associated with the user
12159    pub async fn remove_categories_from_registered_user(
12160        &self,
12161        registered_user_id: i64,
12162        categories: &[i64],
12163    ) -> Result<(), Error> {
12164        let request_body = CategoriesRequest {
12165            categories: categories.to_vec(),
12166        };
12167
12168        self.request_empty(
12169            Method::PUT,
12170            &[
12171                "registered_users",
12172                &registered_user_id.to_string(),
12173                "categories",
12174                "delete",
12175            ],
12176            Some(request_body),
12177        )
12178        .await
12179    }
12180
12181    /// Distributes assets to multiple users through a comprehensive workflow
12182    ///
12183    /// This method orchestrates the complete asset distribution process:
12184    /// 1. Validates input parameters (asset UUID format, assignments structure)
12185    /// 2. Verifies `ElementsRpc` connection and signer interface availability
12186    /// 3. Authenticates with the AMP API using the client's token
12187    /// 4. Creates a distribution request via the AMP API
12188    /// 5. Constructs and signs the blockchain transaction using the provided signer
12189    /// 6. Broadcasts the transaction to the Elements network
12190    /// 7. Waits for blockchain confirmations (2 confirmations minimum)
12191    /// 8. Confirms the distribution with the AMP API
12192    ///
12193    /// # Arguments
12194    /// * `asset_uuid` - The UUID of the asset to distribute (must be valid UUID format)
12195    /// * `assignments` - Vector of assignments specifying `user_id`, address, and amount
12196    /// * `node_rpc` - `ElementsRpc` client for blockchain operations
12197    /// * `signer` - Signer implementation for transaction signing
12198    ///
12199    /// # Returns
12200    /// Returns `Ok(())` if the distribution completes successfully, or an `AmpError` if:
12201    /// - Input validation fails (invalid UUID format, empty assignments, etc.)
12202    /// - `ElementsRpc` connection cannot be established
12203    /// - Signer interface is not available
12204    /// - Authentication with AMP API fails
12205    /// - Distribution creation fails
12206    /// - Transaction construction or signing fails
12207    /// - Blockchain broadcasting fails
12208    /// - Confirmation timeout occurs
12209    /// - Distribution confirmation with AMP API fails
12210    ///
12211    /// # Examples
12212    /// ```no_run
12213    /// # use amp_rs::{ApiClient, ElementsRpc, AmpError};
12214    /// # use amp_rs::model::AssetDistributionAssignment;
12215    /// # use amp_rs::signer::{Signer, LwkSoftwareSigner};
12216    /// # #[tokio::main]
12217    /// # async fn main() -> Result<(), AmpError> {
12218    /// let client = ApiClient::new().await?;
12219    /// let elements_rpc = ElementsRpc::from_env()?;
12220    /// let (_, signer) = LwkSoftwareSigner::generate_new()?;
12221    ///
12222    /// let assignments = vec![
12223    ///     AssetDistributionAssignment {
12224    ///         user_id: "user123".to_string(),
12225    ///         address: "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
12226    ///         amount: 100.0,
12227    ///     },
12228    /// ];
12229    ///
12230    /// client.distribute_asset(
12231    ///     "550e8400-e29b-41d4-a716-446655440000",
12232    ///     assignments,
12233    ///     &elements_rpc,
12234    ///     "wallet_name",
12235    ///     &signer
12236    /// ).await?;
12237    /// # Ok(())
12238    /// # }
12239    /// ```
12240    ///
12241    /// # Requirements
12242    /// This method implements requirements:
12243    /// - 1.1: Single method for complete distribution workflow
12244    /// - 2.2: Assignment details validation
12245    /// - 2.4: Input validation for all parameters
12246    /// - 5.1: Comprehensive error handling with context
12247    #[allow(clippy::cognitive_complexity, clippy::too_many_lines)]
12248    pub async fn distribute_asset(
12249        &self,
12250        asset_uuid: &str,
12251        assignments: Vec<AssetDistributionAssignment>,
12252        node_rpc: &ElementsRpc,
12253        wallet_name: &str,
12254        signer: &dyn Signer,
12255    ) -> Result<(), AmpError> {
12256        let distribution_span = tracing::info_span!(
12257            "distribute_asset",
12258            asset_uuid = %asset_uuid,
12259            assignment_count = assignments.len()
12260        );
12261        let _enter = distribution_span.enter();
12262
12263        tracing::info!(
12264            "Starting asset distribution workflow for asset: {} with {} assignments",
12265            asset_uuid,
12266            assignments.len()
12267        );
12268
12269        // Step 1: Input validation - asset_uuid format
12270        tracing::debug!("Step 1: Validating asset UUID format");
12271        Self::validate_asset_uuid(asset_uuid).map_err(|e| {
12272            let error = AmpError::validation(format!("Invalid asset UUID: {e}"));
12273            tracing::error!("Asset UUID validation failed: {}", e);
12274            error.with_context("Step 1: Asset UUID validation")
12275        })?;
12276        tracing::debug!("Asset UUID validation passed");
12277
12278        // Step 2: Input validation - assignments data structure
12279        tracing::debug!("Step 2: Validating {} assignments", assignments.len());
12280        Self::validate_assignments(&assignments).map_err(|e| {
12281            let error = AmpError::validation(format!("Invalid assignments: {e}"));
12282            tracing::error!("Assignments validation failed: {}", e);
12283            error.with_context("Step 2: Assignments validation")
12284        })?;
12285        tracing::debug!("Assignments validation passed");
12286
12287        // Step 3: Check ElementsRpc connection availability
12288        tracing::debug!("Step 3: Validating Elements RPC connection");
12289        self.validate_elements_rpc_connection(node_rpc)
12290            .await
12291            .map_err(|e| {
12292                let error = AmpError::rpc(format!("ElementsRpc connection validation failed: {e}"));
12293                tracing::error!("Elements RPC connection validation failed: {}", e);
12294                error.with_context("Step 3: Elements RPC connection validation")
12295            })?;
12296        tracing::debug!("Elements RPC connection validation passed");
12297
12298        // Step 4: Check signer interface availability
12299        tracing::debug!("Step 4: Validating signer interface");
12300        self.validate_signer_interface(signer).await.map_err(|e| {
12301            let error = AmpError::validation(format!("Signer interface validation failed: {e}"));
12302            tracing::error!("Signer interface validation failed: {}", e);
12303            error.with_context("Step 4: Signer interface validation")
12304        })?;
12305        tracing::debug!("Signer interface validation passed");
12306
12307        tracing::info!("✓ All input validations completed successfully");
12308
12309        // Step 5: Authenticate with AMP API using existing TokenManager
12310        tracing::debug!("Step 5: Authenticating with AMP API");
12311        let _token = self.token_strategy.get_token().await.map_err(|e| {
12312            tracing::error!("AMP API authentication failed: {}", e);
12313            let amp_error = AmpError::Existing(e);
12314            if amp_error.is_retryable() {
12315                if let Some(instructions) = amp_error.retry_instructions() {
12316                    tracing::warn!("Retry instructions: {}", instructions);
12317                }
12318            }
12319            amp_error.with_context("Step 5: AMP API authentication")
12320        })?;
12321        tracing::info!("✓ Successfully authenticated with AMP API");
12322
12323        // Step 6: Create distribution request and parse response data
12324        tracing::debug!(
12325            "Step 6: Creating distribution request with {} assignments",
12326            assignments.len()
12327        );
12328        let distribution_response = self
12329            .create_distribution(asset_uuid, assignments)
12330            .await
12331            .map_err(|e| {
12332                tracing::error!("Distribution creation failed: {}", e);
12333                if e.is_retryable() {
12334                    if let Some(instructions) = e.retry_instructions() {
12335                        tracing::warn!("Retry instructions: {}", instructions);
12336                    }
12337                }
12338                e.with_context("Step 6: Distribution creation")
12339            })?;
12340
12341        tracing::info!(
12342            "✓ Distribution created successfully: {} with asset_id: {}",
12343            distribution_response.distribution_uuid,
12344            distribution_response.asset_id
12345        );
12346
12347        // Step 7: Verify Elements node status and execute transaction workflow
12348        tracing::debug!("Step 7: Verifying Elements node status");
12349        let (network_info, blockchain_info) = node_rpc.get_node_status().await.map_err(|e| {
12350            tracing::error!("Elements node status verification failed: {}", e);
12351            if e.is_retryable() {
12352                if let Some(instructions) = e.retry_instructions() {
12353                    tracing::warn!("Retry instructions: {}", instructions);
12354                }
12355            }
12356            e.with_context("Step 7: Elements node status verification")
12357        })?;
12358
12359        tracing::info!(
12360            "✓ Elements node verified - chain: {}, blocks: {}, connections: {}",
12361            blockchain_info.chain,
12362            blockchain_info.blocks,
12363            network_info.connections
12364        );
12365
12366        // Step 8: Send distribution transaction using Elements' sendmany
12367        tracing::debug!("Step 8: Sending distribution transaction using Elements sendmany");
12368
12369        // Create asset amounts map for sendmany (all outputs use the same asset)
12370        let mut asset_amounts = std::collections::HashMap::new();
12371        for address in distribution_response.map_address_amount.keys() {
12372            asset_amounts.insert(address.clone(), distribution_response.asset_id.clone());
12373        }
12374
12375        tracing::info!(
12376            "Using sendmany for {} outputs with asset {}",
12377            distribution_response.map_address_amount.len(),
12378            distribution_response.asset_id
12379        );
12380
12381        // Use Elements' sendmany which properly handles confidential transactions
12382        let txid = node_rpc
12383            .sendmany(
12384                wallet_name,
12385                distribution_response.map_address_amount.clone(),
12386                asset_amounts,
12387                Some(0), // min_conf: 0 to include unconfirmed UTXOs (matches Python implementation)
12388                Some("AMP asset distribution"), // comment
12389                None,    // subtract_fee_from: let Elements handle fees automatically
12390                Some(false), // replaceable: false for final transactions
12391                Some(1), // conf_target: 1 block for faster confirmation
12392                Some("UNSET"), // estimate_mode: let Elements choose
12393            )
12394            .await
12395            .map_err(|e| {
12396                tracing::error!("Sendmany transaction failed: {}", e);
12397                if e.is_retryable() {
12398                    if let Some(instructions) = e.retry_instructions() {
12399                        tracing::warn!("Retry instructions: {}", instructions);
12400                    }
12401                }
12402                e.with_context("Step 8: Sendmany transaction")
12403            })?;
12404
12405        tracing::info!("✓ Transaction sent successfully with ID: {}", txid);
12406
12407        // Step 9: Wait for confirmations
12408        tracing::debug!("Step 9: Waiting for blockchain confirmations (minimum 2 confirmations, 10-minute timeout)");
12409        let confirmation_start = std::time::Instant::now();
12410        let tx_detail = node_rpc.wait_for_confirmations(&txid, Some(2), Some(10)).await
12411            .map_err(|e| {
12412                let elapsed = confirmation_start.elapsed();
12413                tracing::error!(
12414                    "Confirmation waiting failed after {:?}: {}",
12415                    elapsed,
12416                    e
12417                );
12418
12419                if let AmpError::Timeout(_) = &e {
12420                    tracing::warn!(
12421                        "Confirmation timeout - transaction {} may still be pending. \
12422                        Use this txid to manually confirm the distribution if it gets confirmed later.",
12423                        txid
12424                    );
12425                    let timeout_error = AmpError::timeout(format!(
12426                        "Confirmation timeout for txid: {txid}. Use this txid to manually confirm the distribution."
12427                    ));
12428                    timeout_error.with_context("Step 9: Confirmation waiting")
12429                } else {
12430                    if e.is_retryable() {
12431                        if let Some(instructions) = e.retry_instructions() {
12432                            tracing::warn!("Retry instructions: {}", instructions);
12433                        }
12434                    }
12435                    e.with_context(format!("Step 9: Confirmation waiting for txid: {txid}"))
12436                }
12437            })?;
12438
12439        let confirmation_duration = confirmation_start.elapsed();
12440        tracing::info!(
12441            "✓ Transaction confirmed with {} confirmations at block height: {:?} (took {:?})",
12442            tx_detail.confirmations,
12443            tx_detail.blockheight,
12444            confirmation_duration
12445        );
12446
12447        // Step 10: Collect change data for confirmation
12448        tracing::debug!("Step 10: Collecting change data for distribution confirmation");
12449        let change_data = node_rpc
12450            .collect_change_data(
12451                &distribution_response.asset_id,
12452                &txid,
12453                node_rpc,
12454                wallet_name,
12455            )
12456            .await
12457            .map_err(|e| {
12458                tracing::error!("Change data collection failed: {}", e);
12459                if e.is_retryable() {
12460                    if let Some(instructions) = e.retry_instructions() {
12461                        tracing::warn!("Retry instructions: {}", instructions);
12462                    }
12463                }
12464                e.with_context("Step 10: Change data collection")
12465            })?;
12466
12467        tracing::info!("✓ Collected {} change UTXOs", change_data.len());
12468        if !change_data.is_empty() {
12469            tracing::debug!("Change UTXOs: {:?}", change_data);
12470        }
12471
12472        // Step 11: Submit final confirmation to AMP API
12473        tracing::debug!("Step 11: Submitting final confirmation to AMP API");
12474
12475        // Extract the details field from the transaction (matching Python implementation)
12476        // Python: details = rpc.call('gettransaction', txid).get('details')
12477        let transaction_details = tx_detail.details.unwrap_or_else(Vec::new);
12478        tracing::debug!(
12479            "Transaction details for confirmation: {:?}",
12480            transaction_details
12481        );
12482
12483        let amp_tx_data = crate::model::AmpTxData {
12484            details: serde_json::Value::Array(transaction_details),
12485            txid: txid.clone(),
12486        };
12487
12488        // Log the exact payload being sent to AMP for debugging
12489        tracing::info!("Sending confirmation payload to AMP:");
12490        tracing::info!("  tx_data.txid: {}", amp_tx_data.txid);
12491        tracing::info!("  tx_data.details: {:?}", amp_tx_data.details);
12492        tracing::info!("  change_data: {} UTXOs", change_data.len());
12493
12494        let confirmation_request = crate::model::ConfirmDistributionRequest {
12495            tx_data: amp_tx_data.clone(),
12496            change_data: change_data.clone(),
12497        };
12498
12499        if let Ok(payload_json) = serde_json::to_string_pretty(&confirmation_request) {
12500            tracing::debug!("Full confirmation payload: {}", payload_json);
12501        }
12502
12503        self.confirm_distribution(
12504            asset_uuid,
12505            &distribution_response.distribution_uuid,
12506            amp_tx_data,
12507            change_data,
12508        )
12509        .await
12510        .map_err(|e| {
12511            tracing::error!("Distribution confirmation failed: {}", e);
12512
12513            // For confirmation failures, always provide retry instructions with txid
12514            let confirmation_error = AmpError::api(format!(
12515                "Failed to confirm distribution {}: {}. \
12516                IMPORTANT: Transaction {} was successful on blockchain. \
12517                Use this txid to manually retry confirmation.",
12518                distribution_response.distribution_uuid, e, txid
12519            ));
12520
12521            if e.is_retryable() {
12522                if let Some(instructions) = e.retry_instructions() {
12523                    tracing::warn!("Retry instructions: {}", instructions);
12524                }
12525            }
12526
12527            confirmation_error.with_context("Step 11: Distribution confirmation")
12528        })?;
12529
12530        tracing::info!(
12531            "🎉 Asset distribution completed successfully for asset: {} with transaction: {}",
12532            asset_uuid,
12533            txid
12534        );
12535
12536        Ok(())
12537    }
12538
12539    /// Reissues an asset through a comprehensive workflow
12540    ///
12541    /// This method orchestrates the complete asset reissuance process:
12542    /// 1. Validates input parameters (asset UUID format, amount)
12543    /// 2. Verifies `ElementsRpc` connection and signer interface availability
12544    /// 3. Authenticates with the AMP API using the client's token
12545    /// 4. Creates a reissuance request via the AMP API
12546    /// 5. Waits for transaction propagation and checks for lost outputs
12547    /// 6. Verifies reissuance token UTXOs are available
12548    /// 7. Calls the Elements node's `reissueasset` RPC method
12549    /// 8. Waits for blockchain confirmations (2 confirmations minimum)
12550    /// 9. Retrieves transaction details and issuance information
12551    /// 10. Confirms the reissuance with the AMP API
12552    ///
12553    /// # Arguments
12554    /// * `asset_uuid` - The UUID of the asset to reissue (must be valid UUID format)
12555    /// * `amount_to_reissue` - The amount to reissue (in satoshis for the asset)
12556    /// * `node_rpc` - `ElementsRpc` client for blockchain operations
12557    /// * `signer` - Signer implementation for future support (currently not used, node RPC signs)
12558    ///
12559    /// # Returns
12560    /// Returns `Ok(())` if the reissuance completes successfully, or an `AmpError` if:
12561    /// - Input validation fails (invalid UUID format, invalid amount, etc.)
12562    /// - `ElementsRpc` connection cannot be established
12563    /// - Signer interface is not available
12564    /// - Authentication with AMP API fails
12565    /// - Reissuance request creation fails
12566    /// - Lost outputs are detected
12567    /// - Required UTXOs are not available
12568    /// - Reissuance transaction creation fails
12569    /// - Confirmation timeout occurs
12570    /// - Reissuance confirmation with AMP API fails
12571    ///
12572    /// # Examples
12573    /// ```no_run
12574    /// # use amp_rs::{ApiClient, ElementsRpc, AmpError};
12575    /// # use amp_rs::signer::LwkSoftwareSigner;
12576    /// # #[tokio::main]
12577    /// # async fn main() -> Result<(), AmpError> {
12578    /// let client = ApiClient::new().await?;
12579    /// let elements_rpc = ElementsRpc::from_env()?;
12580    /// let (_, signer) = LwkSoftwareSigner::generate_new()?;
12581    ///
12582    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
12583    /// let amount = 1000000; // 0.01 of an asset with 8 decimals
12584    ///
12585    /// client.reissue_asset(asset_uuid, amount, &elements_rpc, &signer).await?;
12586    /// println!("Reissuance completed successfully");
12587    /// # Ok(())
12588    /// # }
12589    /// ```
12590    ///
12591    /// # Related Methods
12592    /// - [`reissue_request`](Self::reissue_request) - Create a reissuance request only
12593    /// - [`reissue_confirm`](Self::reissue_confirm) - Confirm a reissuance transaction only
12594    #[allow(clippy::cognitive_complexity, clippy::too_many_lines)]
12595    pub async fn reissue_asset(
12596        &self,
12597        asset_uuid: &str,
12598        amount_to_reissue: i64,
12599        node_rpc: &ElementsRpc,
12600        signer: &dyn Signer,
12601    ) -> Result<(), AmpError> {
12602        let reissue_span = tracing::info_span!(
12603            "reissue_asset",
12604            asset_uuid = %asset_uuid,
12605            amount_to_reissue = amount_to_reissue
12606        );
12607        let _enter = reissue_span.enter();
12608
12609        tracing::info!(
12610            "Starting asset reissuance workflow for asset: {} with amount: {}",
12611            asset_uuid,
12612            amount_to_reissue
12613        );
12614
12615        // Step 1: Input validation - asset_uuid format
12616        tracing::debug!("Step 1: Validating asset UUID format");
12617        Self::validate_asset_uuid(asset_uuid).map_err(|e| {
12618            let error = AmpError::validation(format!("Invalid asset UUID: {e}"));
12619            tracing::error!("Asset UUID validation failed: {}", e);
12620            error.with_context("Step 1: Asset UUID validation")
12621        })?;
12622        tracing::debug!("Asset UUID validation passed");
12623
12624        // Step 2: Input validation - amount
12625        tracing::debug!("Step 2: Validating reissuance amount");
12626        if amount_to_reissue <= 0 {
12627            let error = AmpError::validation("Amount to reissue must be positive".to_string());
12628            tracing::error!("Amount validation failed: amount must be positive");
12629            return Err(error.with_context("Step 2: Amount validation"));
12630        }
12631        tracing::debug!("Amount validation passed");
12632
12633        // Step 3: Check ElementsRpc connection availability
12634        tracing::debug!("Step 3: Validating Elements RPC connection");
12635        self.validate_elements_rpc_connection(node_rpc)
12636            .await
12637            .map_err(|e| {
12638                let error = AmpError::rpc(format!("ElementsRpc connection validation failed: {e}"));
12639                tracing::error!("Elements RPC connection validation failed: {}", e);
12640                error.with_context("Step 3: Elements RPC connection validation")
12641            })?;
12642        tracing::debug!("Elements RPC connection validation passed");
12643
12644        // Step 4: Check signer interface availability (for future support)
12645        tracing::debug!("Step 4: Validating signer interface");
12646        self.validate_signer_interface(signer).await.map_err(|e| {
12647            let error = AmpError::validation(format!("Signer interface validation failed: {e}"));
12648            tracing::error!("Signer interface validation failed: {}", e);
12649            error.with_context("Step 4: Signer interface validation")
12650        })?;
12651        tracing::debug!("Signer interface validation passed");
12652
12653        tracing::info!("✓ All input validations completed successfully");
12654
12655        // Step 5: Authenticate with AMP API using existing TokenManager
12656        tracing::debug!("Step 5: Authenticating with AMP API");
12657        let _token = self.token_strategy.get_token().await.map_err(|e| {
12658            tracing::error!("AMP API authentication failed: {}", e);
12659            let amp_error = AmpError::Existing(e);
12660            if amp_error.is_retryable() {
12661                if let Some(instructions) = amp_error.retry_instructions() {
12662                    tracing::warn!("Retry instructions: {}", instructions);
12663                }
12664            }
12665            amp_error.with_context("Step 5: AMP API authentication")
12666        })?;
12667        tracing::info!("✓ Successfully authenticated with AMP API");
12668
12669        // Step 6: Create reissuance request and parse response data
12670        tracing::debug!(
12671            "Step 6: Creating reissuance request with amount {}",
12672            amount_to_reissue
12673        );
12674        let reissue_response = self
12675            .reissue_request(asset_uuid, amount_to_reissue)
12676            .await
12677            .map_err(|e| {
12678                tracing::error!("Reissuance request creation failed: {}", e);
12679                if e.is_retryable() {
12680                    if let Some(instructions) = e.retry_instructions() {
12681                        tracing::warn!("Retry instructions: {}", instructions);
12682                    }
12683                }
12684                e.with_context("Step 6: Reissuance request creation")
12685            })?;
12686
12687        tracing::info!(
12688            "✓ Reissuance request created successfully: asset_id={}, amount={}",
12689            reissue_response.asset_id,
12690            reissue_response.amount
12691        );
12692
12693        // Step 7: Verify Elements node status
12694        tracing::debug!("Step 7: Verifying Elements node status");
12695        let (network_info, blockchain_info) = node_rpc.get_node_status().await.map_err(|e| {
12696            tracing::error!("Elements node status verification failed: {}", e);
12697            if e.is_retryable() {
12698                if let Some(instructions) = e.retry_instructions() {
12699                    tracing::warn!("Retry instructions: {}", instructions);
12700                }
12701            }
12702            e.with_context("Step 7: Elements node status verification")
12703        })?;
12704
12705        tracing::info!(
12706            "✓ Elements node verified - chain: {}, blocks: {}, connections: {}",
12707            blockchain_info.chain,
12708            blockchain_info.blocks,
12709            network_info.connections
12710        );
12711
12712        // Step 8: Wait for transaction propagation (60 seconds as per Python script)
12713        tracing::debug!("Step 8: Waiting for transaction propagation (60 seconds)");
12714        tracing::info!("Waiting 60 seconds for transaction propagation...");
12715        tokio::time::sleep(tokio::time::Duration::from_secs(60)).await;
12716        tracing::debug!("Transaction propagation wait completed");
12717
12718        // Step 9: Check for lost outputs
12719        tracing::debug!("Step 9: Checking for lost outputs");
12720        let balance_response: serde_json::Value = self
12721            .request_json(Method::GET, &["assets", asset_uuid, "balance"], None::<&()>)
12722            .await
12723            .map_err(|e| {
12724                tracing::error!("Failed to check lost outputs: {}", e);
12725                AmpError::api(format!("Balance check failed: {e}"))
12726                    .with_context("Step 9: Lost outputs check")
12727            })?;
12728
12729        // Check if lost_outputs field exists and is not empty
12730        if let Some(lost_outputs) = balance_response.get("lost_outputs") {
12731            if let Some(lost_outputs_array) = lost_outputs.as_array() {
12732                if !lost_outputs_array.is_empty() {
12733                    let error_msg = format!(
12734                        "Lost outputs detected: {}. Transaction will not be sent.",
12735                        serde_json::to_string(&lost_outputs_array).unwrap_or_default()
12736                    );
12737                    tracing::error!("{}", error_msg);
12738                    return Err(AmpError::api(error_msg).with_context("Step 9: Lost outputs check"));
12739                }
12740            }
12741        }
12742
12743        tracing::info!("✓ No lost outputs detected");
12744
12745        // Step 10: Check UTXOs match reissuance_utxos from response
12746        tracing::debug!(
12747            "Step 10: Verifying {} reissuance token UTXOs are available",
12748            reissue_response.reissuance_utxos.len()
12749        );
12750
12751        let available_utxos = node_rpc.list_unspent(None).await.map_err(|e| {
12752            tracing::error!("Failed to list UTXOs: {}", e);
12753            AmpError::rpc(format!("Failed to list UTXOs: {e}"))
12754                .with_context("Step 10: UTXO verification")
12755        })?;
12756
12757        // Check that all required reissuance UTXOs are available
12758        let local_utxos: std::collections::HashSet<(String, i64)> = available_utxos
12759            .iter()
12760            .map(|utxo| (utxo.txid.clone(), i64::from(utxo.vout)))
12761            .collect();
12762
12763        let mut missing_utxos = Vec::new();
12764        for required_utxo in &reissue_response.reissuance_utxos {
12765            if !local_utxos.contains(&(required_utxo.txid.clone(), required_utxo.vout)) {
12766                missing_utxos.push(format!("{}:{}", required_utxo.txid, required_utxo.vout));
12767            }
12768        }
12769
12770        if !missing_utxos.is_empty() {
12771            let error_msg = format!(
12772                "Missing reissuance token UTXOs: {}. Ensure reissuance tokens are available in the wallet.",
12773                missing_utxos.join(", ")
12774            );
12775            tracing::error!("{}", error_msg);
12776            return Err(AmpError::rpc(error_msg).with_context("Step 10: UTXO verification"));
12777        }
12778
12779        tracing::info!(
12780            "✓ All {} reissuance token UTXOs are available",
12781            reissue_response.reissuance_utxos.len()
12782        );
12783
12784        // Step 11: Call Elements node's reissueasset RPC method
12785        tracing::debug!("Step 11: Calling Elements reissueasset RPC method");
12786        let reissuance_output = node_rpc
12787            .reissueasset(&reissue_response.asset_id, reissue_response.amount)
12788            .await
12789            .map_err(|e| {
12790                tracing::error!("Reissuance transaction creation failed: {}", e);
12791                if e.is_retryable() {
12792                    if let Some(instructions) = e.retry_instructions() {
12793                        tracing::warn!("Retry instructions: {}", instructions);
12794                    }
12795                }
12796                e.with_context("Step 11: Reissuance transaction creation")
12797            })?;
12798
12799        // Extract txid and vin from reissuance output
12800        let txid = reissuance_output
12801            .get("txid")
12802            .and_then(|v| v.as_str())
12803            .ok_or_else(|| {
12804                AmpError::rpc("Reissuance output missing txid field".to_string())
12805                    .with_context("Step 11: Reissuance transaction creation")
12806            })?;
12807        let vin = reissuance_output
12808            .get("vin")
12809            .and_then(serde_json::Value::as_u64)
12810            .ok_or_else(|| {
12811                AmpError::rpc("Reissuance output missing vin field".to_string())
12812                    .with_context("Step 11: Reissuance transaction creation")
12813            })?;
12814
12815        tracing::info!(
12816            "✓ Reissuance transaction created: txid={}, vin={}",
12817            txid,
12818            vin
12819        );
12820
12821        // Step 12: Wait for confirmations
12822        tracing::debug!("Step 12: Waiting for blockchain confirmations (minimum 2 confirmations, 10-minute timeout)");
12823        let confirmation_start = std::time::Instant::now();
12824
12825        // First, wait for 1 confirmation before spawning treasury address task
12826        node_rpc
12827            .wait_for_confirmations(txid, Some(1), Some(10))
12828            .await
12829            .map_err(|e| {
12830                let elapsed = confirmation_start.elapsed();
12831                tracing::error!(
12832                    "Confirmation waiting (1 conf) failed after {:?}: {}",
12833                    elapsed,
12834                    e
12835                );
12836                e.with_context(format!(
12837                    "Step 12: Waiting for 1 confirmation for txid: {txid}"
12838                ))
12839            })?;
12840
12841        tracing::info!(
12842            "✓ Transaction has 1 confirmation, spawning treasury address extraction task"
12843        );
12844
12845        // Spawn async task to extract and submit reissuance token change address
12846        // This runs in parallel with the remaining confirmation wait
12847        let asset_uuid_clone = asset_uuid.to_string();
12848        let txid_clone = txid.to_string();
12849        let client_clone = self.clone();
12850        let node_rpc_clone = node_rpc.clone();
12851
12852        tokio::spawn(async move {
12853            if let Err(e) = client_clone
12854                .extract_and_submit_reissuance_token_change_address(
12855                    &asset_uuid_clone,
12856                    &txid_clone,
12857                    &node_rpc_clone,
12858                )
12859                .await
12860            {
12861                tracing::warn!(
12862                    "Failed to extract/submit reissuance token change address: {}. \
12863                    This is non-critical and does not affect the reissuance operation.",
12864                    e
12865                );
12866            }
12867        });
12868
12869        // Continue waiting for the full 2 confirmations
12870        let _tx_detail = node_rpc
12871            .wait_for_confirmations(txid, Some(2), Some(10))
12872            .await
12873            .map_err(|e| {
12874                let elapsed = confirmation_start.elapsed();
12875                tracing::error!(
12876                    "Confirmation waiting failed after {:?}: {}",
12877                    elapsed,
12878                    e
12879                );
12880
12881                if let AmpError::Timeout(_) = &e {
12882                    tracing::warn!(
12883                        "Confirmation timeout - transaction {} may still be pending. \
12884                        Use this txid to manually confirm the reissuance if it gets confirmed later.",
12885                        txid
12886                    );
12887                    let timeout_error = AmpError::timeout(format!(
12888                        "Confirmation timeout for txid: {txid}. Use this txid to manually confirm the reissuance."
12889                    ));
12890                    timeout_error.with_context("Step 12: Confirmation waiting")
12891                } else {
12892                    if e.is_retryable() {
12893                        if let Some(instructions) = e.retry_instructions() {
12894                            tracing::warn!("Retry instructions: {}", instructions);
12895                        }
12896                    }
12897                    e.with_context(format!("Step 12: Confirmation waiting for txid: {txid}"))
12898                }
12899            })?;
12900
12901        tracing::info!("✓ Transaction confirmed with at least 2 confirmations");
12902
12903        // Step 13: Get transaction details and issuance information
12904        tracing::debug!("Step 13: Retrieving transaction details and issuance information");
12905
12906        // Get transaction details
12907        let tx_detail = node_rpc.get_transaction(txid).await.map_err(|e| {
12908            tracing::error!("Failed to get transaction details: {}", e);
12909            AmpError::rpc(format!("Failed to get transaction details: {e}"))
12910                .with_context("Step 13: Transaction details retrieval")
12911        })?;
12912
12913        // Convert details to JSON Value
12914        let details = serde_json::to_value(tx_detail.details).map_err(|e| {
12915            tracing::error!("Failed to serialize transaction details: {}", e);
12916            AmpError::api(format!("Failed to serialize transaction details: {e}"))
12917                .with_context("Step 13: Transaction details serialization")
12918        })?;
12919
12920        // Get all issuances and filter by txid
12921        let all_issuances = node_rpc.list_issuances(None).await.map_err(|e| {
12922            tracing::error!("Failed to list issuances: {}", e);
12923            AmpError::rpc(format!("Failed to list issuances: {e}"))
12924                .with_context("Step 13: Issuance listing")
12925        })?;
12926
12927        let listissuances: Vec<serde_json::Value> = all_issuances
12928            .into_iter()
12929            .filter(|issuance| {
12930                issuance
12931                    .get("txid")
12932                    .and_then(serde_json::Value::as_str)
12933                    .is_some_and(|tid| tid == txid)
12934            })
12935            .collect();
12936
12937        tracing::info!(
12938            "✓ Retrieved transaction details and {} issuance(s) for txid {}",
12939            listissuances.len(),
12940            txid
12941        );
12942
12943        // Step 14: Confirm reissuance with AMP API
12944        tracing::debug!("Step 14: Confirming reissuance with AMP API");
12945
12946        let reissuance_output_value = serde_json::json!({
12947            "txid": txid,
12948            "vin": vin
12949        });
12950
12951        self.reissue_confirm(asset_uuid, details, listissuances, reissuance_output_value)
12952            .await
12953            .map_err(|e| {
12954                tracing::error!("Reissuance confirmation failed: {}", e);
12955
12956                // For confirmation failures, always provide retry instructions with txid
12957                let confirmation_error = AmpError::api(format!(
12958                    "Failed to confirm reissuance: {e}. \
12959                IMPORTANT: Transaction {txid} was successful on blockchain. \
12960                Use this txid to manually retry confirmation."
12961                ));
12962
12963                if e.is_retryable() {
12964                    if let Some(instructions) = e.retry_instructions() {
12965                        tracing::warn!("Retry instructions: {}", instructions);
12966                    }
12967                }
12968
12969                confirmation_error.with_context("Step 14: Reissuance confirmation")
12970            })?;
12971
12972        tracing::info!(
12973            "🎉 Asset reissuance completed successfully for asset: {} with transaction: {}",
12974            asset_uuid,
12975            txid
12976        );
12977
12978        Ok(())
12979    }
12980
12981    /// Extracts the reissuance token change address from a reissuance transaction
12982    /// and submits it to the asset's treasury addresses list.
12983    ///
12984    /// This method is called asynchronously after a reissuance transaction has 1 confirmation.
12985    /// It runs in a separate task to avoid blocking the main reissuance flow.
12986    ///
12987    /// # Arguments
12988    /// * `asset_uuid` - The UUID of the asset that was reissued
12989    /// * `txid` - The transaction ID of the reissuance transaction
12990    /// * `node_rpc` - Elements RPC client to query transaction details
12991    ///
12992    /// # Returns
12993    /// Returns `Ok(())` if successful, or an error if extraction/submission fails.
12994    /// Errors are logged but do not affect the main reissuance operation.
12995    #[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
12996    async fn extract_and_submit_reissuance_token_change_address(
12997        &self,
12998        asset_uuid: &str,
12999        txid: &str,
13000        node_rpc: &ElementsRpc,
13001    ) -> Result<(), AmpError> {
13002        tracing::info!(
13003            "[Treasury Address Task] Starting extraction for asset {} from txid {}",
13004            asset_uuid,
13005            txid
13006        );
13007
13008        // Get asset to retrieve reissuance token ID
13009        let asset = self.get_asset(asset_uuid).await.map_err(|e| {
13010            tracing::error!("[Treasury Address Task] Failed to get asset: {}", e);
13011            AmpError::api(format!("Failed to get asset: {e}"))
13012        })?;
13013
13014        let reissuance_token_id = asset.reissuance_token_id.as_ref().ok_or_else(|| {
13015            tracing::error!("[Treasury Address Task] Asset has no reissuance token ID");
13016            AmpError::validation("Asset has no reissuance token ID".to_string())
13017        })?;
13018
13019        tracing::debug!(
13020            "[Treasury Address Task] Looking for reissuance token ID: {}",
13021            reissuance_token_id
13022        );
13023
13024        // Get transaction details
13025        let tx_detail = node_rpc.get_transaction(txid).await.map_err(|e| {
13026            tracing::error!(
13027                "[Treasury Address Task] Failed to get transaction details: {}",
13028                e
13029            );
13030            e
13031        })?;
13032
13033        // Find the reissuance token change address
13034        let mut change_address: Option<String> = None;
13035        if let Some(details) = &tx_detail.details {
13036            tracing::debug!(
13037                "[Treasury Address Task] Scanning {} transaction detail entries",
13038                details.len()
13039            );
13040
13041            for (index, detail) in details.iter().enumerate() {
13042                if let (Some(category), Some(asset_id), Some(address)) = (
13043                    detail.get("category").and_then(|v| v.as_str()),
13044                    detail.get("asset").and_then(|v| v.as_str()),
13045                    detail.get("address").and_then(|v| v.as_str()),
13046                ) {
13047                    if category == "receive" && asset_id == reissuance_token_id {
13048                        tracing::info!(
13049                            "[Treasury Address Task] Found reissuance token receive address at index {}: {}",
13050                            index,
13051                            address
13052                        );
13053                        change_address = Some(address.to_string());
13054                        break;
13055                    }
13056                }
13057            }
13058        }
13059
13060        if let Some(address) = change_address {
13061            tracing::info!(
13062                "[Treasury Address Task] Extracted change address: {}",
13063                address
13064            );
13065
13066            // Check if address is already in treasury addresses
13067            let treasury_addresses = self
13068                .get_asset_treasury_addresses(asset_uuid)
13069                .await
13070                .map_err(|e| {
13071                    tracing::error!(
13072                        "[Treasury Address Task] Failed to get treasury addresses: {}",
13073                        e
13074                    );
13075                    AmpError::api(format!("Failed to get treasury addresses: {e}"))
13076                })?;
13077
13078            if treasury_addresses.contains(&address) {
13079                tracing::info!(
13080                    "[Treasury Address Task] Address {} is already in treasury addresses list",
13081                    address
13082                );
13083                return Ok(());
13084            }
13085
13086            // Submit address to treasury addresses
13087            tracing::debug!(
13088                "[Treasury Address Task] Submitting address {} to treasury addresses",
13089                address
13090            );
13091
13092            self.add_asset_treasury_addresses(asset_uuid, std::slice::from_ref(&address))
13093                .await
13094                .map_err(|e| {
13095                    tracing::error!(
13096                        "[Treasury Address Task] Failed to add treasury address: {}",
13097                        e
13098                    );
13099                    AmpError::api(format!("Failed to add treasury address: {e}"))
13100                })?;
13101
13102            tracing::info!(
13103                "[Treasury Address Task] Successfully added {} to treasury addresses for asset {}",
13104                address,
13105                asset_uuid
13106            );
13107
13108            Ok(())
13109        } else {
13110            tracing::warn!(
13111                "[Treasury Address Task] Could not find reissuance token change address in transaction {}. \
13112                This may be normal if all reissuance tokens were consumed.",
13113                txid
13114            );
13115            Err(AmpError::validation(
13116                "No reissuance token change address found".to_string(),
13117            ))
13118        }
13119    }
13120
13121    /// Burns (destroys) a specific amount of an asset
13122    ///
13123    /// This method orchestrates the complete burn workflow:
13124    /// 1. Validates input parameters (asset UUID format, amount)
13125    /// 2. Validates Elements RPC connection and signer interface
13126    /// 3. Authenticates with AMP API
13127    /// 4. Creates a burn request via the AMP API
13128    /// 5. Waits for transaction propagation and checks for lost outputs
13129    /// 6. Verifies required UTXOs are available
13130    /// 7. Verifies sufficient balance exists
13131    /// 8. Calls the Elements node's `destroyamount` RPC method
13132    /// 9. Waits for blockchain confirmations (2 confirmations minimum)
13133    /// 10. Retrieves transaction data and change information
13134    /// 11. Confirms the burn with the AMP API
13135    ///
13136    /// # Arguments
13137    /// * `asset_uuid` - The UUID of the asset to burn (must be valid UUID format)
13138    /// * `amount_to_burn` - The amount to burn (in satoshis for the asset)
13139    /// * `node_rpc` - `ElementsRpc` client for blockchain operations
13140    /// * `wallet_name` - Name of the Elements wallet containing the asset to burn
13141    /// * `signer` - Signer implementation for future support (currently not used, node RPC signs)
13142    ///
13143    /// # Returns
13144    /// Returns `Ok(())` if the burn completes successfully, or an `AmpError` if:
13145    /// - Input validation fails (invalid UUID format, invalid amount, etc.)
13146    /// - `ElementsRpc` connection cannot be established
13147    /// - Signer interface is not available
13148    /// - Authentication with AMP API fails
13149    /// - Burn request creation fails
13150    /// - Lost outputs are detected
13151    /// - Required UTXOs are not available
13152    /// - Insufficient balance exists
13153    /// - Burn transaction creation fails
13154    /// - Confirmation timeout occurs
13155    /// - Burn confirmation with AMP API fails
13156    ///
13157    /// # Examples
13158    /// ```no_run
13159    /// # use amp_rs::{ApiClient, ElementsRpc, AmpError};
13160    /// # use amp_rs::signer::LwkSoftwareSigner;
13161    /// # #[tokio::main]
13162    /// # async fn main() -> Result<(), AmpError> {
13163    /// let client = ApiClient::new().await?;
13164    /// let elements_rpc = ElementsRpc::from_env()?;
13165    /// let (_, signer) = LwkSoftwareSigner::generate_new()?;
13166    ///
13167    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
13168    /// let amount = 1000000; // 0.01 of an asset with 8 decimals
13169    /// let wallet_name = "test_wallet";
13170    ///
13171    /// client.burn_asset(asset_uuid, amount, &elements_rpc, wallet_name, &signer).await?;
13172    /// println!("Burn completed successfully");
13173    /// # Ok(())
13174    /// # }
13175    /// ```
13176    ///
13177    /// # Related Methods
13178    /// - [`burn_request`](Self::burn_request) - Create a burn request only
13179    /// - [`burn_confirm`](Self::burn_confirm) - Confirm a burn transaction only
13180    #[allow(clippy::cognitive_complexity, clippy::too_many_lines)]
13181    pub async fn burn_asset(
13182        &self,
13183        asset_uuid: &str,
13184        amount_to_burn: i64,
13185        node_rpc: &ElementsRpc,
13186        wallet_name: &str,
13187        signer: &dyn Signer,
13188    ) -> Result<(), AmpError> {
13189        let burn_span = tracing::info_span!(
13190            "burn_asset",
13191            asset_uuid = %asset_uuid,
13192            amount_to_burn = amount_to_burn
13193        );
13194        let _enter = burn_span.enter();
13195
13196        tracing::info!(
13197            "Starting asset burn workflow for asset: {} with amount: {}",
13198            asset_uuid,
13199            amount_to_burn
13200        );
13201
13202        // Step 1: Input validation - asset_uuid format
13203        tracing::debug!("Step 1: Validating asset UUID format");
13204        Self::validate_asset_uuid(asset_uuid).map_err(|e| {
13205            let error = AmpError::validation(format!("Invalid asset UUID: {e}"));
13206            tracing::error!("Asset UUID validation failed: {}", e);
13207            error.with_context("Step 1: Asset UUID validation")
13208        })?;
13209        tracing::debug!("Asset UUID validation passed");
13210
13211        // Step 2: Input validation - amount
13212        tracing::debug!("Step 2: Validating burn amount");
13213        if amount_to_burn <= 0 {
13214            let error = AmpError::validation("Amount to burn must be positive".to_string());
13215            tracing::error!("Amount validation failed: amount must be positive");
13216            return Err(error.with_context("Step 2: Amount validation"));
13217        }
13218        tracing::debug!("Amount validation passed");
13219
13220        // Step 3: Check ElementsRpc connection availability
13221        tracing::debug!("Step 3: Validating Elements RPC connection");
13222        self.validate_elements_rpc_connection(node_rpc)
13223            .await
13224            .map_err(|e| {
13225                let error = AmpError::rpc(format!("ElementsRpc connection validation failed: {e}"));
13226                tracing::error!("Elements RPC connection validation failed: {}", e);
13227                error.with_context("Step 3: Elements RPC connection validation")
13228            })?;
13229        tracing::debug!("Elements RPC connection validation passed");
13230
13231        // Step 4: Check signer interface availability (for future support)
13232        tracing::debug!("Step 4: Validating signer interface");
13233        self.validate_signer_interface(signer).await.map_err(|e| {
13234            let error = AmpError::validation(format!("Signer interface validation failed: {e}"));
13235            tracing::error!("Signer interface validation failed: {}", e);
13236            error.with_context("Step 4: Signer interface validation")
13237        })?;
13238        tracing::debug!("Signer interface validation passed");
13239
13240        tracing::info!("✓ All input validations completed successfully");
13241
13242        // Step 5: Authenticate with AMP API using existing TokenManager
13243        tracing::debug!("Step 5: Authenticating with AMP API");
13244        let _token = self.token_strategy.get_token().await.map_err(|e| {
13245            tracing::error!("AMP API authentication failed: {}", e);
13246            let amp_error = AmpError::Existing(e);
13247            if amp_error.is_retryable() {
13248                if let Some(instructions) = amp_error.retry_instructions() {
13249                    tracing::warn!("Retry instructions: {}", instructions);
13250                }
13251            }
13252            amp_error.with_context("Step 5: AMP API authentication")
13253        })?;
13254        tracing::info!("✓ Successfully authenticated with AMP API");
13255
13256        // Step 6: Create burn request and parse response data
13257        tracing::debug!(
13258            "Step 6: Creating burn request with amount {}",
13259            amount_to_burn
13260        );
13261        let burn_response = self
13262            .burn_request(asset_uuid, amount_to_burn)
13263            .await
13264            .map_err(|e| {
13265                tracing::error!("Burn request creation failed: {}", e);
13266                if e.is_retryable() {
13267                    if let Some(instructions) = e.retry_instructions() {
13268                        tracing::warn!("Retry instructions: {}", instructions);
13269                    }
13270                }
13271                e.with_context("Step 6: Burn request creation")
13272            })?;
13273
13274        tracing::info!(
13275            "✓ Burn request created successfully: asset_id={}, amount={}",
13276            burn_response.asset_id,
13277            burn_response.amount
13278        );
13279
13280        // Step 7: Verify Elements node status
13281        tracing::debug!("Step 7: Verifying Elements node status");
13282        let (network_info, blockchain_info) = node_rpc.get_node_status().await.map_err(|e| {
13283            tracing::error!("Elements node status verification failed: {}", e);
13284            if e.is_retryable() {
13285                if let Some(instructions) = e.retry_instructions() {
13286                    tracing::warn!("Retry instructions: {}", instructions);
13287                }
13288            }
13289            e.with_context("Step 7: Elements node status verification")
13290        })?;
13291
13292        tracing::info!(
13293            "✓ Elements node verified - chain: {}, blocks: {}, connections: {}",
13294            blockchain_info.chain,
13295            blockchain_info.blocks,
13296            network_info.connections
13297        );
13298
13299        // Step 8: Wait for transaction propagation (60 seconds as per Python script)
13300        tracing::debug!("Step 8: Waiting for transaction propagation (60 seconds)");
13301        tracing::info!("Waiting 60 seconds for transaction propagation...");
13302        tokio::time::sleep(tokio::time::Duration::from_secs(60)).await;
13303        tracing::debug!("Transaction propagation wait completed");
13304
13305        // Step 9: Check for lost outputs
13306        tracing::debug!("Step 9: Checking for lost outputs");
13307        let balance_response: serde_json::Value = self
13308            .request_json(Method::GET, &["assets", asset_uuid, "balance"], None::<&()>)
13309            .await
13310            .map_err(|e| {
13311                tracing::error!("Failed to check lost outputs: {}", e);
13312                AmpError::api(format!("Balance check failed: {e}"))
13313                    .with_context("Step 9: Lost outputs check")
13314            })?;
13315
13316        // Check if lost_outputs field exists and is not empty
13317        if let Some(lost_outputs) = balance_response.get("lost_outputs") {
13318            if let Some(lost_outputs_array) = lost_outputs.as_array() {
13319                if !lost_outputs_array.is_empty() {
13320                    let error_msg = format!(
13321                        "Lost outputs detected: {}. Transaction will not be sent.",
13322                        serde_json::to_string(&lost_outputs_array).unwrap_or_default()
13323                    );
13324                    tracing::error!("{}", error_msg);
13325                    return Err(AmpError::api(error_msg).with_context("Step 9: Lost outputs check"));
13326                }
13327            }
13328        }
13329
13330        tracing::info!("✓ No lost outputs detected");
13331
13332        // Step 10: Check UTXOs match expected UTXOs from response
13333        tracing::debug!(
13334            "Step 10: Verifying {} required UTXOs are available",
13335            burn_response.utxos.len()
13336        );
13337
13338        let available_utxos = node_rpc.list_unspent(None).await.map_err(|e| {
13339            tracing::error!("Failed to list UTXOs: {}", e);
13340            AmpError::rpc(format!("Failed to list UTXOs: {e}"))
13341                .with_context("Step 10: UTXO verification")
13342        })?;
13343
13344        // Check that all required UTXOs are available
13345        let local_utxos: std::collections::HashSet<(String, i64)> = available_utxos
13346            .iter()
13347            .map(|utxo| (utxo.txid.clone(), i64::from(utxo.vout)))
13348            .collect();
13349
13350        let mut missing_utxos = Vec::new();
13351        for required_utxo in &burn_response.utxos {
13352            if !local_utxos.contains(&(required_utxo.txid.clone(), required_utxo.vout)) {
13353                missing_utxos.push(format!("{}:{}", required_utxo.txid, required_utxo.vout));
13354            }
13355        }
13356
13357        if !missing_utxos.is_empty() {
13358            let error_msg = format!(
13359                "Missing required UTXOs: {}. Ensure the asset UTXOs are available in the wallet.",
13360                missing_utxos.join(", ")
13361            );
13362            tracing::error!("{}", error_msg);
13363            return Err(AmpError::rpc(error_msg).with_context("Step 10: UTXO verification"));
13364        }
13365
13366        tracing::info!(
13367            "✓ All {} required UTXOs are available",
13368            burn_response.utxos.len()
13369        );
13370
13371        // Step 11: Check local balance >= requested amount
13372        tracing::debug!("Step 11: Verifying sufficient balance");
13373        let balances = node_rpc.get_balance(None).await.map_err(|e| {
13374            tracing::error!("Failed to get balance: {}", e);
13375            AmpError::rpc(format!("Failed to get balance: {e}"))
13376                .with_context("Step 11: Balance verification")
13377        })?;
13378
13379        // Extract balance for the specific asset_id (getbalance returns a map)
13380        let local_amount = balances
13381            .get(&burn_response.asset_id)
13382            .and_then(serde_json::Value::as_f64)
13383            .unwrap_or(0.0);
13384        let requested_amount = burn_response.amount;
13385
13386        if local_amount < requested_amount {
13387            let error_msg = format!(
13388                "Insufficient balance: local balance ({local_amount}) is lower than requested amount ({requested_amount})"
13389            );
13390            tracing::error!("{}", error_msg);
13391            return Err(AmpError::rpc(error_msg).with_context("Step 11: Balance verification"));
13392        }
13393
13394        tracing::info!(
13395            "✓ Sufficient balance verified: local={}, requested={}",
13396            local_amount,
13397            requested_amount
13398        );
13399
13400        // Step 12: Call Elements node's destroyamount RPC method
13401        tracing::debug!("Step 12: Calling Elements destroyamount RPC method");
13402        let txid = node_rpc
13403            .destroyamount(&burn_response.asset_id, requested_amount)
13404            .await
13405            .map_err(|e| {
13406                tracing::error!("Burn transaction creation failed: {}", e);
13407                if e.is_retryable() {
13408                    if let Some(instructions) = e.retry_instructions() {
13409                        tracing::warn!("Retry instructions: {}", instructions);
13410                    }
13411                }
13412                e.with_context("Step 12: Burn transaction creation")
13413            })?;
13414
13415        tracing::info!("✓ Burn transaction created: txid={}", txid);
13416
13417        // Step 13: Wait for confirmations
13418        tracing::debug!("Step 13: Waiting for blockchain confirmations (minimum 2 confirmations, 10-minute timeout)");
13419        let confirmation_start = std::time::Instant::now();
13420        let _tx_detail = node_rpc
13421            .wait_for_confirmations(&txid, Some(2), Some(10))
13422            .await
13423            .map_err(|e| {
13424                let elapsed = confirmation_start.elapsed();
13425                tracing::error!(
13426                    "Confirmation waiting failed after {:?}: {}",
13427                    elapsed,
13428                    e
13429                );
13430
13431                if let AmpError::Timeout(_) = &e {
13432                    tracing::warn!(
13433                        "Confirmation timeout - transaction {} may still be pending. \
13434                        Use this txid to manually confirm the burn if it gets confirmed later.",
13435                        txid
13436                    );
13437                    let timeout_error = AmpError::timeout(format!(
13438                        "Confirmation timeout for txid: {txid}. Use this txid to manually confirm the burn."
13439                    ));
13440                    timeout_error.with_context("Step 13: Confirmation waiting")
13441                } else {
13442                    if e.is_retryable() {
13443                        if let Some(instructions) = e.retry_instructions() {
13444                            tracing::warn!("Retry instructions: {}", instructions);
13445                        }
13446                    }
13447                    e.with_context(format!("Step 13: Confirmation waiting for txid: {txid}"))
13448                }
13449            })?;
13450
13451        tracing::info!("✓ Transaction confirmed with at least 2 confirmations");
13452
13453        // Step 14: Get transaction data and change data
13454        tracing::debug!("Step 14: Retrieving transaction data and change information");
13455
13456        // Get transaction details (we only need txid for tx_data)
13457        let tx_data = serde_json::json!({
13458            "txid": txid
13459        });
13460
13461        // Get change_data from listunspent with blinding data filtered by asset_id and txid
13462        // We need to use list_unspent_with_blinding_data to get amountblinder and assetblinder fields
13463        // required by the AMP API
13464        let all_unspent = node_rpc
13465            .list_unspent_with_blinding_data(wallet_name)
13466            .await
13467            .map_err(|e| {
13468                tracing::error!("Failed to list unspent outputs with blinding data: {}", e);
13469                AmpError::rpc(format!(
13470                    "Failed to list unspent outputs with blinding data: {e}"
13471                ))
13472                .with_context("Step 14: Change data retrieval")
13473            })?;
13474
13475        // Filter and convert to JSON values, preserving all fields including blinding data
13476        let change_data: Vec<serde_json::Value> = all_unspent
13477            .into_iter()
13478            .filter(|utxo| utxo.asset == burn_response.asset_id && utxo.txid == txid)
13479            .map(|utxo| {
13480                // Serialize the full Unspent struct to JSON to include all fields
13481                // including amountblinder and assetblinder which are required by the API
13482                serde_json::to_value(&utxo).unwrap_or_else(|e| {
13483                    tracing::warn!("Failed to serialize UTXO to JSON: {}", e);
13484                    // Fallback to manual construction if serialization fails
13485                    serde_json::json!({
13486                        "txid": utxo.txid,
13487                        "vout": utxo.vout,
13488                        "address": utxo.address,
13489                        "amount": utxo.amount,
13490                        "asset": utxo.asset,
13491                        "spendable": utxo.spendable,
13492                        "amountblinder": utxo.amountblinder,
13493                        "assetblinder": utxo.assetblinder
13494                    })
13495                })
13496            })
13497            .collect();
13498
13499        tracing::info!(
13500            "✓ Retrieved transaction data and {} change output(s) for txid {}",
13501            change_data.len(),
13502            txid
13503        );
13504
13505        // Step 15: Confirm burn with AMP API
13506        tracing::debug!("Step 15: Confirming burn with AMP API");
13507
13508        self.burn_confirm(asset_uuid, tx_data, change_data)
13509            .await
13510            .map_err(|e| {
13511                tracing::error!("Burn confirmation failed: {}", e);
13512
13513                // For confirmation failures, always provide retry instructions with txid
13514                let confirmation_error = AmpError::api(format!(
13515                    "Failed to confirm burn: {e}. \
13516                IMPORTANT: Transaction {txid} was successful on blockchain. \
13517                Use this txid to manually retry confirmation."
13518                ));
13519
13520                if e.is_retryable() {
13521                    if let Some(instructions) = e.retry_instructions() {
13522                        tracing::warn!("Retry instructions: {}", instructions);
13523                    }
13524                }
13525
13526                confirmation_error.with_context("Step 15: Burn confirmation")
13527            })?;
13528
13529        tracing::info!(
13530            "🎉 Asset burn completed successfully for asset: {} with transaction: {}",
13531            asset_uuid,
13532            txid
13533        );
13534
13535        Ok(())
13536    }
13537
13538    /// Validates the asset UUID format
13539    ///
13540    /// Ensures the asset UUID follows the standard UUID format (8-4-4-4-12 hexadecimal digits)
13541    ///
13542    /// # Arguments
13543    /// * `asset_uuid` - The asset UUID string to validate
13544    ///
13545    /// # Returns
13546    /// Returns `Ok(())` if valid, or an error describing the validation failure
13547    ///
13548    /// # Errors
13549    /// - Empty or whitespace-only UUID
13550    /// - Invalid UUID format (not matching standard UUID pattern)
13551    /// - UUID contains invalid characters
13552    fn validate_asset_uuid(asset_uuid: &str) -> Result<(), String> {
13553        if asset_uuid.trim().is_empty() {
13554            return Err("Asset UUID cannot be empty".to_string());
13555        }
13556
13557        // Basic UUID format validation (8-4-4-4-12 pattern)
13558        // Expected format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
13559        let parts: Vec<&str> = asset_uuid.split('-').collect();
13560        if parts.len() != 5 {
13561            return Err(format!(
13562                "Asset UUID '{asset_uuid}' does not have 5 parts separated by hyphens"
13563            ));
13564        }
13565
13566        // Check each part has the correct length and contains only hex characters
13567        let expected_lengths = [8, 4, 4, 4, 12];
13568        for (i, (part, &expected_len)) in parts.iter().zip(expected_lengths.iter()).enumerate() {
13569            if part.len() != expected_len {
13570                return Err(format!(
13571                    "Asset UUID part {} has length {} but expected {}",
13572                    i + 1,
13573                    part.len(),
13574                    expected_len
13575                ));
13576            }
13577
13578            // Check if all characters are valid hexadecimal
13579            if !part.chars().all(|c| c.is_ascii_hexdigit()) {
13580                return Err(format!(
13581                    "Asset UUID part {} contains non-hexadecimal characters: '{}'",
13582                    i + 1,
13583                    part
13584                ));
13585            }
13586        }
13587
13588        tracing::debug!("Asset UUID validation passed: {}", asset_uuid);
13589        Ok(())
13590    }
13591
13592    /// Validates the assignments data structure
13593    ///
13594    /// Ensures assignments vector is not empty and each assignment has valid data
13595    ///
13596    /// # Arguments
13597    /// * `assignments` - Vector of assignments to validate
13598    ///
13599    /// # Returns
13600    /// Returns `Ok(())` if valid, or an error describing the validation failure
13601    ///
13602    /// # Errors
13603    /// - Empty assignments vector
13604    /// - Assignment with empty `user_id`
13605    /// - Assignment with empty address
13606    /// - Assignment with non-positive amount
13607    /// - Assignment with invalid address format
13608    #[allow(clippy::cognitive_complexity)]
13609    fn validate_assignments(assignments: &[AssetDistributionAssignment]) -> Result<(), String> {
13610        tracing::debug!("Validating {} assignments", assignments.len());
13611
13612        if assignments.is_empty() {
13613            tracing::error!("Assignments validation failed: empty assignments vector");
13614            return Err("Assignments vector cannot be empty".to_string());
13615        }
13616
13617        let mut total_amount = 0.0;
13618        let mut unique_addresses = std::collections::HashSet::new();
13619        let mut unique_users = std::collections::HashSet::new();
13620
13621        for (index, assignment) in assignments.iter().enumerate() {
13622            tracing::trace!(
13623                "Validating assignment {}: user_id={}, address={}, amount={}",
13624                index,
13625                assignment.user_id,
13626                assignment.address,
13627                assignment.amount
13628            );
13629
13630            // Validate user_id
13631            if assignment.user_id.trim().is_empty() {
13632                tracing::error!("Assignment {} validation failed: empty user_id", index);
13633                return Err(format!("Assignment {index} has empty user_id"));
13634            }
13635
13636            // Validate address
13637            if assignment.address.trim().is_empty() {
13638                tracing::error!("Assignment {} validation failed: empty address", index);
13639                return Err(format!("Assignment {index} has empty address"));
13640            }
13641
13642            // Basic address format validation (should start with appropriate prefix for Liquid)
13643            if !assignment.address.starts_with("lq")
13644                && !assignment.address.starts_with("vj")
13645                && !assignment.address.starts_with("VJ")
13646                && !assignment.address.starts_with("VT")
13647            {
13648                tracing::error!(
13649                    "Assignment {} validation failed: invalid address format '{}' (should start with 'lq', 'vj', 'VJ', or 'VT')",
13650                    index, assignment.address
13651                );
13652                return Err(format!(
13653                    "Assignment {} has invalid address format: '{}' (should start with 'lq', 'vj', 'VJ', or 'VT')",
13654                    index, assignment.address
13655                ));
13656            }
13657
13658            // Validate amount
13659            if assignment.amount <= 0.0 {
13660                tracing::error!(
13661                    "Assignment {} validation failed: non-positive amount {}",
13662                    index,
13663                    assignment.amount
13664                );
13665                return Err(format!(
13666                    "Assignment {} has non-positive amount: {}",
13667                    index, assignment.amount
13668                ));
13669            }
13670
13671            // Check for reasonable amount limits (prevent overflow issues)
13672            if assignment.amount > 21_000_000.0 {
13673                tracing::error!(
13674                    "Assignment {} validation failed: unreasonably large amount {} (max: 21,000,000)",
13675                    index, assignment.amount
13676                );
13677                return Err(format!(
13678                    "Assignment {} has unreasonably large amount: {} (max: 21,000,000)",
13679                    index, assignment.amount
13680                ));
13681            }
13682
13683            // Check for precision issues (more than 8 decimal places)
13684            let amount_str = format!("{:.8}", assignment.amount);
13685            if amount_str.len() > 20 {
13686                // Reasonable length check
13687                tracing::warn!(
13688                    "Assignment {} has high precision amount: {} - may cause precision issues",
13689                    index,
13690                    assignment.amount
13691                );
13692            }
13693
13694            // Track duplicates for warnings
13695            if !unique_addresses.insert(&assignment.address) {
13696                tracing::warn!(
13697                    "Assignment {} uses duplicate address: {} (this may be intentional)",
13698                    index,
13699                    assignment.address
13700                );
13701            }
13702
13703            if !unique_users.insert(&assignment.user_id) {
13704                tracing::warn!(
13705                    "Assignment {} uses duplicate user_id: {} (this may be intentional)",
13706                    index,
13707                    assignment.user_id
13708                );
13709            }
13710
13711            total_amount += assignment.amount;
13712        }
13713
13714        tracing::debug!(
13715            "Assignments validation passed - {} assignments, total amount: {}, unique addresses: {}, unique users: {}",
13716            assignments.len(),
13717            total_amount,
13718            unique_addresses.len(),
13719            unique_users.len()
13720        );
13721
13722        if total_amount > 100_000_000.0 {
13723            tracing::warn!(
13724                "Total distribution amount is very large: {} - ensure this is intentional",
13725                total_amount
13726            );
13727        }
13728
13729        Ok(())
13730    }
13731
13732    /// Validates `ElementsRpc` connection availability
13733    ///
13734    /// Attempts to connect to the Elements node and verify basic functionality
13735    ///
13736    /// # Arguments
13737    /// * `node_rpc` - `ElementsRpc` client to validate
13738    ///
13739    /// # Returns
13740    /// Returns `Ok(())` if connection is valid, or an error describing the failure
13741    ///
13742    /// # Errors
13743    /// - Cannot connect to Elements node
13744    /// - Node is not synchronized
13745    /// - Node version is incompatible
13746    /// - RPC authentication fails
13747    #[allow(clippy::cognitive_complexity)]
13748    async fn validate_elements_rpc_connection(&self, node_rpc: &ElementsRpc) -> Result<(), String> {
13749        tracing::debug!("Validating Elements RPC connection");
13750
13751        // Test basic connectivity by getting network info
13752        tracing::trace!("Testing Elements RPC connectivity with getnetworkinfo");
13753        let network_info = node_rpc.get_network_info().await.map_err(|e| {
13754            tracing::error!("Failed to get network info from Elements node: {}", e);
13755            format!("Failed to get network info: {e}")
13756        })?;
13757
13758        tracing::debug!(
13759            "Network info retrieved - version: {}, connections: {}, network_active: {}",
13760            network_info.version,
13761            network_info.connections,
13762            network_info.networkactive
13763        );
13764
13765        // Check if network is active
13766        if !network_info.networkactive {
13767            tracing::error!("Elements node network is not active");
13768            return Err("Elements node network is not active".to_string());
13769        }
13770
13771        // Verify we have active connections (for non-regtest environments)
13772        if network_info.connections == 0 {
13773            tracing::warn!("Elements node has no peer connections (may be regtest environment)");
13774        } else {
13775            tracing::debug!(
13776                "Elements node has {} peer connections",
13777                network_info.connections
13778            );
13779        }
13780
13781        // Test blockchain info to ensure node is operational
13782        tracing::trace!("Testing Elements RPC with getblockchaininfo");
13783        let blockchain_info = node_rpc.get_blockchain_info().await.map_err(|e| {
13784            tracing::error!("Failed to get blockchain info from Elements node: {}", e);
13785            format!("Failed to get blockchain info: {e}")
13786        })?;
13787
13788        let sync_progress = blockchain_info.verificationprogress.unwrap_or(1.0) * 100.0;
13789        tracing::debug!(
13790            "Blockchain info retrieved - chain: {}, blocks: {}, sync_progress: {:.2}%",
13791            blockchain_info.chain,
13792            blockchain_info.blocks,
13793            sync_progress
13794        );
13795
13796        // Check if node is still in initial block download
13797        if blockchain_info.initialblockdownload.unwrap_or(false) {
13798            tracing::error!(
13799                "Elements node is still in initial block download (sync progress: {:.2}%)",
13800                sync_progress
13801            );
13802            return Err(format!(
13803                "Elements node is still in initial block download (sync progress: {sync_progress:.2}%)"
13804            ));
13805        }
13806
13807        // Check sync progress
13808        if blockchain_info.verificationprogress.unwrap_or(1.0) < 0.99 {
13809            tracing::warn!(
13810                "Elements node may not be fully synced (sync progress: {:.2}%)",
13811                sync_progress
13812            );
13813        }
13814
13815        // Check for warnings
13816        if !network_info.warnings.is_empty() {
13817            tracing::warn!("Elements node network warnings: {}", network_info.warnings);
13818        }
13819
13820        if let Some(warnings) = &blockchain_info.warnings {
13821            if !warnings.is_empty() {
13822                tracing::warn!("Elements node blockchain warnings: {}", warnings);
13823            }
13824        }
13825
13826        tracing::debug!(
13827            "ElementsRpc connection validation passed - chain: {}, blocks: {}, connections: {}, sync: {:.2}%",
13828            blockchain_info.chain,
13829            blockchain_info.blocks,
13830            network_info.connections,
13831            sync_progress
13832        );
13833
13834        Ok(())
13835    }
13836
13837    /// Validates signer interface availability
13838    ///
13839    /// Tests the signer interface with a dummy transaction to ensure it's functional
13840    ///
13841    /// # Arguments
13842    /// * `signer` - Signer implementation to validate
13843    ///
13844    /// # Returns
13845    /// Returns `Ok(())` if signer is functional, or an error describing the failure
13846    ///
13847    /// # Errors
13848    /// - Signer interface is not responsive
13849    /// - Signer fails basic functionality test
13850    #[allow(clippy::cognitive_complexity)]
13851    async fn validate_signer_interface(&self, signer: &dyn Signer) -> Result<(), String> {
13852        tracing::debug!("Validating signer interface");
13853
13854        // Test signer with a minimal dummy transaction hex
13855        // This is a minimal Elements transaction structure that should parse but not be valid for signing
13856        let dummy_tx = "0200000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000";
13857
13858        tracing::trace!("Testing signer interface with dummy transaction");
13859
13860        // Attempt to sign the dummy transaction - we expect this to fail with a specific error
13861        // but the signer should be responsive and not panic
13862        let validation_start = std::time::Instant::now();
13863        match signer.sign_transaction(dummy_tx).await {
13864            Ok(signed_tx) => {
13865                // Unexpected success with dummy transaction - this might indicate an issue
13866                tracing::warn!(
13867                    "Signer unexpectedly succeeded with dummy transaction (returned: {} chars)",
13868                    signed_tx.len()
13869                );
13870                tracing::debug!("Signer validation passed despite unexpected success");
13871            }
13872            Err(SignerError::InvalidTransaction(msg)) => {
13873                // Expected error - signer is working and correctly identified invalid transaction
13874                tracing::debug!(
13875                    "Signer interface validation passed - correctly rejected dummy transaction: {}",
13876                    msg
13877                );
13878            }
13879            Err(SignerError::HexParse(msg)) => {
13880                // Also acceptable - signer is working and correctly identified parsing issue
13881                tracing::debug!(
13882                    "Signer interface validation passed - correctly identified hex parsing issue: {}",
13883                    msg
13884                );
13885            }
13886            Err(SignerError::Lwk(msg)) => {
13887                // LWK-specific errors might be acceptable depending on the message
13888                if msg.contains("invalid") || msg.contains("parse") || msg.contains("decode") {
13889                    tracing::debug!(
13890                        "Signer interface validation passed - LWK correctly identified invalid transaction: {}",
13891                        msg
13892                    );
13893                } else {
13894                    tracing::error!("Signer interface test failed with LWK error: {}", msg);
13895                    return Err(format!(
13896                        "Signer interface test failed with LWK error: {msg}"
13897                    ));
13898                }
13899            }
13900            Err(e) => {
13901                // Other errors might indicate signer interface issues
13902                tracing::error!("Signer interface test failed: {}", e);
13903                return Err(format!("Signer interface test failed: {e}"));
13904            }
13905        }
13906
13907        let validation_duration = validation_start.elapsed();
13908        tracing::debug!(
13909            "Signer interface validation completed in {:?}",
13910            validation_duration
13911        );
13912
13913        // Warn if signer is very slow (might indicate performance issues)
13914        if validation_duration > std::time::Duration::from_secs(5) {
13915            tracing::warn!(
13916                "Signer interface validation took {:?} - this may indicate performance issues",
13917                validation_duration
13918            );
13919        }
13920
13921        Ok(())
13922    }
13923}
13924
13925fn get_amp_api_base_url() -> Result<Url, Error> {
13926    let url_str = env::var("AMP_API_BASE_URL")
13927        .unwrap_or_else(|_| "https://amp-test.blockstream.com/api".to_string());
13928    Url::parse(&url_str).map_err(Error::from)
13929}
13930
13931/// Creates a token strategy based on automatic environment detection
13932///
13933/// This function detects the current environment and creates the appropriate strategy:
13934/// - Mock strategy for mock environments (isolated, no persistence)
13935/// - Live strategy for live environments (full token management)
13936///
13937/// # Arguments
13938/// * `mock_token` - Optional token to use for mock environments
13939///
13940/// # Errors
13941/// Returns an error if strategy creation fails
13942pub async fn create_auto_token_strategy(
13943    mock_token: Option<String>,
13944) -> Result<Box<dyn TokenStrategy>, Error> {
13945    TokenEnvironment::create_auto_strategy(mock_token).await
13946}
13947
13948/// Creates a mock token strategy with the specified token
13949///
13950/// # Arguments
13951/// * `token` - The mock token to use
13952#[must_use]
13953pub fn create_mock_token_strategy(token: String) -> Box<dyn TokenStrategy> {
13954    Box::new(MockTokenStrategy::new(token))
13955}
13956
13957/// Creates a live token strategy with default configuration
13958///
13959/// # Errors
13960/// Returns an error if the `TokenManager` cannot be initialized
13961pub async fn create_live_token_strategy() -> Result<Box<dyn TokenStrategy>, Error> {
13962    let strategy = LiveTokenStrategy::new().await?;
13963    Ok(Box::new(strategy))
13964}
13965
13966/// Creates a token strategy for the specified environment
13967///
13968/// # Arguments
13969/// * `environment` - The target environment
13970/// * `mock_token` - Optional token to use for mock environments
13971///
13972/// # Errors
13973/// Returns an error if strategy creation fails
13974pub async fn create_token_strategy_for_environment(
13975    environment: TokenEnvironment,
13976    mock_token: Option<String>,
13977) -> Result<Box<dyn TokenStrategy>, Error> {
13978    environment.create_strategy(mock_token).await
13979}
13980
13981#[cfg(test)]
13982mod tests {
13983    use super::*;
13984    use crate::signer::LwkSoftwareSigner;
13985    use tokio;
13986
13987    #[tokio::test]
13988    async fn test_mock_token_strategy_basic_functionality() {
13989        let mock_token = "mock_token_12_345".to_string();
13990        let strategy = MockTokenStrategy::new(mock_token.clone());
13991
13992        // Test get_token returns the mock token
13993        let result = strategy.get_token().await;
13994        assert!(result.is_ok());
13995        assert_eq!(result.unwrap(), mock_token);
13996
13997        // Test strategy type identification
13998        assert_eq!(strategy.strategy_type(), "mock");
13999
14000        // Test persistence is disabled
14001        assert!(!strategy.should_persist());
14002
14003        // Test clear_token is a no-op (should not fail)
14004        let clear_result = strategy.clear_token().await;
14005        assert!(clear_result.is_ok());
14006
14007        // Verify token is still available after clear (since it's a no-op for mock)
14008        let token_after_clear = strategy.get_token().await;
14009        assert!(token_after_clear.is_ok());
14010        assert_eq!(token_after_clear.unwrap(), mock_token);
14011    }
14012
14013    #[tokio::test]
14014    async fn test_mock_token_strategy_isolation() {
14015        let token1 = "token_instance_1".to_string();
14016        let token2 = "token_instance_2".to_string();
14017
14018        let strategy1 = MockTokenStrategy::new(token1.clone());
14019        let strategy2 = MockTokenStrategy::new(token2.clone());
14020
14021        // Test that different instances are isolated
14022        let result1 = strategy1.get_token().await.unwrap();
14023        let result2 = strategy2.get_token().await.unwrap();
14024
14025        assert_eq!(result1, token1);
14026        assert_eq!(result2, token2);
14027        assert_ne!(result1, result2);
14028
14029        // Test that operations on one don't affect the other
14030        let _ = strategy1.clear_token().await;
14031        let result2_after_clear = strategy2.get_token().await.unwrap();
14032        assert_eq!(result2_after_clear, token2);
14033    }
14034
14035    #[tokio::test]
14036    async fn test_live_token_strategy_creation() {
14037        // Test creating a live strategy with global instance
14038        let strategy_result = LiveTokenStrategy::new().await;
14039        assert!(strategy_result.is_ok());
14040
14041        let strategy = strategy_result.unwrap();
14042        assert_eq!(strategy.strategy_type(), "live");
14043        assert!(strategy.should_persist());
14044    }
14045
14046    #[tokio::test]
14047    async fn test_live_token_strategy_with_custom_manager() {
14048        // Create a custom token manager for testing
14049        let config = RetryConfig::for_tests();
14050        let base_url = Url::parse("http://localhost:8080").unwrap();
14051        let mock_token = "test_live_token".to_string();
14052
14053        let token_manager =
14054            Arc::new(TokenManager::with_mock_token(config, base_url, mock_token.clone()).unwrap());
14055
14056        let strategy = LiveTokenStrategy::with_token_manager(token_manager);
14057
14058        // Test strategy properties
14059        assert_eq!(strategy.strategy_type(), "live");
14060        assert!(strategy.should_persist());
14061
14062        // Test token retrieval
14063        let token_result = strategy.get_token().await;
14064        assert!(token_result.is_ok());
14065        assert_eq!(token_result.unwrap(), mock_token);
14066    }
14067
14068    #[tokio::test]
14069    async fn test_live_token_strategy_clear_token() {
14070        // Create a live strategy with a mock token manager
14071        let config = RetryConfig::for_tests();
14072        let base_url = Url::parse("http://localhost:8080").unwrap();
14073        let mock_token = "test_clear_token".to_string();
14074
14075        let token_manager =
14076            Arc::new(TokenManager::with_mock_token(config, base_url, mock_token.clone()).unwrap());
14077
14078        let strategy = LiveTokenStrategy::with_token_manager(token_manager);
14079
14080        // Verify token is available initially
14081        let initial_token = strategy.get_token().await;
14082        assert!(initial_token.is_ok());
14083        assert_eq!(initial_token.unwrap(), mock_token);
14084
14085        // Clear the token
14086        let clear_result = strategy.clear_token().await;
14087        assert!(clear_result.is_ok());
14088
14089        // Note: After clearing, the TokenManager would try to obtain a new token
14090        // In a real scenario, this would fail without proper credentials
14091        // But our mock token manager will still return the same token
14092    }
14093
14094    #[tokio::test]
14095    async fn test_strategy_type_identification() {
14096        let mock_strategy = MockTokenStrategy::new("test_token".to_string());
14097        let live_strategy = LiveTokenStrategy::new().await.unwrap();
14098
14099        // Test that we can identify strategy types for debugging
14100        assert_eq!(mock_strategy.strategy_type(), "mock");
14101        assert_eq!(live_strategy.strategy_type(), "live");
14102
14103        // Test persistence settings
14104        assert!(!mock_strategy.should_persist());
14105        assert!(live_strategy.should_persist());
14106    }
14107
14108    #[tokio::test]
14109    async fn test_strategy_debug_formatting() {
14110        let mock_strategy = MockTokenStrategy::new("debug_test_token".to_string());
14111        let debug_output = format!("{mock_strategy:?}");
14112
14113        // Verify debug output contains expected information
14114        assert!(debug_output.contains("MockTokenStrategy"));
14115        assert!(debug_output.contains("debug_test_token"));
14116    }
14117
14118    // Environment Detection Tests
14119
14120    #[test]
14121    fn test_token_environment_detect_live_via_amp_tests() {
14122        // Set up environment for live test detection
14123        env::set_var("AMP_TESTS", "live");
14124        env::set_var("AMP_USERNAME", "real_user");
14125        env::set_var("AMP_PASSWORD", "real_pass");
14126        env::remove_var("AMP_API_BASE_URL");
14127
14128        let environment = TokenEnvironment::detect();
14129        assert_eq!(environment, TokenEnvironment::Live);
14130
14131        // Clean up
14132        env::remove_var("AMP_TESTS");
14133        env::remove_var("AMP_USERNAME");
14134        env::remove_var("AMP_PASSWORD");
14135    }
14136
14137    #[test]
14138    fn test_token_environment_detect_mock_via_credentials() {
14139        // Set up environment for mock detection via username
14140        env::remove_var("AMP_TESTS");
14141        env::set_var("AMP_USERNAME", "mock_user");
14142        env::set_var("AMP_PASSWORD", "real_pass");
14143        env::remove_var("AMP_API_BASE_URL");
14144
14145        let environment = TokenEnvironment::detect();
14146        assert_eq!(environment, TokenEnvironment::Mock);
14147
14148        // Test mock detection via password
14149        env::set_var("AMP_USERNAME", "real_user");
14150        env::set_var("AMP_PASSWORD", "mock_pass");
14151
14152        let environment = TokenEnvironment::detect();
14153        assert_eq!(environment, TokenEnvironment::Mock);
14154
14155        // Clean up
14156        env::remove_var("AMP_USERNAME");
14157        env::remove_var("AMP_PASSWORD");
14158    }
14159
14160    #[test]
14161    fn test_token_environment_detect_mock_via_base_url() {
14162        // Set up environment for mock detection via localhost URL
14163        env::remove_var("AMP_TESTS");
14164        env::set_var("AMP_USERNAME", "real_user");
14165        env::set_var("AMP_PASSWORD", "real_pass");
14166        env::set_var("AMP_API_BASE_URL", "http://localhost:8080/api");
14167
14168        let environment = TokenEnvironment::detect();
14169        assert_eq!(environment, TokenEnvironment::Mock);
14170
14171        // Test with 127.0.0.1
14172        env::set_var("AMP_API_BASE_URL", "http://127.0.0.1:3000/api");
14173        let environment = TokenEnvironment::detect();
14174        assert_eq!(environment, TokenEnvironment::Mock);
14175
14176        // Test with mock in URL
14177        env::set_var("AMP_API_BASE_URL", "http://mock-server.example.com/api");
14178        let environment = TokenEnvironment::detect();
14179        assert_eq!(environment, TokenEnvironment::Mock);
14180
14181        // Clean up
14182        env::remove_var("AMP_USERNAME");
14183        env::remove_var("AMP_PASSWORD");
14184        env::remove_var("AMP_API_BASE_URL");
14185    }
14186
14187    #[test]
14188    fn test_token_environment_detect_live_via_real_credentials() {
14189        // Set up environment for live detection via real credentials
14190        env::remove_var("AMP_TESTS");
14191        env::set_var("AMP_USERNAME", "real_user");
14192        env::set_var("AMP_PASSWORD", "real_pass");
14193        env::set_var("AMP_API_BASE_URL", "https://amp-test.blockstream.com/api");
14194
14195        let environment = TokenEnvironment::detect();
14196        assert_eq!(environment, TokenEnvironment::Live);
14197
14198        // Clean up
14199        env::remove_var("AMP_USERNAME");
14200        env::remove_var("AMP_PASSWORD");
14201        env::remove_var("AMP_API_BASE_URL");
14202    }
14203
14204    #[test]
14205    fn test_token_environment_detect_mock_fallback() {
14206        // Set up environment with no credentials (fallback to mock)
14207        env::remove_var("AMP_TESTS");
14208        env::remove_var("AMP_USERNAME");
14209        env::remove_var("AMP_PASSWORD");
14210        env::remove_var("AMP_API_BASE_URL");
14211
14212        let environment = TokenEnvironment::detect();
14213        assert_eq!(environment, TokenEnvironment::Mock);
14214    }
14215
14216    #[test]
14217    fn test_has_mock_credentials() {
14218        // Test mock username detection
14219        assert!(TokenEnvironment::has_mock_credentials(
14220            "mock_user",
14221            "real_pass",
14222            ""
14223        ));
14224        assert!(TokenEnvironment::has_mock_credentials(
14225            "Mock_User",
14226            "real_pass",
14227            ""
14228        ));
14229        assert!(TokenEnvironment::has_mock_credentials(
14230            "user_mock",
14231            "real_pass",
14232            ""
14233        ));
14234
14235        // Test mock password detection
14236        assert!(TokenEnvironment::has_mock_credentials(
14237            "real_user",
14238            "mock_pass",
14239            ""
14240        ));
14241        assert!(TokenEnvironment::has_mock_credentials(
14242            "real_user",
14243            "Mock_Pass",
14244            ""
14245        ));
14246        assert!(TokenEnvironment::has_mock_credentials(
14247            "real_user",
14248            "pass_mock",
14249            ""
14250        ));
14251
14252        // Test mock URL detection
14253        assert!(TokenEnvironment::has_mock_credentials(
14254            "real_user",
14255            "real_pass",
14256            "http://localhost:8080"
14257        ));
14258        assert!(TokenEnvironment::has_mock_credentials(
14259            "real_user",
14260            "real_pass",
14261            "http://127.0.0.1:3000"
14262        ));
14263        assert!(TokenEnvironment::has_mock_credentials(
14264            "real_user",
14265            "real_pass",
14266            "http://mock-server.com"
14267        ));
14268        assert!(TokenEnvironment::has_mock_credentials(
14269            "real_user",
14270            "real_pass",
14271            "http://Mock-Server.com"
14272        ));
14273
14274        // Test non-mock credentials
14275        assert!(!TokenEnvironment::has_mock_credentials(
14276            "real_user",
14277            "real_pass",
14278            "https://amp-test.blockstream.com"
14279        ));
14280        assert!(!TokenEnvironment::has_mock_credentials("", "", ""));
14281    }
14282
14283    #[test]
14284    fn test_token_environment_should_persist_tokens() {
14285        assert!(!TokenEnvironment::Mock.should_persist_tokens());
14286        assert!(TokenEnvironment::Live.should_persist_tokens());
14287
14288        // Auto should delegate to detect()
14289        env::set_var("AMP_TESTS", "live");
14290        assert!(TokenEnvironment::Auto.should_persist_tokens());
14291
14292        env::set_var("AMP_USERNAME", "mock_user");
14293        env::set_var("AMP_PASSWORD", "some_password");
14294        env::remove_var("AMP_TESTS");
14295        env::remove_var("AMP_API_BASE_URL");
14296        assert!(!TokenEnvironment::Auto.should_persist_tokens());
14297
14298        // Clean up
14299        env::remove_var("AMP_USERNAME");
14300        env::remove_var("AMP_PASSWORD");
14301    }
14302
14303    #[test]
14304    fn test_token_environment_is_mock_and_is_live() {
14305        assert!(TokenEnvironment::Mock.is_mock());
14306        assert!(!TokenEnvironment::Mock.is_live());
14307
14308        assert!(!TokenEnvironment::Live.is_mock());
14309        assert!(TokenEnvironment::Live.is_live());
14310
14311        // Auto should delegate to detect()
14312        env::set_var("AMP_USERNAME", "mock_user");
14313        env::set_var("AMP_PASSWORD", "some_password");
14314        env::remove_var("AMP_TESTS");
14315        env::remove_var("AMP_API_BASE_URL");
14316        assert!(TokenEnvironment::Auto.is_mock());
14317        assert!(!TokenEnvironment::Auto.is_live());
14318
14319        env::set_var("AMP_TESTS", "live");
14320        assert!(!TokenEnvironment::Auto.is_mock());
14321        assert!(TokenEnvironment::Auto.is_live());
14322
14323        // Clean up
14324        env::remove_var("AMP_USERNAME");
14325        env::remove_var("AMP_PASSWORD");
14326        env::remove_var("AMP_TESTS");
14327    }
14328
14329    #[tokio::test]
14330    async fn test_token_environment_create_strategy_mock() {
14331        let mock_token = "test_mock_token".to_string();
14332        let strategy = TokenEnvironment::Mock
14333            .create_strategy(Some(mock_token.clone()))
14334            .await
14335            .unwrap();
14336
14337        assert_eq!(strategy.strategy_type(), "mock");
14338        assert!(!strategy.should_persist());
14339
14340        let token = strategy.get_token().await.unwrap();
14341        assert_eq!(token, mock_token);
14342    }
14343
14344    #[tokio::test]
14345    async fn test_token_environment_create_strategy_live() {
14346        let strategy = TokenEnvironment::Live.create_strategy(None).await.unwrap();
14347
14348        assert_eq!(strategy.strategy_type(), "live");
14349        assert!(strategy.should_persist());
14350    }
14351
14352    #[tokio::test]
14353    async fn test_token_environment_create_auto_strategy() {
14354        // Test with mock environment - need both username and password for proper detection
14355        env::set_var("AMP_USERNAME", "mock_user");
14356        env::set_var("AMP_PASSWORD", "some_password");
14357        env::remove_var("AMP_TESTS");
14358        env::remove_var("AMP_API_BASE_URL");
14359
14360        let mock_token = "auto_mock_token".to_string();
14361        let strategy = TokenEnvironment::create_auto_strategy(Some(mock_token.clone()))
14362            .await
14363            .unwrap();
14364
14365        assert_eq!(strategy.strategy_type(), "mock");
14366        let token = strategy.get_token().await.unwrap();
14367        assert_eq!(token, mock_token);
14368
14369        // Clean up
14370        env::remove_var("AMP_USERNAME");
14371        env::remove_var("AMP_PASSWORD");
14372    }
14373
14374    #[tokio::test]
14375    async fn test_mock_token_strategy_factory_methods() {
14376        // Test with_default_token
14377        let strategy = MockTokenStrategy::with_default_token();
14378        assert_eq!(strategy.strategy_type(), "mock");
14379        let token = strategy.get_token().await.unwrap();
14380        assert_eq!(token, "mock_token_default");
14381
14382        // Test for_test
14383        let strategy = MockTokenStrategy::for_test("my_test");
14384        let token = strategy.get_token().await.unwrap();
14385        assert_eq!(token, "mock_token_my_test");
14386    }
14387
14388    #[tokio::test]
14389    async fn test_live_token_strategy_factory_methods() {
14390        // Test for_testing
14391        let strategy = LiveTokenStrategy::for_testing().await.unwrap();
14392        assert_eq!(strategy.strategy_type(), "live");
14393        assert!(strategy.should_persist());
14394    }
14395
14396    #[tokio::test]
14397    async fn test_standalone_factory_functions() {
14398        // Test create_mock_token_strategy
14399        let mock_token = "standalone_mock".to_string();
14400        let strategy = create_mock_token_strategy(mock_token.clone());
14401        assert_eq!(strategy.strategy_type(), "mock");
14402        let token = strategy.get_token().await.unwrap();
14403        assert_eq!(token, mock_token);
14404
14405        // Test create_live_token_strategy
14406        let strategy = create_live_token_strategy().await.unwrap();
14407        assert_eq!(strategy.strategy_type(), "live");
14408
14409        // Test create_auto_token_strategy with mock environment
14410        env::set_var("AMP_USERNAME", "mock_user");
14411        env::set_var("AMP_PASSWORD", "some_password");
14412        env::remove_var("AMP_TESTS");
14413        env::remove_var("AMP_API_BASE_URL");
14414
14415        let auto_mock_token = "auto_standalone_mock".to_string();
14416        let strategy = create_auto_token_strategy(Some(auto_mock_token.clone()))
14417            .await
14418            .unwrap();
14419        assert_eq!(strategy.strategy_type(), "mock");
14420        let token = strategy.get_token().await.unwrap();
14421        assert_eq!(token, auto_mock_token);
14422
14423        // Test create_token_strategy_for_environment
14424        let env_mock_token = "env_mock".to_string();
14425        let strategy = create_token_strategy_for_environment(
14426            TokenEnvironment::Mock,
14427            Some(env_mock_token.clone()),
14428        )
14429        .await
14430        .unwrap();
14431        assert_eq!(strategy.strategy_type(), "mock");
14432        let token = strategy.get_token().await.unwrap();
14433        assert_eq!(token, env_mock_token);
14434
14435        // Clean up
14436        env::remove_var("AMP_USERNAME");
14437        env::remove_var("AMP_PASSWORD");
14438    }
14439
14440    #[test]
14441    fn test_environment_detection_with_various_credential_combinations() {
14442        // Test case 1: AMP_TESTS=live overrides everything
14443        env::set_var("AMP_TESTS", "live");
14444        env::set_var("AMP_USERNAME", "mock_user");
14445        env::set_var("AMP_PASSWORD", "mock_pass");
14446        env::set_var("AMP_API_BASE_URL", "http://localhost:8080");
14447        assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Live);
14448
14449        // Test case 2: Mock username with real password and URL
14450        env::remove_var("AMP_TESTS");
14451        env::set_var("AMP_USERNAME", "mock_user");
14452        env::set_var("AMP_PASSWORD", "real_password");
14453        env::set_var("AMP_API_BASE_URL", "https://amp-test.blockstream.com/api");
14454        assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Mock);
14455
14456        // Test case 3: Real username with mock password
14457        env::set_var("AMP_USERNAME", "real_user");
14458        env::set_var("AMP_PASSWORD", "mock_password");
14459        env::set_var("AMP_API_BASE_URL", "https://amp-test.blockstream.com/api");
14460        assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Mock);
14461
14462        // Test case 4: Real credentials with localhost URL
14463        env::set_var("AMP_USERNAME", "real_user");
14464        env::set_var("AMP_PASSWORD", "real_password");
14465        env::set_var("AMP_API_BASE_URL", "http://localhost:3000/api");
14466        assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Mock);
14467
14468        // Test case 5: All real credentials
14469        env::set_var("AMP_USERNAME", "real_user");
14470        env::set_var("AMP_PASSWORD", "real_password");
14471        env::set_var("AMP_API_BASE_URL", "https://amp-test.blockstream.com/api");
14472        assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Live);
14473
14474        // Test case 6: Empty credentials
14475        env::remove_var("AMP_USERNAME");
14476        env::remove_var("AMP_PASSWORD");
14477        env::remove_var("AMP_API_BASE_URL");
14478        assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Mock);
14479
14480        // Test case 7: Only username set
14481        env::set_var("AMP_USERNAME", "real_user");
14482        env::remove_var("AMP_PASSWORD");
14483        assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Mock);
14484
14485        // Test case 8: Only password set
14486        env::remove_var("AMP_USERNAME");
14487        env::set_var("AMP_PASSWORD", "real_password");
14488        assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Mock);
14489
14490        // Clean up all environment variables
14491        env::remove_var("AMP_TESTS");
14492        env::remove_var("AMP_USERNAME");
14493        env::remove_var("AMP_PASSWORD");
14494        env::remove_var("AMP_API_BASE_URL");
14495    }
14496
14497    #[tokio::test]
14498    async fn test_distribute_asset_input_validation() {
14499        // Create a mock client for testing
14500        let client = ApiClient::with_mock_token(
14501            reqwest::Url::parse("http://localhost:8080/api").unwrap(),
14502            "test_token".to_string(),
14503        )
14504        .unwrap();
14505
14506        // Test invalid asset UUID
14507        let assignments = vec![AssetDistributionAssignment {
14508            user_id: "user123".to_string(),
14509            address: "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
14510            amount: 100.0,
14511        }];
14512
14513        // Create a mock ElementsRpc (this will fail connection validation, but that's expected)
14514        let elements_rpc = ElementsRpc::new(
14515            "http://localhost:18884".to_string(),
14516            "user".to_string(),
14517            "pass".to_string(),
14518        );
14519
14520        // Create a mock signer
14521        let (_, signer) = LwkSoftwareSigner::generate_new().unwrap();
14522
14523        // Test with invalid UUID format
14524        let result = client
14525            .distribute_asset(
14526                "invalid-uuid",
14527                assignments.clone(),
14528                &elements_rpc,
14529                "test_wallet",
14530                &signer,
14531            )
14532            .await;
14533
14534        assert!(result.is_err());
14535        if let Err(AmpError::Validation(msg)) = result {
14536            assert!(msg.contains("Invalid asset UUID"));
14537        } else {
14538            panic!("Expected validation error for invalid UUID");
14539        }
14540
14541        // Test with empty assignments
14542        let result = client
14543            .distribute_asset(
14544                "550e8400-e29b-41d4-a716-446655440000",
14545                vec![],
14546                &elements_rpc,
14547                "test_wallet",
14548                &signer,
14549            )
14550            .await;
14551
14552        assert!(result.is_err());
14553        if let Err(AmpError::Validation(msg)) = result {
14554            assert!(msg.contains("Invalid assignments"));
14555        } else {
14556            panic!("Expected validation error for empty assignments");
14557        }
14558    }
14559
14560    #[test]
14561    fn test_validate_asset_uuid() {
14562        let _client = ApiClient::with_mock_token(
14563            reqwest::Url::parse("http://localhost:8080/api").unwrap(),
14564            "test_token".to_string(),
14565        )
14566        .unwrap();
14567
14568        // Valid UUID
14569        assert!(ApiClient::validate_asset_uuid("550e8400-e29b-41d4-a716-446655440000").is_ok());
14570
14571        // Invalid UUIDs
14572        assert!(ApiClient::validate_asset_uuid("").is_err());
14573        assert!(ApiClient::validate_asset_uuid("invalid").is_err());
14574        assert!(ApiClient::validate_asset_uuid("550e8400-e29b-41d4-a716").is_err()); // Too short
14575        assert!(
14576            ApiClient::validate_asset_uuid("550e8400-e29b-41d4-a716-446655440000-extra").is_err()
14577        ); // Too long
14578        assert!(ApiClient::validate_asset_uuid("550e8400xe29bx41d4xa716x446655440000").is_err()); // Wrong separators
14579        assert!(ApiClient::validate_asset_uuid("550e8400-e29g-41d4-a716-446655440000").is_err());
14580        // Invalid hex char
14581    }
14582
14583    #[test]
14584    fn test_validate_assignments() {
14585        let _client = ApiClient::with_mock_token(
14586            reqwest::Url::parse("http://localhost:8080/api").unwrap(),
14587            "test_token".to_string(),
14588        )
14589        .unwrap();
14590
14591        // Valid assignments
14592        let valid_assignments = vec![AssetDistributionAssignment {
14593            user_id: "user123".to_string(),
14594            address: "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
14595            amount: 100.0,
14596        }];
14597        assert!(ApiClient::validate_assignments(&valid_assignments).is_ok());
14598
14599        // Empty assignments
14600        assert!(ApiClient::validate_assignments(&[]).is_err());
14601
14602        // Assignment with empty user_id
14603        let invalid_assignments = vec![AssetDistributionAssignment {
14604            user_id: "".to_string(),
14605            address: "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
14606            amount: 100.0,
14607        }];
14608        assert!(ApiClient::validate_assignments(&invalid_assignments).is_err());
14609
14610        // Assignment with empty address
14611        let invalid_assignments = vec![AssetDistributionAssignment {
14612            user_id: "user123".to_string(),
14613            address: "".to_string(),
14614            amount: 100.0,
14615        }];
14616        assert!(ApiClient::validate_assignments(&invalid_assignments).is_err());
14617
14618        // Assignment with invalid address format
14619        let invalid_assignments = vec![AssetDistributionAssignment {
14620            user_id: "user123".to_string(),
14621            address: "invalid_address".to_string(),
14622            amount: 100.0,
14623        }];
14624        assert!(ApiClient::validate_assignments(&invalid_assignments).is_err());
14625
14626        // Assignment with non-positive amount
14627        let invalid_assignments = vec![AssetDistributionAssignment {
14628            user_id: "user123".to_string(),
14629            address: "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
14630            amount: 0.0,
14631        }];
14632        assert!(ApiClient::validate_assignments(&invalid_assignments).is_err());
14633
14634        // Assignment with unreasonably large amount
14635        let invalid_assignments = vec![AssetDistributionAssignment {
14636            user_id: "user123".to_string(),
14637            address: "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
14638            amount: 25_000_000.0,
14639        }];
14640        assert!(ApiClient::validate_assignments(&invalid_assignments).is_err());
14641    }
14642
14643    #[test]
14644    fn test_enhanced_error_handling_and_logging() {
14645        // Test AmpError creation and context enhancement
14646        let api_error = AmpError::api("Distribution creation failed");
14647        let contextual_error = api_error.with_context("Step 6: Distribution creation");
14648
14649        match contextual_error {
14650            AmpError::Api(msg) => {
14651                assert!(msg.contains("Step 6: Distribution creation"));
14652                assert!(msg.contains("Distribution creation failed"));
14653            }
14654            _ => panic!("Expected Api error variant"),
14655        }
14656
14657        // Test retry instructions for different error types
14658        let rpc_error = AmpError::rpc("Connection failed");
14659        assert!(rpc_error.is_retryable());
14660        assert!(rpc_error.retry_instructions().is_some());
14661        assert!(rpc_error
14662            .retry_instructions()
14663            .unwrap()
14664            .contains("Elements node"));
14665
14666        let validation_error = AmpError::validation("Invalid UUID");
14667        assert!(!validation_error.is_retryable());
14668        assert!(validation_error.retry_instructions().is_none());
14669
14670        let timeout_error = AmpError::timeout("Confirmation timeout for txid abc123");
14671        assert!(!timeout_error.is_retryable());
14672        let instructions = timeout_error.retry_instructions();
14673        assert!(instructions.is_some());
14674        assert!(instructions.unwrap().contains("transaction ID"));
14675
14676        // Test error helper methods
14677        let signer_error =
14678            AmpError::Signer(crate::signer::SignerError::Lwk("Test error".to_string()));
14679        assert!(!signer_error.is_retryable());
14680        assert!(signer_error.retry_instructions().is_none());
14681
14682        // Test serialization error
14683        let json_error = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
14684        let serialization_error = AmpError::from(json_error);
14685        assert!(matches!(serialization_error, AmpError::Serialization(_)));
14686        assert!(!serialization_error.is_retryable());
14687    }
14688}