amp_rs/
client.rs

1use std::env;
2use std::sync::Arc;
3use std::time::Duration as StdDuration;
4
5use async_trait::async_trait;
6use chrono::{Duration, Utc};
7
8use reqwest::header::AUTHORIZATION;
9use reqwest::{Client, Method, Url};
10use serde::de::DeserializeOwned;
11use thiserror::Error;
12use tokio::sync::{Mutex, OnceCell, Semaphore};
13use tokio::time::sleep;
14
15use elements::encode::Decodable;
16use secrecy::ExposeSecret;
17use secrecy::Secret;
18use std::str::FromStr;
19
20use crate::model::{
21    Activity, Asset, AssetActivityParams, AssetDistributionAssignment, AssetSummary, Assignment,
22    Balance, BroadcastResponse, CategoriesRequest, CategoryAdd, CategoryEdit, CategoryResponse,
23    ChangePasswordRequest, ChangePasswordResponse, CreateAssetAssignmentRequest, EditAssetRequest,
24    GaidBalanceEntry, GaidRequest, IssuanceRequest, IssuanceResponse, Outpoint, Ownership,
25    Password, ReceivedByAddress, RegisterAssetResponse, TokenData, TokenInfo, TokenRequest,
26    TokenResponse, 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("Failed to parse AMP response: {0}")]
411    ResponseParsingFailed(String),
412    #[error("AMP token request failed with status {status}: {error_text}")]
413    TokenRequestFailed {
414        status: reqwest::StatusCode,
415        error_text: String,
416    },
417    #[error("Failed to parse url: {0}")]
418    UrlParse(#[from] url::ParseError),
419    #[error("Reqwest error: {0}")]
420    Reqwest(#[from] reqwest::Error),
421    #[error("Invalid retry configuration: {0}")]
422    InvalidRetryConfig(String),
423    #[error("Token management error: {0}")]
424    Token(#[from] TokenError),
425}
426
427/// Enhanced error enum for distribution operations and `ElementsRpc`
428#[derive(Error, Debug)]
429pub enum AmpError {
430    #[error("API error: {0}")]
431    Api(String),
432
433    #[error("RPC error: {0}")]
434    Rpc(String),
435
436    #[error("Signer error: {0}")]
437    Signer(#[from] SignerError),
438
439    #[error("Timeout waiting for confirmations: {0}")]
440    Timeout(String),
441
442    #[error("Validation error: {0}")]
443    Validation(String),
444
445    #[error("Network error: {0}")]
446    Network(#[from] reqwest::Error),
447
448    #[error("Serialization error: {0}")]
449    Serialization(#[from] serde_json::Error),
450
451    #[error(transparent)]
452    Existing(#[from] Error),
453}
454
455impl AmpError {
456    /// Creates a new API error
457    pub fn api<S: Into<String>>(message: S) -> Self {
458        Self::Api(message.into())
459    }
460
461    /// Creates a new RPC error
462    pub fn rpc<S: Into<String>>(message: S) -> Self {
463        Self::Rpc(message.into())
464    }
465
466    /// Creates a new timeout error
467    pub fn timeout<S: Into<String>>(message: S) -> Self {
468        Self::Timeout(message.into())
469    }
470
471    /// Creates a new validation error
472    pub fn validation<S: Into<String>>(message: S) -> Self {
473        Self::Validation(message.into())
474    }
475
476    /// Adds context to an error
477    #[must_use]
478    pub fn with_context<S: Into<String>>(self, context: S) -> Self {
479        let context_str = context.into();
480        match self {
481            Self::Api(msg) => Self::Api(format!("{context_str}: {msg}")),
482            Self::Rpc(msg) => Self::Rpc(format!("{context_str}: {msg}")),
483            Self::Timeout(msg) => Self::Timeout(format!("{context_str}: {msg}")),
484            Self::Validation(msg) => Self::Validation(format!("{context_str}: {msg}")),
485            other => other, // Don't modify other error types
486        }
487    }
488
489    /// Returns true if this error indicates a retryable condition
490    #[must_use]
491    pub const fn is_retryable(&self) -> bool {
492        match self {
493            Self::Network(_) | Self::Rpc(_) => true, // RPC errors might be transient
494            Self::Existing(Error::Token(token_err)) => token_err.is_retryable(),
495            _ => false,
496        }
497    }
498
499    /// Provides user-friendly retry instructions when applicable
500    #[must_use]
501    pub fn retry_instructions(&self) -> Option<String> {
502        match self {
503            Self::Network(_) => Some("Check network connection and retry".to_string()),
504            Self::Rpc(_) => Some("Check Elements node connection and retry".to_string()),
505            Self::Timeout(msg) if msg.contains("txid") => {
506                Some("Use the transaction ID to manually confirm the distribution".to_string())
507            }
508            Self::Existing(Error::Token(TokenError::RateLimited {
509                retry_after_seconds,
510            })) => Some(format!(
511                "Rate limited. Retry after {retry_after_seconds} seconds"
512            )),
513            _ => None,
514        }
515    }
516}
517
518/// Detailed error types for token management operations
519#[derive(Error, Debug, Clone, PartialEq, Eq)]
520pub enum TokenError {
521    #[error("Token refresh failed: {0}")]
522    RefreshFailed(String),
523    #[error("Token obtain failed after {attempts} attempts: {last_error}")]
524    ObtainFailed { attempts: u32, last_error: String },
525    #[error("Rate limited: retry after {retry_after_seconds} seconds")]
526    RateLimited { retry_after_seconds: u64 },
527    #[error("Request timeout after {timeout_seconds} seconds")]
528    Timeout { timeout_seconds: u64 },
529    #[error("Serialization error: {0}")]
530    Serialization(String),
531    #[error("Token storage error: {0}")]
532    Storage(String),
533    #[error("Token validation error: {0}")]
534    Validation(String),
535}
536
537impl TokenError {
538    /// Creates a new `RefreshFailed` error
539    #[must_use]
540    pub fn refresh_failed<S: Into<String>>(message: S) -> Self {
541        Self::RefreshFailed(message.into())
542    }
543
544    /// Creates a new `ObtainFailed` error
545    #[must_use]
546    pub const fn obtain_failed(attempts: u32, last_error: String) -> Self {
547        Self::ObtainFailed {
548            attempts,
549            last_error,
550        }
551    }
552
553    /// Creates a new `RateLimited` error
554    #[must_use]
555    pub const fn rate_limited(retry_after_seconds: u64) -> Self {
556        Self::RateLimited {
557            retry_after_seconds,
558        }
559    }
560
561    /// Creates a new Timeout error
562    #[must_use]
563    pub const fn timeout(timeout_seconds: u64) -> Self {
564        Self::Timeout { timeout_seconds }
565    }
566
567    /// Creates a new Serialization error
568    #[must_use]
569    pub fn serialization<S: Into<String>>(message: S) -> Self {
570        Self::Serialization(message.into())
571    }
572
573    /// Creates a new Storage error
574    #[must_use]
575    pub fn storage<S: Into<String>>(message: S) -> Self {
576        Self::Storage(message.into())
577    }
578
579    /// Creates a new Validation error
580    #[must_use]
581    pub fn validation<S: Into<String>>(message: S) -> Self {
582        Self::Validation(message.into())
583    }
584
585    /// Returns true if this error indicates a retryable condition
586    #[must_use]
587    pub const fn is_retryable(&self) -> bool {
588        matches!(
589            self,
590            Self::RefreshFailed(_) | Self::RateLimited { .. } | Self::Timeout { .. }
591        )
592    }
593
594    /// Returns true if this error indicates a rate limiting condition
595    #[must_use]
596    pub const fn is_rate_limited(&self) -> bool {
597        matches!(self, Self::RateLimited { .. })
598    }
599
600    /// Returns the retry delay in seconds if this is a rate limited error
601    #[must_use]
602    pub const fn retry_after_seconds(&self) -> Option<u64> {
603        match self {
604            Self::RateLimited {
605                retry_after_seconds,
606            } => Some(*retry_after_seconds),
607            _ => None,
608        }
609    }
610}
611
612// Conversion from serde_json::Error for serialization errors
613impl From<serde_json::Error> for TokenError {
614    fn from(err: serde_json::Error) -> Self {
615        Self::Serialization(err.to_string())
616    }
617}
618
619#[cfg(test)]
620mod amp_error_tests {
621    use super::*;
622
623    #[test]
624    fn test_amp_error_creation_helpers() {
625        let api_error = AmpError::api("Failed to create distribution");
626        assert!(matches!(api_error, AmpError::Api(_)));
627
628        let rpc_error = AmpError::rpc("Elements node connection failed");
629        assert!(matches!(rpc_error, AmpError::Rpc(_)));
630
631        let validation_error = AmpError::validation("Invalid asset UUID format");
632        assert!(matches!(validation_error, AmpError::Validation(_)));
633
634        let timeout_error = AmpError::timeout("Confirmation timeout");
635        assert!(matches!(timeout_error, AmpError::Timeout(_)));
636    }
637
638    #[test]
639    fn test_amp_error_with_context() {
640        let api_error = AmpError::api("Failed to create distribution");
641        let contextual_error = api_error.with_context("During distribution creation");
642
643        match contextual_error {
644            AmpError::Api(msg) => {
645                assert!(msg.contains("During distribution creation"));
646                assert!(msg.contains("Failed to create distribution"));
647            }
648            _ => panic!("Expected Api error variant"),
649        }
650
651        // Test that context doesn't modify errors that already have good context
652        let signer_error = AmpError::Signer(SignerError::Lwk("Test error".to_string()));
653        let contextual_signer = signer_error.with_context("Additional context");
654        assert!(matches!(contextual_signer, AmpError::Signer(_)));
655    }
656
657    #[test]
658    fn test_amp_error_retryability() {
659        let api_error = AmpError::api("Failed to create distribution");
660        assert!(!api_error.is_retryable());
661
662        let rpc_error = AmpError::rpc("Elements node connection failed");
663        assert!(rpc_error.is_retryable());
664
665        let validation_error = AmpError::validation("Invalid asset UUID format");
666        assert!(!validation_error.is_retryable());
667
668        let timeout_error = AmpError::timeout("Confirmation timeout");
669        assert!(!timeout_error.is_retryable());
670
671        let signer_error = AmpError::Signer(SignerError::Lwk("Test error".to_string()));
672        assert!(!signer_error.is_retryable());
673    }
674
675    #[test]
676    fn test_amp_error_retry_instructions() {
677        let rpc_error = AmpError::rpc("Elements node connection failed");
678        let instructions = rpc_error.retry_instructions();
679        assert!(instructions.is_some());
680        assert!(instructions.unwrap().contains("Elements node"));
681
682        let validation_error = AmpError::validation("Invalid asset UUID format");
683        assert!(validation_error.retry_instructions().is_none());
684
685        let timeout_with_txid = AmpError::timeout("Confirmation timeout for txid abc123");
686        let timeout_instructions = timeout_with_txid.retry_instructions();
687        assert!(timeout_instructions.is_some());
688        assert!(timeout_instructions.unwrap().contains("transaction ID"));
689    }
690
691    #[test]
692    fn test_amp_error_display() {
693        let api_error = AmpError::api("Test API error");
694        assert_eq!(format!("{}", api_error), "API error: Test API error");
695
696        let rpc_error = AmpError::rpc("Test RPC error");
697        assert_eq!(format!("{}", rpc_error), "RPC error: Test RPC error");
698
699        let validation_error = AmpError::validation("Test validation error");
700        assert_eq!(
701            format!("{}", validation_error),
702            "Validation error: Test validation error"
703        );
704
705        let timeout_error = AmpError::timeout("Test timeout error");
706        assert_eq!(
707            format!("{}", timeout_error),
708            "Timeout waiting for confirmations: Test timeout error"
709        );
710    }
711
712    #[test]
713    fn test_amp_error_from_conversions() {
714        // Test conversion from SignerError
715        let signer_error = SignerError::Lwk("Test LWK error".to_string());
716        let amp_error = AmpError::from(signer_error);
717        assert!(matches!(amp_error, AmpError::Signer(_)));
718
719        // Test conversion from existing Error
720        let existing_error = Error::MissingEnvVar("TEST_VAR".to_string());
721        let amp_error = AmpError::from(existing_error);
722        assert!(matches!(amp_error, AmpError::Existing(_)));
723
724        // Test conversion from serde_json::Error
725        let json_error = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
726        let amp_error = AmpError::from(json_error);
727        assert!(matches!(amp_error, AmpError::Serialization(_)));
728    }
729}
730
731/// Elements RPC client for blockchain operations
732#[derive(Debug)]
733pub struct ElementsRpc {
734    client: reqwest::Client,
735    base_url: String,
736    username: String,
737    password: String,
738}
739
740/// Network information from Elements node
741#[derive(Debug, serde::Deserialize)]
742pub struct NetworkInfo {
743    pub version: i64,
744    pub subversion: String,
745    pub protocolversion: i64,
746    pub localservices: String,
747    pub localrelay: bool,
748    pub timeoffset: i64,
749    pub networkactive: bool,
750    pub connections: i64,
751    pub networks: Vec<serde_json::Value>,
752    pub relayfee: f64,
753    pub incrementalfee: f64,
754    pub localaddresses: Vec<serde_json::Value>,
755    pub warnings: String,
756}
757
758/// Blockchain information from Elements node
759#[derive(Debug, serde::Deserialize)]
760pub struct BlockchainInfo {
761    pub chain: String,
762    pub blocks: i64,
763    pub headers: i64,
764    pub bestblockhash: String,
765    #[serde(default)]
766    pub difficulty: Option<f64>,
767    #[serde(default)]
768    pub mediantime: Option<i64>,
769    #[serde(default)]
770    pub verificationprogress: Option<f64>,
771    #[serde(default)]
772    pub initialblockdownload: Option<bool>,
773    #[serde(default)]
774    pub chainwork: Option<String>,
775    #[serde(default)]
776    pub size_on_disk: Option<i64>,
777    #[serde(default)]
778    pub pruned: Option<bool>,
779    #[serde(default)]
780    pub softforks: Option<serde_json::Value>,
781    #[serde(default)]
782    pub warnings: Option<String>,
783}
784
785/// RPC request structure for Elements node
786#[derive(Debug, serde::Serialize)]
787struct RpcRequest {
788    jsonrpc: String,
789    id: String,
790    method: String,
791    params: serde_json::Value,
792}
793
794/// RPC response structure from Elements node
795#[derive(Debug, serde::Deserialize)]
796struct RpcResponse<T> {
797    #[allow(dead_code)]
798    jsonrpc: Option<String>, // Optional for JSON-RPC 1.0 compatib
799    #[allow(dead_code)]
800    id: String,
801    result: Option<T>,
802    error: Option<RpcError>,
803}
804
805/// RPC error structure from Elements node
806#[derive(Debug, serde::Deserialize)]
807struct RpcError {
808    code: i32,
809    message: String,
810}
811
812impl ElementsRpc {
813    /// Creates a new `ElementsRpc` client with connection parameters
814    ///
815    /// # Arguments
816    /// * `url` - The RPC endpoint URL (e.g., <http://localhost:18884>)
817    /// * `username` - RPC authentication username
818    /// * `password` - RPC authentication password
819    ///
820    /// # Examples
821    /// ```
822    /// use amp_rs::ElementsRpc;
823    ///
824    /// let rpc = ElementsRpc::new(
825    ///     "http://localhost:18884".to_string(),
826    ///     "user".to_string(),
827    ///     "pass".to_string()
828    /// );
829    /// ```
830    /// # Panics
831    ///
832    /// Panics if the HTTP client cannot be created.
833    #[must_use]
834    pub fn new(url: String, username: String, password: String) -> Self {
835        let client = reqwest::Client::builder()
836            .timeout(std::time::Duration::from_secs(30))
837            .build()
838            .expect("Failed to create HTTP client");
839
840        Self {
841            client,
842            base_url: url,
843            username,
844            password,
845        }
846    }
847
848    /// Creates a new `ElementsRpc` client from environment variables
849    ///
850    /// Expected environment variables:
851    /// - `ELEMENTS_RPC_URL`: RPC endpoint URL
852    /// - `ELEMENTS_RPC_USER`: RPC username
853    /// - `ELEMENTS_RPC_PASSWORD`: RPC password
854    ///
855    /// # Errors
856    /// Returns an error if any required environment variable is missing
857    ///
858    /// # Examples
859    /// ```no_run
860    /// use amp_rs::ElementsRpc;
861    ///
862    /// let rpc = ElementsRpc::from_env().unwrap();
863    /// ```
864    pub fn from_env() -> Result<Self, AmpError> {
865        let url = env::var("ELEMENTS_RPC_URL")
866            .map_err(|_| AmpError::validation("Missing ELEMENTS_RPC_URL environment variable"))?;
867        let username = env::var("ELEMENTS_RPC_USER")
868            .map_err(|_| AmpError::validation("Missing ELEMENTS_RPC_USER environment variable"))?;
869        let password = env::var("ELEMENTS_RPC_PASSWORD").map_err(|_| {
870            AmpError::validation("Missing ELEMENTS_RPC_PASSWORD environment variable")
871        })?;
872
873        Ok(Self::new(url, username, password))
874    }
875
876    /// Makes an RPC call to the Elements node
877    ///
878    /// # Arguments
879    /// * `method` - The RPC method name
880    /// * `params` - The parameters for the RPC call
881    ///
882    /// # Errors
883    /// Returns an error if the RPC call fails or returns an error
884    async fn rpc_call<T: serde::de::DeserializeOwned>(
885        &self,
886        method: &str,
887        params: serde_json::Value,
888    ) -> Result<T, AmpError> {
889        tracing::debug!("Making RPC call: {} with params: {:?}", method, params);
890
891        let request = RpcRequest {
892            jsonrpc: "1.0".to_string(),
893            id: "amp-client".to_string(),
894            method: method.to_string(),
895            params,
896        };
897
898        let response = self
899            .client
900            .post(&self.base_url)
901            .basic_auth(&self.username, Some(&self.password))
902            .json(&request)
903            .send()
904            .await
905            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
906
907        if !response.status().is_success() {
908            let status = response.status();
909            let error_body = response
910                .text()
911                .await
912                .unwrap_or_else(|_| "Unable to read error body".to_string());
913            return Err(AmpError::rpc(format!(
914                "RPC request failed with status: {status} - Body: {error_body}"
915            )));
916        }
917
918        let rpc_response: RpcResponse<T> = response
919            .json()
920            .await
921            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
922
923        if let Some(error) = rpc_response.error {
924            return Err(AmpError::rpc(format!(
925                "RPC error {}: {}",
926                error.code, error.message
927            )));
928        }
929
930        rpc_response
931            .result
932            .ok_or_else(|| AmpError::rpc("RPC response missing result field".to_string()))
933    }
934
935    /// Retrieves network information from the Elements node
936    ///
937    /// # Errors
938    /// Returns an error if the RPC call fails
939    ///
940    /// # Examples
941    /// ```no_run
942    /// # use amp_rs::ElementsRpc;
943    /// # #[tokio::main]
944    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
945    /// let rpc = ElementsRpc::from_env()?;
946    /// let network_info = rpc.get_network_info().await?;
947    /// println!("Node version: {}", network_info.version);
948    /// # Ok(())
949    /// # }
950    /// ```
951    pub async fn get_network_info(&self) -> Result<NetworkInfo, AmpError> {
952        self.rpc_call("getnetworkinfo", serde_json::Value::Array(vec![]))
953            .await
954    }
955
956    /// Retrieves blockchain information from the Elements node
957    ///
958    /// # Errors
959    /// Returns an error if the RPC call fails
960    ///
961    /// # Examples
962    /// ```no_run
963    /// # use amp_rs::ElementsRpc;
964    /// # #[tokio::main]
965    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
966    /// let rpc = ElementsRpc::from_env()?;
967    /// let blockchain_info = rpc.get_blockchain_info().await?;
968    /// println!("Current block height: {}", blockchain_info.blocks);
969    /// # Ok(())
970    /// # }
971    /// ```
972    pub async fn get_blockchain_info(&self) -> Result<BlockchainInfo, AmpError> {
973        self.rpc_call("getblockchaininfo", serde_json::Value::Array(vec![]))
974            .await
975    }
976
977    /// Unlocks the wallet with a passphrase for the specified timeout
978    ///
979    /// # Arguments
980    /// * `passphrase` - The wallet passphrase
981    /// * `timeout` - Timeout in seconds for the unlock
982    ///
983    /// # Errors
984    /// Returns an error if the RPC call fails
985    ///
986    /// # Examples
987    /// ```no_run
988    /// # use amp_rs::ElementsRpc;
989    /// # #[tokio::main]
990    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
991    /// let rpc = ElementsRpc::from_env()?;
992    /// rpc.wallet_passphrase("my_passphrase", 300).await?;
993    /// # Ok(())
994    /// # }
995    /// ```
996    pub async fn wallet_passphrase(&self, passphrase: &str, timeout: u64) -> Result<(), AmpError> {
997        let params = serde_json::json!([passphrase, timeout]);
998
999        // wallet_passphrase returns null on success, so we need to handle this specially
1000        let request = RpcRequest {
1001            jsonrpc: "1.0".to_string(),
1002            id: "amp-client".to_string(),
1003            method: "walletpassphrase".to_string(),
1004            params,
1005        };
1006
1007        let response = self
1008            .client
1009            .post(&self.base_url)
1010            .basic_auth(&self.username, Some(&self.password))
1011            .json(&request)
1012            .send()
1013            .await
1014            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
1015
1016        if !response.status().is_success() {
1017            return Err(AmpError::rpc(format!(
1018                "RPC request failed with status: {}",
1019                response.status()
1020            )));
1021        }
1022
1023        let rpc_response: RpcResponse<serde_json::Value> = response
1024            .json()
1025            .await
1026            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
1027
1028        if let Some(error) = rpc_response.error {
1029            return Err(AmpError::rpc(format!(
1030                "RPC error {}: {}",
1031                error.code, error.message
1032            )));
1033        }
1034
1035        // For wallet_passphrase, null result is success
1036        Ok(())
1037    }
1038
1039    /// Validates the connection to the Elements node
1040    ///
1041    /// This method performs basic connectivity and authentication checks by
1042    /// retrieving network information from the node.
1043    ///
1044    /// # Errors
1045    /// Returns an error if the connection validation fails
1046    ///
1047    /// # Examples
1048    /// ```no_run
1049    /// # use amp_rs::ElementsRpc;
1050    /// # #[tokio::main]
1051    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1052    /// let rpc = ElementsRpc::from_env()?;
1053    /// rpc.validate_connection().await?;
1054    /// println!("Connection to Elements node is valid");
1055    /// # Ok(())
1056    /// # }
1057    /// ```
1058    pub async fn validate_connection(&self) -> Result<(), AmpError> {
1059        tracing::info!(
1060            "Validating connection to Elements node at {}",
1061            self.base_url
1062        );
1063
1064        let network_info = self
1065            .get_network_info()
1066            .await
1067            .map_err(|e| e.with_context("Failed to validate Elements node connection"))?;
1068
1069        tracing::info!(
1070            "Successfully connected to Elements node - Version: {}, Connections: {}",
1071            network_info.version,
1072            network_info.connections
1073        );
1074
1075        Ok(())
1076    }
1077
1078    /// Retrieves comprehensive node status including network and blockchain information
1079    ///
1080    /// This method combines network and blockchain information to provide a complete
1081    /// status overview of the Elements node.
1082    ///
1083    /// # Errors
1084    /// Returns an error if any RPC call fails
1085    ///
1086    /// # Examples
1087    /// ```no_run
1088    /// # use amp_rs::ElementsRpc;
1089    /// # #[tokio::main]
1090    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1091    /// let rpc = ElementsRpc::from_env()?;
1092    /// let (network_info, blockchain_info) = rpc.get_node_status().await?;
1093    /// println!("Node version: {}, Block height: {}", network_info.version, blockchain_info.blocks);
1094    /// # Ok(())
1095    /// # }
1096    /// ```
1097    pub async fn get_node_status(&self) -> Result<(NetworkInfo, BlockchainInfo), AmpError> {
1098        let network_info = self.get_network_info().await?;
1099        let blockchain_info = self.get_blockchain_info().await?;
1100
1101        Ok((network_info, blockchain_info))
1102    }
1103
1104    /// Lists unspent transaction outputs (UTXOs) for a specific asset
1105    ///
1106    /// # Arguments
1107    /// * `asset_id` - Optional asset ID to filter UTXOs. If None, returns all UTXOs
1108    ///
1109    /// # Errors
1110    /// Returns an error if the RPC call fails
1111    ///
1112    /// # Panics
1113    /// May panic if `asset_id` is `Some` but the warning log message attempts to unwrap it.
1114    /// This is a known logging issue and does not affect normal operation.
1115    ///
1116    /// # Examples
1117    /// ```no_run
1118    /// # use amp_rs::ElementsRpc;
1119    /// # #[tokio::main]
1120    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1121    /// let rpc = ElementsRpc::from_env()?;
1122    /// let utxos = rpc.list_unspent(Some("asset_id_hex")).await?;
1123    /// println!("Found {} UTXOs", utxos.len());
1124    /// # Ok(())
1125    /// # }
1126    /// ```
1127    pub async fn list_unspent(&self, asset_id: Option<&str>) -> Result<Vec<Unspent>, AmpError> {
1128        tracing::debug!("Listing unspent outputs for asset: {:?}", asset_id);
1129
1130        let params = asset_id.map_or_else(
1131            || serde_json::json!([1, 9_999_999, [], true]),
1132            |asset| serde_json::json!([1, 9_999_999, [], true, {"asset": asset}]),
1133        );
1134
1135        let utxos: Vec<Unspent> = self
1136            .rpc_call("listunspent", params)
1137            .await
1138            .map_err(|e| {
1139                if let Some(asset) = asset_id {
1140                    e.with_context(format!(
1141                        "Failed to list unspent outputs for asset {asset}. \
1142                        This may indicate that the treasury address is not imported in the Elements node. \
1143                        Ensure the treasury address is properly imported as a watch-only address."
1144                    ))
1145                } else {
1146                    e.with_context("Failed to list unspent outputs")
1147                }
1148            })?;
1149
1150        tracing::debug!("Found {} unspent outputs", utxos.len());
1151
1152        // If we're looking for a specific asset and found no UTXOs, provide helpful context
1153        if utxos.is_empty() && asset_id.is_some() {
1154            tracing::warn!(
1155                "No UTXOs found for asset {}. This may indicate:\n\
1156                1. The treasury address is not imported in the Elements node\n\
1157                2. The asset issuance transaction hasn't been confirmed yet\n\
1158                3. The UTXOs have already been spent",
1159                asset_id.unwrap()
1160            );
1161        }
1162
1163        Ok(utxos)
1164    }
1165
1166    /// List unspent outputs for a specific wallet
1167    ///
1168    /// This method lists unspent transaction outputs (UTXOs) for a specific wallet,
1169    /// optionally filtered by asset ID.
1170    ///
1171    /// # Arguments
1172    ///
1173    /// * `wallet_name` - Name of the Elements wallet to query
1174    /// * `asset_id` - Optional asset ID to filter UTXOs by
1175    ///
1176    /// # Returns
1177    ///
1178    /// Returns a vector of unspent outputs
1179    ///
1180    /// # Errors
1181    /// Returns an error if the RPC call fails or the wallet cannot be loaded
1182    ///
1183    /// # Panics
1184    /// May panic when processing UTXO blinding data if scriptpubkey is unexpectedly missing.
1185    /// This should not occur under normal operation with valid Elements node responses.
1186    ///
1187    /// # Example
1188    ///
1189    /// ```no_run
1190    /// # use amp_rs::ElementsRpc;
1191    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1192    /// let rpc = ElementsRpc::from_env()?;
1193    /// // Note: This would need to be called in an async context
1194    /// // let utxos = rpc.list_unspent_for_wallet("test_wallet", None).await?;
1195    /// // println!("Found {} UTXOs", utxos.len());
1196    /// # Ok(())
1197    /// # }
1198    /// ```
1199    #[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
1200    pub async fn list_unspent_for_wallet(
1201        &self,
1202        wallet_name: &str,
1203        asset_id: Option<&str>,
1204    ) -> Result<Vec<Unspent>, AmpError> {
1205        tracing::debug!(
1206            "Listing unspent outputs for wallet {} and asset: {:?}",
1207            wallet_name,
1208            asset_id
1209        );
1210
1211        // First load the wallet to ensure it's available
1212        self.load_wallet(wallet_name).await?;
1213
1214        let params = asset_id.map_or_else(
1215            || serde_json::json!([1, 9_999_999, [], true]),
1216            |asset| serde_json::json!([1, 9_999_999, [], true, {"asset": asset}]),
1217        );
1218
1219        let request = RpcRequest {
1220            jsonrpc: "1.0".to_string(),
1221            id: "amp-client".to_string(),
1222            method: "listunspent".to_string(),
1223            params,
1224        };
1225
1226        // Use the wallet-specific RPC endpoint
1227        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
1228
1229        let response = self
1230            .client
1231            .post(&wallet_url)
1232            .basic_auth(&self.username, Some(&self.password))
1233            .json(&request)
1234            .send()
1235            .await
1236            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
1237
1238        if !response.status().is_success() {
1239            let status = response.status();
1240            let error_body = response
1241                .text()
1242                .await
1243                .unwrap_or_else(|_| "Unable to read error body".to_string());
1244            return Err(AmpError::rpc(format!(
1245                "RPC request failed with status: {status} - Body: {error_body}"
1246            )));
1247        }
1248
1249        let rpc_response: RpcResponse<Vec<Unspent>> = response
1250            .json()
1251            .await
1252            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
1253
1254        if let Some(error) = rpc_response.error {
1255            return Err(AmpError::rpc(format!(
1256                "RPC error listing unspent outputs: {} (code: {})",
1257                error.message, error.code
1258            )));
1259        }
1260
1261        let mut utxos = rpc_response.result.unwrap_or_default();
1262
1263        // Enrich UTXOs with scriptpubkey information if missing
1264        for utxo in &mut utxos {
1265            if utxo.scriptpubkey.is_none() {
1266                tracing::debug!(
1267                    "UTXO {}:{} missing scriptpubkey, attempting to derive from address",
1268                    utxo.txid,
1269                    utxo.vout
1270                );
1271
1272                // Try to derive scriptpubkey from the address
1273                if let Ok(address) = elements::Address::from_str(&utxo.address) {
1274                    let script_pubkey = address.script_pubkey();
1275                    utxo.scriptpubkey = Some(hex::encode(script_pubkey.as_bytes()));
1276                    tracing::info!(
1277                        "Derived scriptpubkey for UTXO {}:{} from address {}: {}",
1278                        utxo.txid,
1279                        utxo.vout,
1280                        utxo.address,
1281                        utxo.scriptpubkey.as_ref().unwrap()
1282                    );
1283                } else {
1284                    tracing::error!(
1285                        "Failed to parse address {} for UTXO {}:{}",
1286                        utxo.address,
1287                        utxo.txid,
1288                        utxo.vout
1289                    );
1290
1291                    // Fallback: try to get transaction details
1292                    match self.get_transaction(&utxo.txid).await {
1293                        Ok(tx_detail) => {
1294                            tracing::debug!(
1295                                "Retrieved transaction details for {} as fallback",
1296                                utxo.txid
1297                            );
1298                            // Parse the transaction hex to extract the scriptpubkey for this output
1299                            match hex::decode(&tx_detail.hex) {
1300                                Ok(tx_bytes) => {
1301                                    match elements::Transaction::consensus_decode(&tx_bytes[..]) {
1302                                        Ok(tx) => {
1303                                            if let Some(output) = tx.output.get(utxo.vout as usize)
1304                                            {
1305                                                utxo.scriptpubkey = Some(hex::encode(
1306                                                    output.script_pubkey.as_bytes(),
1307                                                ));
1308                                                tracing::info!("Enriched UTXO {}:{} with scriptpubkey from transaction: {}", 
1309                                                    utxo.txid, utxo.vout, utxo.scriptpubkey.as_ref().unwrap());
1310                                            } else {
1311                                                tracing::error!(
1312                                                    "Output {} not found in transaction {}",
1313                                                    utxo.vout,
1314                                                    utxo.txid
1315                                                );
1316                                            }
1317                                        }
1318                                        Err(e) => {
1319                                            tracing::error!(
1320                                                "Failed to decode transaction {}: {}",
1321                                                utxo.txid,
1322                                                e
1323                                            );
1324                                        }
1325                                    }
1326                                }
1327                                Err(e) => {
1328                                    tracing::error!(
1329                                        "Failed to decode hex for transaction {}: {}",
1330                                        utxo.txid,
1331                                        e
1332                                    );
1333                                }
1334                            }
1335                        }
1336                        Err(e) => {
1337                            tracing::error!(
1338                                "Failed to get transaction details for {}: {}",
1339                                utxo.txid,
1340                                e
1341                            );
1342                        }
1343                    }
1344                }
1345            } else {
1346                tracing::debug!("UTXO {}:{} already has scriptpubkey", utxo.txid, utxo.vout);
1347            }
1348        }
1349
1350        tracing::debug!(
1351            "Found {} unspent outputs for wallet {}",
1352            utxos.len(),
1353            wallet_name
1354        );
1355
1356        // If we're looking for a specific asset and found no UTXOs, provide helpful context
1357        if utxos.is_empty() && asset_id.is_some() {
1358            tracing::warn!(
1359                "No UTXOs found for asset {} in wallet {}. This may indicate:\n\
1360                1. The asset issuance transaction hasn't been confirmed yet\n\
1361                2. The UTXOs have already been spent\n\
1362                3. The wallet doesn't contain the expected addresses",
1363                asset_id.unwrap(),
1364                wallet_name
1365            );
1366        }
1367
1368        Ok(utxos)
1369    }
1370
1371    /// Creates a raw transaction with the specified inputs and outputs
1372    ///
1373    /// # Arguments
1374    /// * `inputs` - Vector of transaction inputs (UTXOs to spend)
1375    /// * `outputs` - Map of addresses to amounts for regular outputs
1376    /// * `assets` - Map of addresses to asset IDs for Liquid-specific outputs
1377    ///
1378    /// # Errors
1379    /// Returns an error if the RPC call fails or transaction creation fails
1380    ///
1381    /// # Examples
1382    /// ```no_run
1383    /// # use amp_rs::{ElementsRpc, model::{TxInput}};
1384    /// # use std::collections::HashMap;
1385    /// # #[tokio::main]
1386    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1387    /// let rpc = ElementsRpc::from_env()?;
1388    /// let inputs = vec![TxInput {
1389    ///     txid: "abc123".to_string(),
1390    ///     vout: 0,
1391    ///     sequence: None,
1392    /// }];
1393    /// let mut outputs = HashMap::new();
1394    /// outputs.insert("address1".to_string(), 100.0);
1395    /// let mut assets = HashMap::new();
1396    /// assets.insert("address1".to_string(), "asset_id".to_string());
1397    /// let raw_tx = rpc.create_raw_transaction(inputs, outputs, assets).await?;
1398    /// # Ok(())
1399    /// # }
1400    /// ```
1401    #[allow(clippy::cognitive_complexity)]
1402    pub async fn create_raw_transaction(
1403        &self,
1404        inputs: Vec<TxInput>,
1405        outputs: std::collections::HashMap<String, f64>,
1406        assets: std::collections::HashMap<String, String>,
1407    ) -> Result<String, AmpError> {
1408        tracing::debug!(
1409            "Creating raw transaction with {} inputs and {} outputs",
1410            inputs.len(),
1411            outputs.len()
1412        );
1413
1414        // Elements RPC createrawtransaction expects:
1415        // createrawtransaction inputs outputs locktime replaceable assets
1416        let params = serde_json::json!([
1417            inputs,  // inputs as TxInput array
1418            outputs, // outputs as address->amount map
1419            0,       // locktime (0 = no locktime)
1420            false,   // replaceable (false = not replaceable)
1421            assets   // assets as address->asset_id map
1422        ]);
1423
1424        // Debug: Log the exact parameters being sent to createrawtransaction
1425        tracing::error!("createrawtransaction parameters:");
1426        tracing::error!(
1427            "  inputs: {}",
1428            serde_json::to_string_pretty(&inputs).unwrap_or_default()
1429        );
1430        tracing::error!(
1431            "  outputs: {}",
1432            serde_json::to_string_pretty(&outputs).unwrap_or_default()
1433        );
1434        tracing::error!(
1435            "  assets: {}",
1436            serde_json::to_string_pretty(&assets).unwrap_or_default()
1437        );
1438
1439        let raw_tx: String = self
1440            .rpc_call("createrawtransaction", params)
1441            .await
1442            .map_err(|e| {
1443                tracing::error!("createrawtransaction RPC call failed: {}", e);
1444                e.with_context("Failed to create raw transaction")
1445            })?;
1446
1447        tracing::debug!("Created raw transaction: {}", raw_tx);
1448        Ok(raw_tx)
1449    }
1450
1451    /// Imports an address into a specific wallet as watch-only
1452    ///
1453    /// # Arguments
1454    /// * `wallet_name` - Name of the wallet to import into
1455    /// * `address` - The address to import
1456    /// * `label` - Optional label for the address
1457    /// * `rescan` - Whether to rescan the blockchain for transactions
1458    ///
1459    /// # Errors
1460    /// Returns an error if the RPC call fails
1461    async fn import_address_to_wallet(
1462        &self,
1463        wallet_name: &str,
1464        address: &str,
1465        label: Option<&str>,
1466        rescan: bool,
1467    ) -> Result<(), AmpError> {
1468        tracing::debug!("Importing address {} into wallet {}", address, wallet_name);
1469
1470        // First load the wallet to ensure it's available
1471        self.load_wallet(wallet_name).await?;
1472
1473        let params = serde_json::json!([address, label.unwrap_or(""), rescan]);
1474
1475        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
1476
1477        let request = RpcRequest {
1478            jsonrpc: "1.0".to_string(),
1479            id: "amp-client".to_string(),
1480            method: "importaddress".to_string(),
1481            params,
1482        };
1483
1484        let response = self
1485            .client
1486            .post(&wallet_url)
1487            .basic_auth(&self.username, Some(&self.password))
1488            .json(&request)
1489            .send()
1490            .await
1491            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
1492
1493        if !response.status().is_success() {
1494            let status = response.status();
1495            let error_body = response
1496                .text()
1497                .await
1498                .unwrap_or_else(|_| "Unable to read error body".to_string());
1499            return Err(AmpError::rpc(format!(
1500                "RPC request failed with status: {status} - Body: {error_body}"
1501            )));
1502        }
1503
1504        let rpc_response: RpcResponse<serde_json::Value> = response
1505            .json()
1506            .await
1507            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
1508
1509        if let Some(error) = rpc_response.error {
1510            // Ignore "already imported" errors
1511            if error.code != -4 {
1512                return Err(AmpError::rpc(format!(
1513                    "RPC error importing address: {} (code: {})",
1514                    error.message, error.code
1515                )));
1516            }
1517        }
1518
1519        tracing::debug!(
1520            "Successfully imported address {} into wallet {}",
1521            address,
1522            wallet_name
1523        );
1524        Ok(())
1525    }
1526
1527    /// Creates a raw transaction using a specific wallet context
1528    ///
1529    /// This method uses the wallet-specific RPC endpoint which is necessary
1530    /// for confidential transactions that require wallet context for blinding keys.
1531    ///
1532    /// # Arguments
1533    /// * `wallet_name` - Name of the wallet to use for transaction creation
1534    /// * `inputs` - Transaction inputs
1535    /// * `outputs` - Map of addresses to amounts
1536    /// * `assets` - Map of addresses to asset IDs
1537    ///
1538    /// # Returns
1539    /// Returns the raw transaction hex
1540    ///
1541    /// # Errors
1542    /// Returns an error if the RPC call fails
1543    #[allow(dead_code)]
1544    #[allow(clippy::cognitive_complexity)]
1545    async fn create_raw_transaction_with_wallet(
1546        &self,
1547        wallet_name: &str,
1548        inputs: Vec<TxInput>,
1549        outputs: std::collections::HashMap<String, f64>,
1550        assets: std::collections::HashMap<String, String>,
1551    ) -> Result<String, AmpError> {
1552        tracing::debug!(
1553            "Creating raw transaction with wallet {} - {} inputs and {} outputs",
1554            wallet_name,
1555            inputs.len(),
1556            outputs.len()
1557        );
1558
1559        // First load the wallet to ensure it's available
1560        self.load_wallet(wallet_name).await?;
1561
1562        // Elements RPC createrawtransaction expects outputs as an array of objects
1563        // Each output object should contain both address, amount, and asset
1564        let mut outputs_array = Vec::new();
1565
1566        for (address, amount) in &outputs {
1567            let asset_id = assets.get(address).ok_or_else(|| {
1568                AmpError::validation(format!("No asset ID found for address {address}"))
1569            })?;
1570
1571            // Convert amount to string with proper precision for Elements
1572            let amount_str = format!("{amount:.8}");
1573
1574            outputs_array.push(serde_json::json!({
1575                address.clone(): amount_str,
1576                "asset": asset_id
1577            }));
1578        }
1579
1580        let params = serde_json::json!([
1581            inputs,        // inputs as TxInput array
1582            outputs_array, // outputs as array of {address: amount, asset: id} objects
1583            0,             // locktime (0 = no locktime)
1584            false,         // replaceable (false = not replaceable)
1585        ]);
1586
1587        // Debug: Log the exact parameters being sent to createrawtransaction
1588        tracing::error!("createrawtransaction parameters (wallet-specific, corrected format):");
1589        tracing::error!("  wallet: {}", wallet_name);
1590        tracing::error!(
1591            "  inputs: {}",
1592            serde_json::to_string_pretty(&inputs).unwrap_or_default()
1593        );
1594        tracing::error!(
1595            "  outputs_array: {}",
1596            serde_json::to_string_pretty(&outputs_array).unwrap_or_default()
1597        );
1598
1599        // Use the wallet-specific RPC endpoint
1600        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
1601
1602        let request = RpcRequest {
1603            jsonrpc: "1.0".to_string(),
1604            id: "amp-client".to_string(),
1605            method: "createrawtransaction".to_string(),
1606            params,
1607        };
1608
1609        let response = self
1610            .client
1611            .post(&wallet_url)
1612            .basic_auth(&self.username, Some(&self.password))
1613            .json(&request)
1614            .send()
1615            .await
1616            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
1617
1618        if !response.status().is_success() {
1619            let status = response.status();
1620            let error_body = response
1621                .text()
1622                .await
1623                .unwrap_or_else(|_| "Unable to read error body".to_string());
1624            return Err(AmpError::rpc(format!(
1625                "RPC request failed with status: {status} - Body: {error_body}"
1626            )));
1627        }
1628
1629        let rpc_response: RpcResponse<String> = response
1630            .json()
1631            .await
1632            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
1633
1634        if let Some(error) = rpc_response.error {
1635            return Err(AmpError::rpc(format!(
1636                "RPC error creating raw transaction: {} (code: {})",
1637                error.message, error.code
1638            )));
1639        }
1640
1641        let raw_tx = rpc_response
1642            .result
1643            .ok_or_else(|| AmpError::rpc("No raw transaction returned".to_string()))?;
1644
1645        tracing::debug!(
1646            "Created raw transaction with wallet {}: {}",
1647            wallet_name,
1648            raw_tx
1649        );
1650        Ok(raw_tx)
1651    }
1652
1653    /// Imports an address into a specific wallet as watch-only
1654    ///
1655    /// # Arguments
1656    /// * `wallet_name` - Name of the wallet
1657    /// * `address` - The address to import
1658    /// * `label` - Optional label for the address
1659    /// * `rescan` - Optional whether to rescan the blockchain (default: false)
1660    ///
1661    /// # Errors
1662    /// Returns an error if the RPC call fails
1663    ///
1664    /// # Examples
1665    /// ```no_run
1666    /// # use amp_rs::ElementsRpc;
1667    /// # #[tokio::main]
1668    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1669    /// let rpc = ElementsRpc::from_env()?;
1670    /// rpc.import_address("my_wallet", "vjU8L4dKa1XyyVcPqKBbTgjT1tRC7qYp5VJGwndZSCFk4ntpWey1pQe6hcSGDMVurr9CsZ21EGsqGjWA", Some("test_address"), Some(false)).await?;
1671    /// # Ok(())
1672    /// # }
1673    /// ```
1674    pub async fn import_address(
1675        &self,
1676        wallet_name: &str,
1677        address: &str,
1678        label: Option<&str>,
1679        rescan: Option<bool>,
1680    ) -> Result<(), AmpError> {
1681        let rescan_value = rescan.unwrap_or(false);
1682        tracing::debug!(
1683            "Importing address: {} into wallet: {} with label: {:?}, rescan: {}",
1684            address,
1685            wallet_name,
1686            label,
1687            rescan_value
1688        );
1689
1690        let params = serde_json::json!([address, label.unwrap_or(""), rescan_value]);
1691
1692        // importaddress returns null on success
1693        let request = RpcRequest {
1694            jsonrpc: "1.0".to_string(),
1695            id: "amp-client".to_string(),
1696            method: "importaddress".to_string(),
1697            params,
1698        };
1699
1700        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
1701
1702        let response = self
1703            .client
1704            .post(&wallet_url)
1705            .basic_auth(&self.username, Some(&self.password))
1706            .json(&request)
1707            .send()
1708            .await
1709            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
1710
1711        if !response.status().is_success() {
1712            return Err(AmpError::rpc(format!(
1713                "RPC request failed with status: {}",
1714                response.status()
1715            )));
1716        }
1717
1718        let rpc_response: RpcResponse<serde_json::Value> = response
1719            .json()
1720            .await
1721            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
1722
1723        if let Some(error) = rpc_response.error {
1724            return Err(AmpError::rpc(format!(
1725                "RPC error {}: {}",
1726                error.code, error.message
1727            )));
1728        }
1729
1730        tracing::debug!(
1731            "Successfully imported address: {} into wallet: {}",
1732            address,
1733            wallet_name
1734        );
1735        Ok(())
1736    }
1737
1738    /// Rescans the blockchain for a wallet
1739    ///
1740    /// # Arguments
1741    /// * `wallet_name` - Name of the wallet to rescan
1742    /// * `start_height` - Optional start height for rescan (default: 0)
1743    ///
1744    /// # Errors
1745    /// Returns an error if the RPC call fails
1746    ///
1747    /// # Examples
1748    /// ```no_run
1749    /// # use amp_rs::ElementsRpc;
1750    /// # #[tokio::main]
1751    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1752    /// let rpc = ElementsRpc::from_env()?;
1753    /// let result = rpc.rescan_blockchain("my_wallet", None).await?;
1754    /// # Ok(())
1755    /// # }
1756    /// ```
1757    pub async fn rescan_blockchain(
1758        &self,
1759        wallet_name: &str,
1760        start_height: Option<u64>,
1761    ) -> Result<serde_json::Value, AmpError> {
1762        tracing::debug!("Rescanning blockchain for wallet: {}", wallet_name);
1763
1764        let params = start_height.map_or_else(
1765            || serde_json::json!([]),
1766            |height| serde_json::json!([height]),
1767        );
1768
1769        let request = RpcRequest {
1770            jsonrpc: "1.0".to_string(),
1771            id: "amp-client".to_string(),
1772            method: "rescanblockchain".to_string(),
1773            params,
1774        };
1775
1776        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
1777
1778        let response = self
1779            .client
1780            .post(&wallet_url)
1781            .basic_auth(&self.username, Some(&self.password))
1782            .json(&request)
1783            .send()
1784            .await
1785            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
1786
1787        if !response.status().is_success() {
1788            return Err(AmpError::rpc(format!(
1789                "RPC request failed with status: {}",
1790                response.status()
1791            )));
1792        }
1793
1794        let rpc_response: RpcResponse<serde_json::Value> = response
1795            .json()
1796            .await
1797            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
1798
1799        if let Some(error) = rpc_response.error {
1800            return Err(AmpError::rpc(format!(
1801                "RPC error rescanning blockchain: {} (code: {})",
1802                error.message, error.code
1803            )));
1804        }
1805
1806        let result = rpc_response
1807            .result
1808            .ok_or_else(|| AmpError::rpc("No result returned from rescanblockchain".to_string()))?;
1809
1810        tracing::debug!(
1811            "Successfully rescanned blockchain for wallet: {}",
1812            wallet_name
1813        );
1814        Ok(result)
1815    }
1816
1817    /// Creates or loads a wallet
1818    ///
1819    /// # Arguments
1820    /// * `wallet_name` - Name of the wallet to create or load
1821    /// * `disable_private_keys` - Whether to disable private keys (watch-only wallet)
1822    ///
1823    /// # Errors
1824    /// Returns an error if the RPC call fails
1825    ///
1826    /// # Examples
1827    /// ```no_run
1828    /// # use amp_rs::ElementsRpc;
1829    /// # #[tokio::main]
1830    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1831    /// let rpc = ElementsRpc::from_env()?;
1832    /// rpc.create_wallet("test_wallet", true).await?;
1833    /// # Ok(())
1834    /// # }
1835    /// ```
1836    pub async fn create_wallet(
1837        &self,
1838        wallet_name: &str,
1839        disable_private_keys: bool,
1840    ) -> Result<(), AmpError> {
1841        tracing::debug!(
1842            "Creating wallet: {} with disable_private_keys: {}",
1843            wallet_name,
1844            disable_private_keys
1845        );
1846
1847        let params = serde_json::json!([wallet_name, disable_private_keys]);
1848
1849        let request = RpcRequest {
1850            jsonrpc: "1.0".to_string(),
1851            id: "amp-client".to_string(),
1852            method: "createwallet".to_string(),
1853            params,
1854        };
1855
1856        let response = self
1857            .client
1858            .post(&self.base_url)
1859            .basic_auth(&self.username, Some(&self.password))
1860            .json(&request)
1861            .send()
1862            .await
1863            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
1864
1865        if !response.status().is_success() {
1866            return Err(AmpError::rpc(format!(
1867                "RPC request failed with status: {}",
1868                response.status()
1869            )));
1870        }
1871
1872        let rpc_response: RpcResponse<serde_json::Value> = response
1873            .json()
1874            .await
1875            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
1876
1877        if let Some(error) = rpc_response.error {
1878            // Ignore "wallet already exists" error
1879            if error.code != -4 {
1880                return Err(AmpError::rpc(format!(
1881                    "RPC error {}: {}",
1882                    error.code, error.message
1883                )));
1884            }
1885            tracing::debug!("Wallet {} already exists", wallet_name);
1886        } else {
1887            tracing::debug!("Successfully created wallet: {}", wallet_name);
1888        }
1889
1890        Ok(())
1891    }
1892
1893    /// Loads an existing wallet
1894    ///
1895    /// # Arguments
1896    /// * `wallet_name` - Name of the wallet to load
1897    ///
1898    /// # Errors
1899    /// Returns an error if the RPC call fails
1900    ///
1901    /// # Examples
1902    /// ```no_run
1903    /// # use amp_rs::ElementsRpc;
1904    /// # #[tokio::main]
1905    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1906    /// let rpc = ElementsRpc::from_env()?;
1907    /// rpc.load_wallet("test_wallet").await?;
1908    /// # Ok(())
1909    /// # }
1910    /// ```
1911    #[allow(clippy::cognitive_complexity)]
1912    pub async fn load_wallet(&self, wallet_name: &str) -> Result<(), AmpError> {
1913        tracing::debug!("Loading wallet: {}", wallet_name);
1914
1915        let params = serde_json::json!([wallet_name]);
1916
1917        let request = RpcRequest {
1918            jsonrpc: "1.0".to_string(),
1919            id: "amp-client".to_string(),
1920            method: "loadwallet".to_string(),
1921            params,
1922        };
1923
1924        let response = self
1925            .client
1926            .post(&self.base_url)
1927            .basic_auth(&self.username, Some(&self.password))
1928            .json(&request)
1929            .send()
1930            .await
1931            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
1932
1933        if !response.status().is_success() {
1934            let status = response.status();
1935            let error_body = response
1936                .text()
1937                .await
1938                .unwrap_or_else(|_| "Unable to read error body".to_string());
1939            tracing::debug!(
1940                "Load wallet failed with status: {} - Body: {}",
1941                status,
1942                error_body
1943            );
1944
1945            // For wallet loading, we want to be more permissive with errors
1946            // since the wallet might already be loaded
1947            if status == 500 && error_body.contains("already loaded") {
1948                tracing::debug!(
1949                    "Wallet {} appears to already be loaded (500 error)",
1950                    wallet_name
1951                );
1952                return Ok(());
1953            }
1954
1955            return Err(AmpError::rpc(format!(
1956                "RPC request failed with status: {status} - Body: {error_body}"
1957            )));
1958        }
1959
1960        let rpc_response: RpcResponse<serde_json::Value> = response
1961            .json()
1962            .await
1963            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
1964
1965        if let Some(error) = rpc_response.error {
1966            // Ignore "wallet already loaded" error
1967            if error.code != -35 {
1968                return Err(AmpError::rpc(format!(
1969                    "RPC error {}: {}",
1970                    error.code, error.message
1971                )));
1972            }
1973            tracing::debug!("Wallet {} already loaded", wallet_name);
1974        } else {
1975            tracing::debug!("Successfully loaded wallet: {}", wallet_name);
1976        }
1977
1978        Ok(())
1979    }
1980
1981    /// Unloads a wallet
1982    ///
1983    /// # Arguments
1984    /// * `wallet_name` - Name of the wallet to unload
1985    ///
1986    /// # Errors
1987    /// Returns an error if the RPC call fails
1988    ///
1989    /// # Examples
1990    /// ```no_run
1991    /// # use amp_rs::ElementsRpc;
1992    /// # #[tokio::main]
1993    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1994    /// let rpc = ElementsRpc::from_env()?;
1995    /// rpc.unload_wallet("test_wallet").await?;
1996    /// # Ok(())
1997    /// # }
1998    /// ```
1999    pub async fn unload_wallet(&self, wallet_name: &str) -> Result<(), AmpError> {
2000        tracing::debug!("Unloading wallet: {}", wallet_name);
2001
2002        let params = serde_json::json!([wallet_name]);
2003
2004        let request = RpcRequest {
2005            jsonrpc: "1.0".to_string(),
2006            id: "amp-client".to_string(),
2007            method: "unloadwallet".to_string(),
2008            params,
2009        };
2010
2011        let response = self
2012            .client
2013            .post(&self.base_url)
2014            .basic_auth(&self.username, Some(&self.password))
2015            .json(&request)
2016            .send()
2017            .await
2018            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
2019
2020        if !response.status().is_success() {
2021            return Err(AmpError::rpc(format!(
2022                "RPC request failed with status: {}",
2023                response.status()
2024            )));
2025        }
2026
2027        let rpc_response: RpcResponse<serde_json::Value> = response
2028            .json()
2029            .await
2030            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
2031
2032        if let Some(error) = rpc_response.error {
2033            return Err(AmpError::rpc(format!(
2034                "RPC error {}: {}",
2035                error.code, error.message
2036            )));
2037        }
2038
2039        tracing::debug!("Successfully unloaded wallet: {}", wallet_name);
2040        Ok(())
2041    }
2042
2043    /// Lists all available wallets
2044    ///
2045    /// # Errors
2046    /// Returns an error if the RPC call fails
2047    ///
2048    /// # Examples
2049    /// ```no_run
2050    /// # use amp_rs::ElementsRpc;
2051    /// # #[tokio::main]
2052    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2053    /// let rpc = ElementsRpc::from_env()?;
2054    /// let wallets = rpc.list_wallets().await?;
2055    /// println!("Available wallets: {:?}", wallets);
2056    /// # Ok(())
2057    /// # }
2058    /// ```
2059    pub async fn list_wallets(&self) -> Result<Vec<String>, AmpError> {
2060        tracing::debug!("Listing available wallets");
2061
2062        let params = serde_json::json!([]);
2063
2064        let wallets: Vec<String> = self
2065            .rpc_call("listwallets", params)
2066            .await
2067            .map_err(|e| e.with_context("Failed to list wallets"))?;
2068
2069        tracing::debug!("Found {} wallets", wallets.len());
2070        Ok(wallets)
2071    }
2072
2073    /// Sets up a watch-only wallet with the given address
2074    ///
2075    /// This is a convenience method that creates a watch-only wallet and imports the address
2076    ///
2077    /// # Arguments
2078    /// * `wallet_name` - Name of the wallet to create
2079    /// * `address` - Address to import as watch-only
2080    /// * `label` - Optional label for the address
2081    ///
2082    /// # Errors
2083    /// Returns an error if wallet creation or address import fails
2084    ///
2085    /// # Examples
2086    /// ```no_run
2087    /// # use amp_rs::ElementsRpc;
2088    /// # #[tokio::main]
2089    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2090    /// let rpc = ElementsRpc::from_env()?;
2091    /// rpc.setup_watch_only_wallet("test_wallet", "vjU8L4dKa1XyyVcPqKBbTgjT1tRC7qYp5VJGwndZSCFk4ntpWey1pQe6hcSGDMVurr9CsZ21EGsqGjWA", Some("treasury")).await?;
2092    /// # Ok(())
2093    /// # }
2094    /// ```
2095    #[allow(clippy::cognitive_complexity)]
2096    pub async fn setup_watch_only_wallet(
2097        &self,
2098        wallet_name: &str,
2099        address: &str,
2100        label: Option<&str>,
2101    ) -> Result<(), AmpError> {
2102        tracing::info!(
2103            "Setting up watch-only wallet '{}' with address: {}",
2104            wallet_name,
2105            address
2106        );
2107
2108        // Try the full wallet setup approach first
2109        match self
2110            .setup_wallet_with_address(wallet_name, address, label)
2111            .await
2112        {
2113            Ok(()) => {
2114                tracing::info!(
2115                    "Successfully set up watch-only wallet '{}' with address: {}",
2116                    wallet_name,
2117                    address
2118                );
2119                return Ok(());
2120            }
2121            Err(e) => {
2122                tracing::warn!(
2123                    "Full wallet setup failed: {}, trying direct address import",
2124                    e
2125                );
2126            }
2127        }
2128
2129        // Fallback: Try to import the address directly without wallet operations
2130        match self.import_address_direct(address, label) {
2131            Ok(()) => {
2132                tracing::info!("Successfully imported address directly: {}", address);
2133                Ok(())
2134            }
2135            Err(e) => {
2136                tracing::error!(
2137                    "Both wallet setup and direct import failed for address: {}",
2138                    address
2139                );
2140                Err(AmpError::rpc(format!(
2141                    "Failed to set up watch-only wallet or import address: wallet setup error: {e}, direct import error: {e}"
2142                )))
2143            }
2144        }
2145    }
2146
2147    /// Attempts to set up a wallet with address using the standard approach
2148    async fn setup_wallet_with_address(
2149        &self,
2150        wallet_name: &str,
2151        address: &str,
2152        label: Option<&str>,
2153    ) -> Result<(), AmpError> {
2154        // Try to create the wallet (will ignore if it already exists)
2155        self.create_wallet(wallet_name, true).await?;
2156
2157        // Try to load the wallet (will ignore if already loaded)
2158        self.load_wallet(wallet_name).await?;
2159
2160        // Import the address without rescanning (for faster setup)
2161        self.import_address(wallet_name, address, label, Some(false))
2162            .await?;
2163
2164        Ok(())
2165    }
2166
2167    /// Attempts to import an address directly without wallet operations (uses default wallet)
2168    #[allow(clippy::unused_self)]
2169    fn import_address_direct(&self, address: &str, _label: Option<&str>) -> Result<(), AmpError> {
2170        tracing::debug!("Attempting direct address import for: {}", address);
2171
2172        // This is a fallback method - we'll use empty string for wallet name to use default behavior
2173        // Note: This may not work as expected with the new signature, but kept for compatibility
2174        Err(AmpError::rpc(
2175            "Direct address import not supported with wallet-specific import_address".to_string(),
2176        ))
2177    }
2178
2179    /// Broadcasts a signed raw transaction to the network
2180    ///
2181    /// # Arguments
2182    /// * `hex` - The signed transaction in hexadecimal format
2183    ///
2184    /// # Errors
2185    /// Returns an error if the RPC call fails or transaction broadcast fails
2186    ///
2187    /// # Examples
2188    /// ```no_run
2189    /// # use amp_rs::ElementsRpc;
2190    /// # #[tokio::main]
2191    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2192    /// let rpc = ElementsRpc::from_env()?;
2193    /// let signed_tx_hex = "0200000000..."; // Signed transaction hex
2194    /// let txid = rpc.send_raw_transaction(signed_tx_hex).await?;
2195    /// println!("Transaction broadcast with ID: {}", txid);
2196    /// # Ok(())
2197    /// # }
2198    /// ```
2199    pub async fn send_raw_transaction(&self, hex: &str) -> Result<String, AmpError> {
2200        tracing::debug!(
2201            "Broadcasting raw transaction: {}",
2202            &hex[..std::cmp::min(hex.len(), 64)]
2203        );
2204
2205        let params = serde_json::json!([hex]);
2206
2207        let txid: String = self
2208            .rpc_call("sendrawtransaction", params)
2209            .await
2210            .map_err(|e| {
2211                tracing::error!("Raw transaction broadcast failed: {}", e);
2212                tracing::error!("Transaction hex (first 200 chars): {}", &hex[..std::cmp::min(hex.len(), 200)]);
2213
2214                // Provide specific guidance for blinding-related errors
2215                if e.to_string().contains("bad-txns-in-ne-out") || e.to_string().contains("value in != value out") {
2216                    AmpError::rpc(format!(
2217                        "Transaction broadcast failed due to confidential transaction blinding error. \
2218                        This indicates that the blinding factors don't balance properly. \
2219                        Possible solutions:\n\
2220                        1. Ensure all addresses have proper blinding keys in the wallet\n\
2221                        2. Verify that blindrawtransaction was called before signing\n\
2222                        3. Check that UTXO blinding factors match between Elements and LWK\n\
2223                        4. Try using unconfidential addresses for testing\n\
2224                        Original error: {e}"
2225                    ))
2226                } else {
2227                    e.with_context("Failed to broadcast raw transaction")
2228                }
2229            })?;
2230
2231        tracing::info!("Successfully broadcast transaction with ID: {}", txid);
2232        Ok(txid)
2233    }
2234
2235    /// Retrieves detailed information about a transaction
2236    ///
2237    /// # Arguments
2238    /// * `txid` - The transaction ID to retrieve
2239    ///
2240    /// # Errors
2241    /// Returns an error if the RPC call fails or transaction is not found
2242    ///
2243    /// # Examples
2244    /// ```no_run
2245    /// # use amp_rs::ElementsRpc;
2246    /// # #[tokio::main]
2247    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2248    /// let rpc = ElementsRpc::from_env()?;
2249    /// let tx_detail = rpc.get_transaction("abc123...").await?;
2250    /// println!("Transaction has {} confirmations", tx_detail.confirmations);
2251    /// # Ok(())
2252    /// # }
2253    /// ```
2254    pub async fn get_transaction(&self, txid: &str) -> Result<TransactionDetail, AmpError> {
2255        tracing::debug!("Retrieving transaction details for: {}", txid);
2256
2257        let params = serde_json::json!([txid, true]); // true for verbose output
2258
2259        let tx_detail: TransactionDetail = self
2260            .rpc_call("gettransaction", params)
2261            .await
2262            .map_err(|e| e.with_context(format!("Failed to get transaction details for {txid}")))?;
2263
2264        tracing::debug!(
2265            "Retrieved transaction {} with {} confirmations",
2266            txid,
2267            tx_detail.confirmations
2268        );
2269
2270        Ok(tx_detail)
2271    }
2272
2273    /// Sends multiple outputs to multiple addresses using Elements' sendmany RPC
2274    ///
2275    /// This method uses Elements' built-in sendmany command which properly handles
2276    /// confidential transactions and blinding. This is the recommended approach for
2277    /// asset distribution as it avoids manual transaction construction issues.
2278    ///
2279    /// # Arguments
2280    /// * `wallet_name` - Name of the Elements wallet to use
2281    /// * `address_amounts` - Map of addresses to amounts to send
2282    /// * `asset_amounts` - Map of addresses to asset IDs for each output
2283    /// * `min_conf` - Minimum confirmations for inputs (default: 1)
2284    /// * `comment` - Optional transaction comment
2285    /// * `subtract_fee_from` - Optional addresses to subtract fees from
2286    /// * `replaceable` - Whether transaction is replaceable (default: false)
2287    /// * `conf_target` - Confirmation target for fee estimation (default: 1)
2288    /// * `estimate_mode` - Fee estimation mode (default: "UNSET")
2289    ///
2290    /// # Returns
2291    /// Returns the transaction ID of the sent transaction
2292    ///
2293    /// # Errors
2294    /// Returns an error if the RPC call fails or transaction creation fails
2295    ///
2296    /// # Examples
2297    /// ```no_run
2298    /// # use amp_rs::ElementsRpc;
2299    /// # use std::collections::HashMap;
2300    /// # #[tokio::main]
2301    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2302    /// let rpc = ElementsRpc::from_env()?;
2303    ///
2304    /// let mut address_amounts = HashMap::new();
2305    /// address_amounts.insert("address1".to_string(), 100.0);
2306    /// address_amounts.insert("address2".to_string(), 50.0);
2307    ///
2308    /// let mut asset_amounts = HashMap::new();
2309    /// asset_amounts.insert("address1".to_string(), "asset_id_hex".to_string());
2310    /// asset_amounts.insert("address2".to_string(), "asset_id_hex".to_string());
2311    ///
2312    /// let txid = rpc.sendmany("wallet_name", address_amounts, asset_amounts, None, None, None, None, None, None).await?;
2313    /// println!("Transaction sent with ID: {}", txid);
2314    /// # Ok(())
2315    /// # }
2316    /// ```
2317    #[allow(clippy::too_many_arguments, clippy::cognitive_complexity)]
2318    pub async fn sendmany(
2319        &self,
2320        wallet_name: &str,
2321        address_amounts: std::collections::HashMap<String, f64>,
2322        asset_amounts: std::collections::HashMap<String, String>,
2323        min_conf: Option<u32>,
2324        comment: Option<&str>,
2325        subtract_fee_from: Option<Vec<String>>,
2326        replaceable: Option<bool>,
2327        conf_target: Option<u32>,
2328        estimate_mode: Option<&str>,
2329    ) -> Result<String, AmpError> {
2330        tracing::debug!(
2331            "Sending to {} addresses using sendmany for wallet {}",
2332            address_amounts.len(),
2333            wallet_name
2334        );
2335
2336        // First load the wallet to ensure it's available
2337        self.load_wallet(wallet_name).await?;
2338
2339        // Elements sendmany parameters:
2340        // 1. dummy (empty string for compatibility)
2341        // 2. amounts (map of address -> amount)
2342        // 3. minconf (minimum confirmations, default 1)
2343        // 4. comment (optional comment)
2344        // 5. subtractfeefrom (array of addresses to subtract fee from)
2345        // 6. replaceable (boolean, default false)
2346        // 7. conf_target (confirmation target for fee estimation)
2347        // 8. estimate_mode (fee estimation mode)
2348        // 9. assetlabel (map of address -> asset_id for multi-asset sends)
2349        let params = serde_json::json!([
2350            "",                                    // dummy (required for compatibility)
2351            address_amounts,                       // amounts map
2352            min_conf.unwrap_or(1),                 // minconf
2353            comment.unwrap_or(""),                 // comment
2354            subtract_fee_from.unwrap_or_default(), // subtractfeefrom
2355            replaceable.unwrap_or(false),          // replaceable
2356            conf_target.unwrap_or(1),              // conf_target
2357            estimate_mode.unwrap_or("UNSET"),      // estimate_mode
2358            asset_amounts                          // assetlabel (asset map)
2359        ]);
2360
2361        // Use the wallet-specific RPC endpoint
2362        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
2363
2364        let request = RpcRequest {
2365            jsonrpc: "1.0".to_string(),
2366            id: "amp-client".to_string(),
2367            method: "sendmany".to_string(),
2368            params,
2369        };
2370
2371        tracing::debug!("Sendmany request parameters:");
2372        tracing::debug!("  wallet: {}", wallet_name);
2373        tracing::debug!("  address_amounts: {:?}", address_amounts);
2374        tracing::debug!("  asset_amounts: {:?}", asset_amounts);
2375
2376        let response = self
2377            .client
2378            .post(&wallet_url)
2379            .basic_auth(&self.username, Some(&self.password))
2380            .json(&request)
2381            .send()
2382            .await
2383            .map_err(|e| AmpError::rpc(format!("Failed to send sendmany RPC request: {e}")))?;
2384
2385        if !response.status().is_success() {
2386            let status = response.status();
2387            let error_body = response
2388                .text()
2389                .await
2390                .unwrap_or_else(|_| "Unable to read error body".to_string());
2391            return Err(AmpError::rpc(format!(
2392                "Sendmany RPC request failed with status: {status} - Body: {error_body}"
2393            )));
2394        }
2395
2396        let rpc_response: RpcResponse<String> = response
2397            .json()
2398            .await
2399            .map_err(|e| AmpError::rpc(format!("Failed to parse sendmany RPC response: {e}")))?;
2400
2401        if let Some(error) = rpc_response.error {
2402            return Err(AmpError::rpc(format!(
2403                "Sendmany RPC error: {} (code: {})",
2404                error.message, error.code
2405            )));
2406        }
2407
2408        let txid = rpc_response.result.unwrap_or_default();
2409        tracing::info!("Successfully sent transaction with sendmany: {}", txid);
2410        Ok(txid)
2411    }
2412
2413    /// Waits for blockchain confirmations with configurable timeout
2414    ///
2415    /// This method polls the blockchain every 15 seconds to check for transaction confirmations.
2416    /// It waits for a minimum number of confirmations (default 2) before returning successfully.
2417    /// The method includes a configurable timeout to prevent indefinite waiting.
2418    ///
2419    /// # Arguments
2420    /// * `txid` - The transaction ID to monitor for confirmations
2421    /// * `min_confirmations` - Minimum number of confirmations required (default: 2)
2422    /// * `timeout_minutes` - Timeout in minutes (default: 10)
2423    ///
2424    /// # Returns
2425    /// Returns the final `TransactionDetail` when sufficient confirmations are reached
2426    ///
2427    /// # Errors
2428    /// Returns `AmpError::Timeout` if the timeout is exceeded before confirmations are received
2429    /// Returns `AmpError::Rpc` if there are issues communicating with the Elements node
2430    ///
2431    /// # Examples
2432    /// ```no_run
2433    /// # use amp_rs::ElementsRpc;
2434    /// # #[tokio::main]
2435    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2436    /// let rpc = ElementsRpc::from_env()?;
2437    /// let tx_detail = rpc.wait_for_confirmations("abc123...", Some(2), Some(10)).await?;
2438    /// println!("Transaction confirmed with {} confirmations", tx_detail.confirmations);
2439    /// # Ok(())
2440    /// # }
2441    /// ```
2442    pub async fn wait_for_confirmations(
2443        &self,
2444        txid: &str,
2445        min_confirmations: Option<u32>,
2446        timeout_minutes: Option<u64>,
2447    ) -> Result<TransactionDetail, AmpError> {
2448        self.wait_for_confirmations_with_interval(txid, min_confirmations, timeout_minutes, None)
2449            .await
2450    }
2451
2452    /// Internal method for waiting for confirmations with configurable poll interval
2453    /// This is primarily used for testing to avoid long waits
2454    ///
2455    /// # Errors
2456    ///
2457    /// Returns an error if:
2458    /// - The timeout is exceeded before confirmations are received
2459    /// - There are issues communicating with the Elements node
2460    /// - The transaction cannot be found or is invalid
2461    #[allow(clippy::cognitive_complexity)]
2462    pub async fn wait_for_confirmations_with_interval(
2463        &self,
2464        txid: &str,
2465        min_confirmations: Option<u32>,
2466        timeout_minutes: Option<u64>,
2467        poll_interval_secs: Option<u64>,
2468    ) -> Result<TransactionDetail, AmpError> {
2469        let min_confirmations = min_confirmations.unwrap_or(2);
2470        let timeout_minutes = timeout_minutes.unwrap_or(10);
2471        let timeout_duration = if timeout_minutes == 0 {
2472            std::time::Duration::from_secs(3) // Minimum 3 seconds for testing
2473        } else {
2474            std::time::Duration::from_secs(timeout_minutes * 60)
2475        };
2476        let poll_interval = std::time::Duration::from_secs(poll_interval_secs.unwrap_or(15));
2477
2478        tracing::info!(
2479            "Starting confirmation monitoring for transaction {} (min_confirmations: {}, timeout: {} minutes)",
2480            txid,
2481            min_confirmations,
2482            timeout_minutes
2483        );
2484
2485        let start_time = std::time::Instant::now();
2486
2487        loop {
2488            // Check if we've exceeded the timeout
2489            if start_time.elapsed() >= timeout_duration {
2490                let error_msg = format!(
2491                    "Timeout waiting for confirmations after {timeout_minutes} minutes. Transaction ID: {txid}. \
2492                    You can retry confirmation by calling the confirmation API with this txid."
2493                );
2494                tracing::error!("{}", error_msg);
2495                return Err(AmpError::Timeout(error_msg));
2496            }
2497
2498            // Get current transaction details
2499            match self.get_transaction(txid).await {
2500                Ok(tx_detail) => {
2501                    tracing::debug!(
2502                        "Transaction {} has {} confirmations (need {})",
2503                        txid,
2504                        tx_detail.confirmations,
2505                        min_confirmations
2506                    );
2507
2508                    if tx_detail.confirmations >= min_confirmations {
2509                        tracing::info!(
2510                            "Transaction {} confirmed with {} confirmations",
2511                            txid,
2512                            tx_detail.confirmations
2513                        );
2514                        return Ok(tx_detail);
2515                    }
2516
2517                    // Log progress every few polls to avoid spam
2518                    if start_time.elapsed().as_secs() % 60 < 15 {
2519                        tracing::info!(
2520                            "Waiting for confirmations: {}/{} (elapsed: {}s)",
2521                            tx_detail.confirmations,
2522                            min_confirmations,
2523                            start_time.elapsed().as_secs()
2524                        );
2525                    }
2526                }
2527                Err(e) => {
2528                    tracing::warn!(
2529                        "Failed to get transaction details for {}: {}. Retrying in {} seconds...",
2530                        txid,
2531                        e,
2532                        poll_interval.as_secs()
2533                    );
2534                    // Continue polling even if individual calls fail, as the transaction
2535                    // might not be visible immediately after broadcasting
2536                }
2537            }
2538
2539            // Wait before next poll
2540            tokio::time::sleep(poll_interval).await;
2541        }
2542    }
2543
2544    /// Selects appropriate UTXOs to cover the required amount plus fees
2545    ///
2546    /// This method implements a simple UTXO selection algorithm that:
2547    /// 1. Filters UTXOs by asset ID and spendability
2548    /// 2. Sorts UTXOs by amount (largest first) for efficiency
2549    /// 3. Selects UTXOs until the target amount plus estimated fees is covered
2550    ///
2551    /// # Arguments
2552    /// * `asset_id` - The asset ID to select UTXOs for
2553    /// * `target_amount` - The total amount needed for distribution
2554    /// * `estimated_fee` - Estimated transaction fee in the same asset
2555    ///
2556    /// # Returns
2557    /// Returns a tuple of (`selected_utxos`, `total_selected_amount`)
2558    ///
2559    /// # Errors
2560    /// Returns an error if insufficient UTXOs are available or RPC calls fail
2561    ///
2562    /// # Examples
2563    /// ```no_run
2564    /// # use amp_rs::ElementsRpc;
2565    /// # #[tokio::main]
2566    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2567    /// let rpc = ElementsRpc::from_env()?;
2568    /// let (selected_utxos, total_amount) = rpc.select_utxos_for_amount(
2569    ///     "wallet_name",
2570    ///     "asset_id_hex",
2571    ///     150.0,
2572    ///     0.001
2573    /// ).await?;
2574    /// println!("Selected {} UTXOs totaling {}", selected_utxos.len(), total_amount);
2575    /// # Ok(())
2576    /// # }
2577    /// ```
2578    pub async fn select_utxos_for_amount(
2579        &self,
2580        wallet_name: &str,
2581        asset_id: &str,
2582        target_amount: f64,
2583        estimated_fee: f64,
2584    ) -> Result<(Vec<Unspent>, f64), AmpError> {
2585        tracing::debug!(
2586            "Selecting UTXOs for asset {} from wallet {} - target: {}, fee: {}",
2587            asset_id,
2588            wallet_name,
2589            target_amount,
2590            estimated_fee
2591        );
2592
2593        // Get all UTXOs for this asset from the specified wallet
2594        let mut utxos = self
2595            .list_unspent_for_wallet(wallet_name, Some(asset_id))
2596            .await?;
2597
2598        // Filter for spendable UTXOs only
2599        utxos.retain(|utxo| utxo.spendable && utxo.asset == asset_id);
2600
2601        if utxos.is_empty() {
2602            return Err(AmpError::validation(format!(
2603                "No spendable UTXOs found for asset {asset_id}. \
2604                This typically means:\n\
2605                1. The treasury address is not imported in the Elements node as a watch-only address\n\
2606                2. The asset issuance transaction hasn't been confirmed yet\n\
2607                3. The UTXOs have already been spent\n\
2608                \n\
2609                To fix this:\n\
2610                - Ensure the treasury address is imported: `elements-cli importaddress <treasury_address> treasury false`\n\
2611                - Wait for the asset issuance transaction to be confirmed\n\
2612                - Check that the treasury address matches the one used for asset issuance"
2613            )));
2614        }
2615
2616        // Sort UTXOs by amount (largest first) for efficient selection
2617        utxos.sort_by(|a, b| {
2618            b.amount
2619                .partial_cmp(&a.amount)
2620                .unwrap_or(std::cmp::Ordering::Equal)
2621        });
2622
2623        let required_amount = target_amount + estimated_fee;
2624        let mut selected_utxos = Vec::new();
2625        let mut total_selected = 0.0;
2626
2627        // Select UTXOs until we have enough to cover the required amount
2628        for utxo in utxos {
2629            selected_utxos.push(utxo.clone());
2630            total_selected += utxo.amount;
2631
2632            if total_selected >= required_amount {
2633                break;
2634            }
2635        }
2636
2637        // Check if we have sufficient funds
2638        if total_selected < required_amount {
2639            return Err(AmpError::validation(format!(
2640                "Insufficient UTXOs: need {required_amount}, have {total_selected} (target: {target_amount}, fee: {estimated_fee})"
2641            )));
2642        }
2643
2644        tracing::info!(
2645            "Selected {} UTXOs totaling {} for target {} + fee {}",
2646            selected_utxos.len(),
2647            total_selected,
2648            target_amount,
2649            estimated_fee
2650        );
2651
2652        Ok((selected_utxos, total_selected))
2653    }
2654
2655    /// Builds a raw transaction for asset distribution with proper change handling
2656    ///
2657    /// This method orchestrates the complete transaction building process:
2658    /// 1. Selects appropriate UTXOs using `select_utxos_for_amount`
2659    /// 2. Creates transaction inputs from selected UTXOs
2660    /// 3. Creates outputs for distribution addresses
2661    /// 4. Calculates and creates change output if necessary
2662    /// 5. Builds the raw transaction using `create_raw_transaction`
2663    ///
2664    /// # Arguments
2665    /// * `asset_id` - The asset ID being distributed
2666    /// * `address_amounts` - Map of recipient addresses to amounts
2667    /// * `change_address` - Address to send change to (if any)
2668    /// * `estimated_fee` - Estimated transaction fee
2669    ///
2670    /// # Returns
2671    /// Returns a tuple of (`raw_transaction_hex`, `selected_utxos`, `change_amount`)
2672    ///
2673    /// # Errors
2674    /// Returns an error if UTXO selection fails or transaction building fails
2675    ///
2676    /// # Examples
2677    /// ```no_run
2678    /// # use amp_rs::ElementsRpc;
2679    /// # use std::collections::HashMap;
2680    /// # #[tokio::main]
2681    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2682    /// let rpc = ElementsRpc::from_env()?;
2683    /// let mut address_amounts = HashMap::new();
2684    /// address_amounts.insert("address1".to_string(), 100.0);
2685    /// address_amounts.insert("address2".to_string(), 50.0);
2686    ///
2687    /// let (raw_tx, utxos, change) = rpc.build_distribution_transaction(
2688    ///     "wallet_name",
2689    ///     "asset_id_hex",
2690    ///     address_amounts,
2691    ///     "change_address",
2692    ///     0.001
2693    /// ).await?;
2694    /// println!("Built transaction with {} inputs, change: {}", utxos.len(), change);
2695    /// # Ok(())
2696    /// # }
2697    /// ```
2698    #[allow(clippy::cognitive_complexity)]
2699    #[allow(clippy::too_many_lines)]
2700    pub async fn build_distribution_transaction(
2701        &self,
2702        wallet_name: &str,
2703        asset_id: &str,
2704        address_amounts: std::collections::HashMap<String, f64>,
2705        change_address: &str,
2706        _estimated_fee: f64,
2707    ) -> Result<(String, Vec<Unspent>, f64), AmpError> {
2708        const DUST_THRESHOLD: f64 = 0.00001;
2709        const LBTC_ASSET_ID: &str =
2710            "144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49"; // L-BTC on Liquid testnet
2711
2712        tracing::debug!(
2713            "Building distribution transaction for asset {} with {} outputs",
2714            asset_id,
2715            address_amounts.len()
2716        );
2717
2718        // Calculate total distribution amount
2719        let total_distribution: f64 = address_amounts.values().sum();
2720
2721        if total_distribution <= 0.0 {
2722            return Err(AmpError::validation(
2723                "Total distribution amount must be greater than zero".to_string(),
2724            ));
2725        }
2726
2727        // Select UTXOs to cover the distribution (custom asset)
2728        let (selected_asset_utxos, total_selected) = self
2729            .select_utxos_for_amount(wallet_name, asset_id, total_distribution, 0.0)
2730            .await?;
2731
2732        // Also select L-BTC UTXOs for transaction fees
2733        // Elements requires L-BTC inputs for fees even when distributing custom assets
2734        let min_lbtc_fee = 0.00001; // Minimum L-BTC needed for fees
2735        let (selected_lbtc_utxos, lbtc_total) = match self
2736            .select_utxos_for_amount(wallet_name, LBTC_ASSET_ID, 0.0, min_lbtc_fee)
2737            .await
2738        {
2739            Ok((utxos, total)) => {
2740                tracing::info!(
2741                    "Selected {} L-BTC UTXOs totaling {} for fees",
2742                    utxos.len(),
2743                    total
2744                );
2745                (utxos, total)
2746            }
2747            Err(e) => {
2748                tracing::warn!(
2749                    "Could not select L-BTC UTXOs for fees: {}. Transaction may fail.",
2750                    e
2751                );
2752                (Vec::new(), 0.0)
2753            }
2754        };
2755
2756        // Combine custom asset UTXOs and L-BTC UTXOs
2757        let mut all_utxos = selected_asset_utxos.clone();
2758        all_utxos.extend(selected_lbtc_utxos.clone());
2759
2760        if selected_lbtc_utxos.is_empty() {
2761            tracing::warn!(
2762                "No L-BTC UTXOs selected for fees. Transaction may fail during broadcast."
2763            );
2764        } else {
2765            tracing::info!(
2766                "Transaction includes {} custom asset UTXOs and {} L-BTC UTXOs for fees",
2767                selected_asset_utxos.len(),
2768                selected_lbtc_utxos.len()
2769            );
2770        }
2771
2772        // Create transaction inputs from all selected UTXOs
2773        let inputs: Vec<TxInput> = all_utxos
2774            .iter()
2775            .map(|utxo| TxInput {
2776                txid: utxo.txid.clone(),
2777                vout: utxo.vout,
2778                sequence: None, // Use default sequence
2779            })
2780            .collect();
2781
2782        // Create outputs for distribution (custom asset)
2783        // We need to track outputs as a vector since we may have multiple outputs to the same address
2784        // (e.g., custom asset change + L-BTC change to the same change address)
2785        let mut output_list = Vec::new();
2786
2787        // Add distribution outputs (custom asset)
2788        for (address, amount) in &address_amounts {
2789            output_list.push((address.clone(), *amount, asset_id.to_string()));
2790        }
2791
2792        // Calculate change amount for custom asset (total selected - distribution)
2793        let asset_change_amount = total_selected - total_distribution;
2794
2795        // Add asset change output if there's a significant amount left
2796        if asset_change_amount > DUST_THRESHOLD {
2797            output_list.push((
2798                change_address.to_string(),
2799                asset_change_amount,
2800                asset_id.to_string(),
2801            ));
2802
2803            tracing::debug!(
2804                "Adding asset change output: {} {} to address {}",
2805                asset_change_amount,
2806                asset_id,
2807                change_address
2808            );
2809        } else if asset_change_amount > 0.0 {
2810            tracing::warn!(
2811                "Asset change amount {} is below dust threshold {}, will be lost",
2812                asset_change_amount,
2813                DUST_THRESHOLD
2814            );
2815        }
2816
2817        // Handle L-BTC change if we selected L-BTC UTXOs for fees
2818        // In Elements, the fee is implicit - it's the difference between L-BTC inputs and outputs
2819        // We should NOT subtract the fee from outputs; Elements calculates it automatically
2820        if !selected_lbtc_utxos.is_empty() {
2821            tracing::debug!(
2822                "L-BTC input total: {}, minimum fee needed: {}",
2823                lbtc_total,
2824                min_lbtc_fee
2825            );
2826
2827            // Check if we have enough L-BTC for the minimum fee
2828            if lbtc_total < min_lbtc_fee {
2829                return Err(AmpError::validation(format!(
2830                    "Insufficient L-BTC for fees: have {lbtc_total}, need at least {min_lbtc_fee}"
2831                )));
2832            }
2833
2834            // For now, let's try NOT adding any L-BTC change output
2835            // and let Elements handle the fee automatically from the input/output difference
2836            tracing::info!(
2837                "Using L-BTC input {} for fees - no explicit L-BTC change output (Elements will handle fee automatically)",
2838                lbtc_total
2839            );
2840
2841            // Note: If this approach works, the entire L-BTC input will become the fee
2842            // If we need change, we'll need to figure out the correct way to handle it
2843        }
2844
2845        // For confidential addresses, we need to import them into the wallet first
2846        // so Elements knows about the blinding keys
2847        for address in address_amounts.keys() {
2848            if address.starts_with('v') {
2849                // Confidential address
2850                tracing::debug!("Importing confidential address into wallet: {}", address);
2851                if let Err(e) = self
2852                    .import_address_to_wallet(wallet_name, address, None, false)
2853                    .await
2854                {
2855                    tracing::warn!("Failed to import confidential address {}: {}", address, e);
2856                    // Continue anyway - the address might already be imported
2857                }
2858            }
2859        }
2860
2861        // Build the raw transaction using wallet-specific endpoint for confidential transactions
2862        // For confidential transactions, we need to use blindrawtransaction to properly handle blinding
2863        let raw_transaction = self
2864            .create_raw_transaction_with_outputs(wallet_name, inputs, output_list)
2865            .await
2866            .map_err(|e| {
2867                // Provide more helpful error message for the common L-BTC fee issue
2868                if e.to_string().contains("bad-txns-in-ne-out") || e.to_string().contains("value in != value out") {
2869                    AmpError::validation(format!(
2870                        "Transaction failed due to confidential transaction blinding mismatch. \
2871                        This occurs when Elements creates blinding factors that don't match LWK's expectations. \
2872                        To fix this:\n\
2873                        1. Ensure the wallet has proper blinding keys for all addresses\n\
2874                        2. Use blindrawtransaction before signing\n\
2875                        3. Verify UTXO blinding factors match between Elements and LWK\n\
2876                        4. Original error: {e}"
2877                    ))
2878                } else {
2879                    e.with_context("Failed to build distribution transaction")
2880                }
2881            })?;
2882
2883        // For confidential transactions, we need to blind the transaction properly
2884        // This ensures the blinding factors are compatible with LWK signing
2885        tracing::debug!("Blinding raw transaction for confidential asset distribution");
2886        let blinded_transaction = self
2887            .blind_raw_transaction(wallet_name, &raw_transaction)
2888            .await
2889            .map_err(|e| {
2890                tracing::warn!(
2891                    "Failed to blind transaction, proceeding with unblinded: {}",
2892                    e
2893                );
2894                // If blinding fails, we'll try to proceed with the unblinded transaction
2895                // This might work for some cases but could fail during broadcast
2896                e.with_context("Transaction blinding failed")
2897            })
2898            .unwrap_or_else(|_| {
2899                tracing::warn!("Using unblinded transaction - this may cause broadcast failures");
2900                raw_transaction.clone()
2901            });
2902
2903        tracing::info!(
2904            "Built distribution transaction: {} inputs, {} outputs, asset change: {}",
2905            all_utxos.len(),
2906            address_amounts.len() + usize::from(asset_change_amount > DUST_THRESHOLD),
2907            if asset_change_amount > DUST_THRESHOLD {
2908                asset_change_amount
2909            } else {
2910                0.0
2911            }
2912        );
2913
2914        Ok((blinded_transaction, all_utxos, asset_change_amount))
2915    }
2916
2917    /// Creates a raw transaction with multiple outputs that can handle multiple assets to the same address
2918    ///
2919    /// This method is similar to `create_raw_transaction_with_wallet` but handles the case where
2920    /// multiple outputs with different assets need to go to the same address (e.g., asset change + L-BTC change).
2921    ///
2922    /// # Arguments
2923    /// * `wallet_name` - Name of the Elements wallet to use
2924    /// * `inputs` - Vector of transaction inputs
2925    /// * `outputs` - Vector of (address, amount, `asset_id`) tuples
2926    ///
2927    /// # Returns
2928    /// Returns the raw transaction hex string
2929    #[allow(clippy::cognitive_complexity)]
2930    async fn create_raw_transaction_with_outputs(
2931        &self,
2932        wallet_name: &str,
2933        inputs: Vec<TxInput>,
2934        outputs: Vec<(String, f64, String)>, // (address, amount, asset_id)
2935    ) -> Result<String, AmpError> {
2936        tracing::debug!(
2937            "Creating raw transaction with wallet {} - {} inputs and {} outputs",
2938            wallet_name,
2939            inputs.len(),
2940            outputs.len()
2941        );
2942
2943        // First load the wallet to ensure it's available
2944        self.load_wallet(wallet_name).await?;
2945
2946        // Elements RPC createrawtransaction expects outputs as an array of objects
2947        // Each output object should contain both address, amount, and asset
2948        let mut outputs_array = Vec::new();
2949
2950        for (address, amount, asset_id) in &outputs {
2951            // Convert amount to string with proper precision for Elements
2952            let amount_str = format!("{amount:.8}");
2953
2954            outputs_array.push(serde_json::json!({
2955                address.clone(): amount_str,
2956                "asset": asset_id
2957            }));
2958        }
2959
2960        let params = serde_json::json!([
2961            inputs,        // inputs as TxInput array
2962            outputs_array, // outputs as array of {address: amount, asset: id} objects
2963            0,             // locktime (0 = no locktime)
2964            false,         // replaceable (false = not replaceable)
2965        ]);
2966
2967        // Debug: Log the exact parameters being sent to createrawtransaction
2968        tracing::error!("createrawtransaction parameters (wallet-specific, corrected format):");
2969        tracing::error!("  wallet: {}", wallet_name);
2970        tracing::error!(
2971            "  inputs: {}",
2972            serde_json::to_string_pretty(&inputs).unwrap_or_default()
2973        );
2974        tracing::error!(
2975            "  outputs_array: {}",
2976            serde_json::to_string_pretty(&outputs_array).unwrap_or_default()
2977        );
2978
2979        // Use the wallet-specific RPC endpoint
2980        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
2981
2982        let request = RpcRequest {
2983            jsonrpc: "1.0".to_string(),
2984            id: "amp-client".to_string(),
2985            method: "createrawtransaction".to_string(),
2986            params,
2987        };
2988
2989        let response = self
2990            .client
2991            .post(&wallet_url)
2992            .basic_auth(&self.username, Some(&self.password))
2993            .json(&request)
2994            .send()
2995            .await
2996            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
2997
2998        if !response.status().is_success() {
2999            let status = response.status();
3000            let error_body = response
3001                .text()
3002                .await
3003                .unwrap_or_else(|_| "Unable to read error body".to_string());
3004            return Err(AmpError::rpc(format!(
3005                "RPC request failed with status: {status} - Body: {error_body}"
3006            )));
3007        }
3008
3009        let rpc_response: RpcResponse<String> = response
3010            .json()
3011            .await
3012            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
3013
3014        if let Some(error) = rpc_response.error {
3015            return Err(AmpError::rpc(format!(
3016                "RPC error creating raw transaction: {} (code: {})",
3017                error.message, error.code
3018            )));
3019        }
3020
3021        Ok(rpc_response.result.unwrap_or_default())
3022    }
3023
3024    /// Blinds a raw transaction for confidential transactions
3025    ///
3026    /// This method uses Elements' blindrawtransaction RPC to properly blind a transaction
3027    /// for confidential asset transfers. This is crucial for Liquid transactions to ensure
3028    /// the blinding factors are properly balanced.
3029    ///
3030    /// # Arguments
3031    /// * `wallet_name` - Name of the Elements wallet to use for blinding
3032    /// * `raw_transaction` - The raw transaction hex to blind
3033    ///
3034    /// # Returns
3035    /// Returns the blinded transaction hex string
3036    ///
3037    /// # Errors
3038    /// Returns an error if the RPC call fails or blinding is not possible
3039    pub async fn blind_raw_transaction(
3040        &self,
3041        wallet_name: &str,
3042        raw_transaction: &str,
3043    ) -> Result<String, AmpError> {
3044        tracing::debug!(
3045            "Blinding raw transaction for wallet {} - tx length: {} chars",
3046            wallet_name,
3047            raw_transaction.len()
3048        );
3049
3050        // First load the wallet to ensure it's available
3051        self.load_wallet(wallet_name).await?;
3052
3053        // Elements blindrawtransaction parameters:
3054        // 1. Raw transaction hex
3055        // 2. Input blinding data (can be empty array for auto-detection)
3056        // 3. Input amounts (can be empty array for auto-detection from UTXOs)
3057        // 4. Input assets (can be empty array for auto-detection from UTXOs)
3058        // 5. Input asset blinders (can be empty array for auto-detection)
3059        // 6. Input amount blinders (can be empty array for auto-detection)
3060        let params = serde_json::json!([
3061            raw_transaction, // Raw transaction hex
3062            [],              // Input blinding data (empty for auto-detection)
3063            [],              // Input amounts (empty for auto-detection)
3064            [],              // Input assets (empty for auto-detection)
3065            [],              // Input asset blinders (empty for auto-detection)
3066            []               // Input amount blinders (empty for auto-detection)
3067        ]);
3068
3069        // Use the wallet-specific RPC endpoint
3070        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
3071
3072        let request = RpcRequest {
3073            jsonrpc: "1.0".to_string(),
3074            id: "amp-client".to_string(),
3075            method: "blindrawtransaction".to_string(),
3076            params,
3077        };
3078
3079        let response = self
3080            .client
3081            .post(&wallet_url)
3082            .basic_auth(&self.username, Some(&self.password))
3083            .json(&request)
3084            .send()
3085            .await
3086            .map_err(|e| {
3087                AmpError::rpc(format!("Failed to send blindrawtransaction request: {e}"))
3088            })?;
3089
3090        if !response.status().is_success() {
3091            let status = response.status();
3092            let error_body = response
3093                .text()
3094                .await
3095                .unwrap_or_else(|_| "Unable to read error body".to_string());
3096            return Err(AmpError::rpc(format!(
3097                "blindrawtransaction failed with status: {status} - Body: {error_body}"
3098            )));
3099        }
3100
3101        let rpc_response: RpcResponse<String> = response.json().await.map_err(|e| {
3102            AmpError::rpc(format!("Failed to parse blindrawtransaction response: {e}"))
3103        })?;
3104
3105        if let Some(error) = rpc_response.error {
3106            return Err(AmpError::rpc(format!(
3107                "RPC error blinding transaction: {} (code: {})",
3108                error.message, error.code
3109            )));
3110        }
3111
3112        let blinded_tx = rpc_response.result.unwrap_or_default();
3113
3114        tracing::info!(
3115            "Successfully blinded transaction - original: {} chars, blinded: {} chars",
3116            raw_transaction.len(),
3117            blinded_tx.len()
3118        );
3119
3120        Ok(blinded_tx)
3121    }
3122
3123    /// Signs a raw transaction using the provided signer callback
3124    ///
3125    /// This method integrates with the Signer trait to sign unsigned transactions.
3126    /// It handles the complete signing workflow including:
3127    /// 1. Validation of the unsigned transaction hex format
3128    /// 2. Calling the signer's `sign_transaction` method
3129    /// 3. Validation of the signed transaction format and structure
3130    /// 4. Proper error handling and context propagation
3131    ///
3132    /// # Arguments
3133    /// * `unsigned_tx_hex` - The unsigned transaction in hexadecimal format
3134    /// * `signer` - Implementation of the Signer trait for transaction signing
3135    ///
3136    /// # Returns
3137    /// Returns the signed transaction as a hex string
3138    ///
3139    /// # Errors
3140    /// Returns an error if:
3141    /// - The unsigned transaction hex is invalid or malformed
3142    /// - The signer fails to sign the transaction
3143    /// - The signed transaction format is invalid
3144    /// - Any validation checks fail
3145    ///
3146    /// # Examples
3147    /// ```no_run
3148    /// # use amp_rs::{ElementsRpc, signer::{Signer, LwkSoftwareSigner}};
3149    /// # #[tokio::main]
3150    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3151    /// let rpc = ElementsRpc::from_env()?;
3152    /// let (_, signer) = LwkSoftwareSigner::generate_new()?;
3153    /// let unsigned_tx = "020000000001..."; // Unsigned transaction hex
3154    /// let signed_tx = rpc.sign_transaction(unsigned_tx, &signer).await?;
3155    /// println!("Transaction signed successfully: {}", signed_tx);
3156    /// # Ok(())
3157    /// # }
3158    /// ```
3159    #[allow(clippy::cognitive_complexity)]
3160    pub async fn sign_transaction(
3161        &self,
3162        unsigned_tx_hex: &str,
3163        signer: &dyn crate::signer::Signer,
3164    ) -> Result<String, AmpError> {
3165        const MIN_TX_SIZE: usize = 10; // Minimum bytes for a valid transaction
3166
3167        tracing::debug!(
3168            "Signing transaction: {}...",
3169            &unsigned_tx_hex[..std::cmp::min(unsigned_tx_hex.len(), 64)]
3170        );
3171
3172        // Validate unsigned transaction hex format
3173        if unsigned_tx_hex.is_empty() {
3174            return Err(AmpError::validation(
3175                "Unsigned transaction hex cannot be empty".to_string(),
3176            ));
3177        }
3178
3179        // Check if hex string has valid format (even length, valid hex characters)
3180        if unsigned_tx_hex.len() % 2 != 0 {
3181            return Err(AmpError::validation(
3182                "Unsigned transaction hex must have even length".to_string(),
3183            ));
3184        }
3185
3186        // Validate hex characters
3187        if !unsigned_tx_hex.chars().all(|c| c.is_ascii_hexdigit()) {
3188            return Err(AmpError::validation(
3189                "Unsigned transaction contains invalid hex characters".to_string(),
3190            ));
3191        }
3192
3193        // Attempt to decode hex to validate transaction structure
3194        let tx_bytes = hex::decode(unsigned_tx_hex).map_err(|e| {
3195            AmpError::validation(format!("Failed to decode unsigned transaction hex: {e}"))
3196        })?;
3197
3198        tracing::debug!("Unsigned transaction validation passed, calling signer");
3199
3200        // Call the signer to sign the transaction
3201        let signed_tx_hex = signer
3202            .sign_transaction(unsigned_tx_hex)
3203            .await
3204            .map_err(|e| {
3205                tracing::error!("Transaction signing failed: {}", e);
3206                AmpError::Signer(e).with_context("Failed to sign transaction")
3207            })?;
3208
3209        tracing::debug!(
3210            "Signer returned signed transaction: {}...",
3211            &signed_tx_hex[..std::cmp::min(signed_tx_hex.len(), 64)]
3212        );
3213
3214        // Validate signed transaction format and structure
3215        if signed_tx_hex.is_empty() {
3216            return Err(AmpError::validation(
3217                "Signed transaction hex cannot be empty".to_string(),
3218            ));
3219        }
3220
3221        // Check if signed transaction has valid hex format
3222        if signed_tx_hex.len() % 2 != 0 {
3223            return Err(AmpError::validation(
3224                "Signed transaction hex must have even length".to_string(),
3225            ));
3226        }
3227
3228        // Validate hex characters in signed transaction
3229        if !signed_tx_hex.chars().all(|c| c.is_ascii_hexdigit()) {
3230            return Err(AmpError::validation(
3231                "Signed transaction contains invalid hex characters".to_string(),
3232            ));
3233        }
3234
3235        // Attempt to decode signed transaction to validate structure
3236        let signed_tx_bytes = hex::decode(&signed_tx_hex).map_err(|e| {
3237            AmpError::validation(format!("Failed to decode signed transaction hex: {e}"))
3238        })?;
3239
3240        // Basic validation: signed transaction should be at least as long as unsigned
3241        // (signatures add data, so signed tx should be larger or equal)
3242        if signed_tx_bytes.len() < tx_bytes.len() {
3243            return Err(AmpError::validation(
3244                "Signed transaction is shorter than unsigned transaction, which is invalid"
3245                    .to_string(),
3246            ));
3247        }
3248
3249        // Additional validation: check that the transaction structure is reasonable
3250        // Minimum transaction size for Elements (very basic check)
3251        if signed_tx_bytes.len() < MIN_TX_SIZE {
3252            return Err(AmpError::validation(format!(
3253                "Signed transaction does not meet minimum size ({} bytes), minimum is {} bytes",
3254                signed_tx_bytes.len(),
3255                MIN_TX_SIZE
3256            )));
3257        }
3258
3259        tracing::info!(
3260            "Transaction signed successfully - unsigned: {} bytes, signed: {} bytes",
3261            tx_bytes.len(),
3262            signed_tx_bytes.len()
3263        );
3264
3265        Ok(signed_tx_hex)
3266    }
3267
3268    /// Signs and broadcasts a transaction in a single operation
3269    ///
3270    /// This is a convenience method that combines transaction signing and broadcasting.
3271    /// It performs the complete workflow of signing an unsigned transaction and
3272    /// immediately broadcasting it to the network.
3273    ///
3274    /// # Arguments
3275    /// * `unsigned_tx_hex` - The unsigned transaction in hexadecimal format
3276    /// * `signer` - Implementation of the Signer trait for transaction signing
3277    ///
3278    /// # Returns
3279    /// Returns the transaction ID of the broadcast transaction
3280    ///
3281    /// # Errors
3282    /// Returns an error if signing or broadcasting fails
3283    ///
3284    /// # Examples
3285    /// ```no_run
3286    /// # use amp_rs::{ElementsRpc, signer::{Signer, LwkSoftwareSigner}};
3287    /// # #[tokio::main]
3288    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3289    /// let rpc = ElementsRpc::from_env()?;
3290    /// let (_, signer) = LwkSoftwareSigner::generate_new()?;
3291    /// let unsigned_tx = "020000000001..."; // Unsigned transaction hex
3292    /// let txid = rpc.sign_and_broadcast_transaction(unsigned_tx, &signer).await?;
3293    /// println!("Transaction broadcast with ID: {}", txid);
3294    /// # Ok(())
3295    /// # }
3296    /// ```
3297    pub async fn sign_and_broadcast_transaction(
3298        &self,
3299        unsigned_tx_hex: &str,
3300        signer: &dyn crate::signer::Signer,
3301    ) -> Result<String, AmpError> {
3302        tracing::info!("Signing and broadcasting transaction");
3303
3304        // Sign the transaction
3305        let signed_tx_hex = self
3306            .sign_transaction(unsigned_tx_hex, signer)
3307            .await
3308            .map_err(|e| e.with_context("Failed during transaction signing phase"))?;
3309
3310        // Broadcast the signed transaction
3311        let txid = self
3312            .send_raw_transaction(&signed_tx_hex)
3313            .await
3314            .map_err(|e| e.with_context("Failed during transaction broadcast phase"))?;
3315
3316        tracing::info!("Successfully signed and broadcast transaction: {}", txid);
3317        Ok(txid)
3318    }
3319
3320    /// Signs and broadcasts a transaction with UTXO information for proper PSBT construction
3321    ///
3322    /// This method provides UTXO information to the signer for proper PSBT construction,
3323    /// which is required for confidential transactions where the signer needs to know
3324    /// the previous transaction outputs being spent.
3325    ///
3326    /// # Arguments
3327    /// * `unsigned_tx_hex` - The unsigned transaction in hexadecimal format
3328    /// * `utxos` - Vector of UTXOs being spent in the transaction
3329    /// * `signer` - Implementation of the Signer trait for transaction signing
3330    ///
3331    /// # Returns
3332    /// Returns the transaction ID of the broadcast transaction
3333    ///
3334    /// # Errors
3335    /// Returns an error if signing or broadcasting fails
3336    #[allow(clippy::cognitive_complexity)]
3337    pub async fn sign_and_broadcast_transaction_with_utxos(
3338        &self,
3339        unsigned_tx_hex: &str,
3340        utxos: &[Unspent],
3341        signer: &dyn crate::signer::Signer,
3342    ) -> Result<String, AmpError> {
3343        tracing::info!(
3344            "Signing and broadcasting transaction with {} UTXOs",
3345            utxos.len()
3346        );
3347
3348        // Try to use the enhanced signing method if the signer supports it
3349        let signed_tx_hex = if let Some(lwk_signer) = signer
3350            .as_any()
3351            .downcast_ref::<crate::signer::LwkSoftwareSigner>(
3352        ) {
3353            // Use the enhanced signing method with UTXO information
3354            tracing::debug!("Using LWK signer with UTXO information");
3355            lwk_signer
3356                .sign_transaction_with_utxos(unsigned_tx_hex, utxos)
3357                .await
3358                .map_err(|e| {
3359                    AmpError::Signer(e)
3360                        .with_context("Failed during enhanced transaction signing phase")
3361                })?
3362        } else {
3363            // Fall back to standard signing method
3364            tracing::debug!("Using standard signing method (no UTXO information)");
3365            self.sign_transaction(unsigned_tx_hex, signer)
3366                .await
3367                .map_err(|e| e.with_context("Failed during transaction signing phase"))?
3368        };
3369
3370        // Broadcast the signed transaction
3371        let txid = self
3372            .send_raw_transaction(&signed_tx_hex)
3373            .await
3374            .map_err(|e| e.with_context("Failed during transaction broadcast phase"))?;
3375
3376        tracing::info!("Successfully signed and broadcast transaction: {}", txid);
3377        Ok(txid)
3378    }
3379
3380    /// Collects change data from a confirmed transaction for distribution confirmation
3381    ///
3382    /// This method queries the Elements node to find change UTXOs from a specific transaction
3383    /// that belong to the specified asset. It's used after a distribution transaction is
3384    /// confirmed to collect the change outputs for the final confirmation API call.
3385    ///
3386    /// # Arguments
3387    /// * `asset_id` - The asset ID to filter change UTXOs for
3388    /// * `txid` - The transaction ID to filter change UTXOs from
3389    ///
3390    /// # Returns
3391    /// Returns a vector of Unspent UTXOs that represent change outputs from the transaction.
3392    /// Returns an empty vector if no change outputs exist for the specified asset and transaction.
3393    ///
3394    /// # Errors
3395    /// Returns an error if the RPC call fails or if there are issues querying the Elements node
3396    ///
3397    /// # Examples
3398    /// ```no_run
3399    /// # use amp_rs::ElementsRpc;
3400    /// # #[tokio::main]
3401    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3402    /// let rpc = ElementsRpc::from_env()?;
3403    /// let change_data = rpc.collect_change_data(
3404    ///     "asset_id_hex",
3405    ///     "transaction_id_hex",
3406    ///     &rpc,
3407    ///     "wallet_name"
3408    /// ).await?;
3409    ///
3410    /// if change_data.is_empty() {
3411    ///     println!("No change outputs found for this transaction");
3412    /// } else {
3413    ///     println!("Found {} change outputs", change_data.len());
3414    /// }
3415    /// # Ok(())
3416    /// # }
3417    /// ```
3418    #[allow(clippy::cognitive_complexity)]
3419    pub async fn collect_change_data(
3420        &self,
3421        asset_id: &str,
3422        txid: &str,
3423        node_rpc: &Self,
3424        wallet_name: &str,
3425    ) -> Result<Vec<Unspent>, AmpError> {
3426        tracing::debug!(
3427            "Collecting change data for asset {} from transaction {}",
3428            asset_id,
3429            txid
3430        );
3431
3432        // Use the raw listunspent RPC call to get full blinding information
3433        // This is essential for confidential transactions as the AMP API requires
3434        // both amountblinder and assetblinder fields
3435        let all_utxos = node_rpc
3436            .list_unspent_with_blinding_data(wallet_name)
3437            .await
3438            .map_err(|e| {
3439                e.with_context(
3440                    "Failed to query unspent outputs with blinding data for change data collection",
3441                )
3442            })?;
3443
3444        // Filter UTXOs to only include those from the specified transaction
3445        let change_utxos: Vec<Unspent> = all_utxos
3446            .into_iter()
3447            .filter(|utxo| {
3448                // Match UTXOs that:
3449                // 1. Come from the specified transaction (txid matches)
3450                // 2. Are for the correct asset
3451                // 3. Are spendable
3452                utxo.txid == txid && utxo.asset == asset_id && utxo.spendable
3453            })
3454            .collect();
3455
3456        tracing::info!(
3457            "Collected {} change UTXOs for asset {} from transaction {}",
3458            change_utxos.len(),
3459            asset_id,
3460            txid
3461        );
3462
3463        // Log details of found change UTXOs for debugging
3464        for (index, utxo) in change_utxos.iter().enumerate() {
3465            tracing::debug!(
3466                "Change UTXO {}: txid={}, vout={}, amount={}, asset={}, amountblinder={:?}, assetblinder={:?}",
3467                index + 1,
3468                utxo.txid,
3469                utxo.vout,
3470                utxo.amount,
3471                utxo.asset,
3472                utxo.amountblinder,
3473                utxo.assetblinder
3474            );
3475        }
3476
3477        // Handle the case where no change outputs exist
3478        if change_utxos.is_empty() {
3479            tracing::info!(
3480                "No change outputs found for asset {} in transaction {} - this is normal if all funds were distributed",
3481                asset_id,
3482                txid
3483            );
3484        }
3485
3486        Ok(change_utxos)
3487    }
3488
3489    /// Lists unspent outputs with full blinding data for confidential transactions
3490    ///
3491    /// This method calls the raw `listunspent` RPC to get complete UTXO information
3492    /// including blinding data (amountblinder and assetblinder) which is required
3493    /// for confidential transaction confirmation with the AMP API.
3494    ///
3495    /// # Arguments
3496    /// * `wallet_name` - Name of the Elements wallet to query
3497    ///
3498    /// # Returns
3499    /// Returns a vector of `Unspent` structs with complete blinding information
3500    ///
3501    /// # Errors
3502    /// Returns an error if the RPC call fails
3503    ///
3504    /// # Examples
3505    /// ```no_run
3506    /// # use amp_rs::ElementsRpc;
3507    /// # #[tokio::main]
3508    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3509    /// let rpc = ElementsRpc::from_env()?;
3510    /// let utxos = rpc.list_unspent_with_blinding_data("wallet_name").await?;
3511    /// for utxo in utxos {
3512    ///     println!("UTXO: {} with blinders: {:?}, {:?}",
3513    ///              utxo.txid, utxo.amountblinder, utxo.assetblinder);
3514    /// }
3515    /// # Ok(())
3516    /// # }
3517    /// ```
3518    pub async fn list_unspent_with_blinding_data(
3519        &self,
3520        wallet_name: &str,
3521    ) -> Result<Vec<Unspent>, AmpError> {
3522        tracing::debug!(
3523            "Listing unspent outputs with blinding data for wallet: {}",
3524            wallet_name
3525        );
3526
3527        // First load the wallet to ensure it's available
3528        self.load_wallet(wallet_name).await?;
3529
3530        // Call listunspent with parameters to get all UTXOs
3531        // Parameters: minconf, maxconf, addresses, include_unsafe, query_options
3532        let params = serde_json::json!([
3533            0,         // minconf: include unconfirmed
3534            9_999_999, // maxconf: include all confirmed
3535            [],        // addresses: empty array means all addresses
3536            true,      // include_unsafe: include unconfirmed transactions
3537            {}         // query_options: empty object for default options
3538        ]);
3539
3540        // Use the wallet-specific RPC endpoint
3541        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
3542
3543        let request = RpcRequest {
3544            jsonrpc: "1.0".to_string(),
3545            id: "amp-client".to_string(),
3546            method: "listunspent".to_string(),
3547            params,
3548        };
3549
3550        let response = self
3551            .client
3552            .post(&wallet_url)
3553            .basic_auth(&self.username, Some(&self.password))
3554            .json(&request)
3555            .send()
3556            .await
3557            .map_err(|e| AmpError::rpc(format!("Failed to send listunspent RPC request: {e}")))?;
3558
3559        if !response.status().is_success() {
3560            let status = response.status();
3561            let error_body = response
3562                .text()
3563                .await
3564                .unwrap_or_else(|_| "Unable to read error body".to_string());
3565            return Err(AmpError::rpc(format!(
3566                "Listunspent RPC request failed with status: {status} - Body: {error_body}"
3567            )));
3568        }
3569
3570        let rpc_response: RpcResponse<Vec<Unspent>> = response
3571            .json()
3572            .await
3573            .map_err(|e| AmpError::rpc(format!("Failed to parse listunspent RPC response: {e}")))?;
3574
3575        if let Some(error) = rpc_response.error {
3576            return Err(AmpError::rpc(format!(
3577                "Listunspent RPC error: {} (code: {})",
3578                error.message, error.code
3579            )));
3580        }
3581
3582        let utxos = rpc_response.result.unwrap_or_default();
3583        tracing::info!(
3584            "Retrieved {} UTXOs with blinding data from wallet {}",
3585            utxos.len(),
3586            wallet_name
3587        );
3588
3589        Ok(utxos)
3590    }
3591
3592    /// Creates a standard wallet in Elements (Elements-first approach)
3593    ///
3594    /// This method creates a new standard wallet in the Elements node that can generate
3595    /// addresses and private keys. This is part of the Elements-first approach where
3596    /// we create the wallet in Elements first, then export keys to LWK.
3597    ///
3598    /// # Arguments
3599    /// * `wallet_name` - Name for the new wallet
3600    ///
3601    /// # Errors
3602    /// Returns an error if the RPC call fails
3603    ///
3604    /// # Examples
3605    /// ```no_run
3606    /// # use amp_rs::ElementsRpc;
3607    /// # #[tokio::main]
3608    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3609    /// let rpc = ElementsRpc::from_env()?;
3610    /// rpc.create_elements_wallet("test_wallet").await?;
3611    /// # Ok(())
3612    /// # }
3613    /// ```
3614    pub async fn create_elements_wallet(&self, wallet_name: &str) -> Result<(), AmpError> {
3615        let params = serde_json::json!([wallet_name]);
3616
3617        let _result: serde_json::Value = self.rpc_call("createwallet", params).await?;
3618
3619        tracing::info!("Successfully created Elements wallet: {}", wallet_name);
3620        Ok(())
3621    }
3622
3623    /// Get a new address from an Elements wallet
3624    ///
3625    /// This method requests a new address from the specified Elements wallet.
3626    /// The address will be generated by Elements and can be used for receiving funds.
3627    /// Defaults to native segwit (bech32) addresses for optimal compatibility.
3628    ///
3629    /// # Arguments
3630    /// * `wallet_name` - Name of the wallet to get address from
3631    /// * `address_type` - Optional address type ("bech32", "legacy", "p2sh-segwit"). Defaults to "bech32"
3632    ///
3633    /// # Errors
3634    /// Returns an error if the RPC call fails or the response format is unexpected
3635    ///
3636    /// # Examples
3637    /// ```no_run
3638    /// # use amp_rs::ElementsRpc;
3639    /// # #[tokio::main]
3640    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3641    /// let rpc = ElementsRpc::from_env()?;
3642    ///
3643    /// // Generate native segwit address (default)
3644    /// let address = rpc.get_new_address("test_wallet", None).await?;
3645    ///
3646    /// // Or explicitly request native segwit
3647    /// let bech32_address = rpc.get_new_address("test_wallet", Some("bech32")).await?;
3648    ///
3649    /// println!("Native segwit address: {}", address);
3650    /// # Ok(())
3651    /// # }
3652    /// ```
3653    pub async fn get_new_address(
3654        &self,
3655        wallet_name: &str,
3656        address_type: Option<&str>,
3657    ) -> Result<String, AmpError> {
3658        // First load the wallet to ensure it's available
3659        self.load_wallet(wallet_name).await?;
3660
3661        // Set default to native segwit (bech32) for Elements
3662        let addr_type = address_type.unwrap_or("bech32");
3663
3664        // For Elements, we need to use the correct parameters for getnewaddress
3665        // getnewaddress [label] [address_type]
3666        let params = serde_json::json!(["", addr_type]);
3667
3668        // Create RPC request for getnewaddress
3669        let request = RpcRequest {
3670            jsonrpc: "1.0".to_string(),
3671            id: "amp-client".to_string(),
3672            method: "getnewaddress".to_string(),
3673            params,
3674        };
3675
3676        // Use the wallet-specific RPC endpoint
3677        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
3678
3679        let response = self
3680            .client
3681            .post(&wallet_url)
3682            .basic_auth(&self.username, Some(&self.password))
3683            .json(&request)
3684            .send()
3685            .await
3686            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
3687
3688        if !response.status().is_success() {
3689            let status = response.status();
3690            let error_body = response
3691                .text()
3692                .await
3693                .unwrap_or_else(|_| "Unable to read error body".to_string());
3694            return Err(AmpError::rpc(format!(
3695                "RPC request failed with status: {status} - Body: {error_body}"
3696            )));
3697        }
3698
3699        let rpc_response: RpcResponse<serde_json::Value> = response
3700            .json()
3701            .await
3702            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
3703
3704        if let Some(error) = rpc_response.error {
3705            return Err(AmpError::rpc(format!(
3706                "RPC error getting new address: {} (code: {})",
3707                error.message, error.code
3708            )));
3709        }
3710
3711        if let Some(result) = rpc_response.result {
3712            if let Some(address) = result.as_str() {
3713                tracing::info!("Generated new {} address: {}", addr_type, address);
3714                return Ok(address.to_string());
3715            }
3716        }
3717
3718        Err(AmpError::rpc(format!(
3719            "Failed to get new address from wallet '{wallet_name}': unexpected response format"
3720        )))
3721    }
3722
3723    /// Get the confidential version of an address from Elements wallet
3724    ///
3725    /// This method takes a regular (unconfidential) address and returns its confidential
3726    /// counterpart, which includes blinding keys for confidential transactions.
3727    ///
3728    /// # Arguments
3729    ///
3730    /// * `wallet_name` - Name of the Elements wallet
3731    /// * `address` - The unconfidential address to get info for
3732    ///
3733    /// # Returns
3734    ///
3735    /// Returns the confidential address string
3736    ///
3737    /// # Example
3738    ///
3739    /// ```no_run
3740    /// # use amp_rs::ElementsRpc;
3741    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
3742    /// let rpc = ElementsRpc::from_env()?;
3743    /// let unconfidential_address = "tex1q...";
3744    /// // Note: This would need to be called in an async context
3745    /// // let confidential_address = rpc.get_confidential_address("test_wallet", unconfidential_address).await?;
3746    /// // println!("Confidential address: {}", confidential_address);
3747    /// # Ok(())
3748    /// # }
3749    /// ```
3750    /// Gets the confidential address for a given unconfidential address from a wallet
3751    ///
3752    /// # Errors
3753    /// Returns an error if the RPC call fails or the response format is unexpected
3754    pub async fn get_confidential_address(
3755        &self,
3756        wallet_name: &str,
3757        address: &str,
3758    ) -> Result<String, AmpError> {
3759        // First load the wallet to ensure it's available
3760        self.load_wallet(wallet_name).await?;
3761
3762        let params = serde_json::json!([address]);
3763
3764        // Create RPC request for getaddressinfo
3765        let request = RpcRequest {
3766            jsonrpc: "1.0".to_string(),
3767            id: "amp-client".to_string(),
3768            method: "getaddressinfo".to_string(),
3769            params,
3770        };
3771
3772        // Use the wallet-specific RPC endpoint
3773        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
3774
3775        let response = self
3776            .client
3777            .post(&wallet_url)
3778            .basic_auth(&self.username, Some(&self.password))
3779            .json(&request)
3780            .send()
3781            .await
3782            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
3783
3784        if !response.status().is_success() {
3785            let status = response.status();
3786            let error_body = response
3787                .text()
3788                .await
3789                .unwrap_or_else(|_| "Unable to read error body".to_string());
3790            return Err(AmpError::rpc(format!(
3791                "RPC request failed with status: {status} - Body: {error_body}"
3792            )));
3793        }
3794
3795        let rpc_response: RpcResponse<serde_json::Value> = response
3796            .json()
3797            .await
3798            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
3799
3800        if let Some(error) = rpc_response.error {
3801            return Err(AmpError::rpc(format!(
3802                "RPC error getting address info: {} (code: {})",
3803                error.message, error.code
3804            )));
3805        }
3806
3807        if let Some(result) = rpc_response.result {
3808            if let Some(confidential_address) = result.get("confidential").and_then(|v| v.as_str())
3809            {
3810                tracing::info!("Retrieved confidential address for: {}", address);
3811                return Ok(confidential_address.to_string());
3812            }
3813        }
3814
3815        Err(AmpError::rpc(format!(
3816            "Failed to get confidential address for '{address}': unexpected response format"
3817        )))
3818    }
3819
3820    /// Get the private key for an address from Elements wallet
3821    ///
3822    /// This method exports the private key for a specific address from the Elements wallet.
3823    /// The private key can then be imported into LWK for signing.
3824    ///
3825    /// Note: This is a simplified implementation that returns a placeholder private key.
3826    /// For production use, implement proper wallet-specific RPC calls.
3827    ///
3828    /// # Arguments
3829    /// * `wallet_name` - Name of the wallet containing the address
3830    /// * `address` - The address to get the private key for
3831    ///
3832    /// # Errors
3833    /// Returns an error if the RPC call fails
3834    ///
3835    /// # Examples
3836    /// ```no_run
3837    /// # use amp_rs::ElementsRpc;
3838    /// # #[tokio::main]
3839    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3840    /// let rpc = ElementsRpc::from_env()?;
3841    /// let address = rpc.get_new_address("test_wallet", None).await?;
3842    /// let private_key = rpc.dump_private_key("test_wallet", &address).await?;
3843    /// println!("Private key: {}", private_key);
3844    /// # Ok(())
3845    /// # }
3846    /// ```
3847    pub async fn dump_private_key(
3848        &self,
3849        wallet_name: &str,
3850        address: &str,
3851    ) -> Result<String, AmpError> {
3852        // First load the wallet to ensure it's available
3853        self.load_wallet(wallet_name).await?;
3854
3855        let params = serde_json::json!([address]);
3856
3857        // Create RPC request for dumpprivkey
3858        let request = RpcRequest {
3859            jsonrpc: "1.0".to_string(),
3860            id: "amp-client".to_string(),
3861            method: "dumpprivkey".to_string(),
3862            params,
3863        };
3864
3865        // Use the wallet-specific RPC endpoint
3866        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
3867
3868        let response = self
3869            .client
3870            .post(&wallet_url)
3871            .basic_auth(&self.username, Some(&self.password))
3872            .json(&request)
3873            .send()
3874            .await
3875            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
3876
3877        if !response.status().is_success() {
3878            return Err(AmpError::rpc(format!(
3879                "RPC request failed with status: {}",
3880                response.status()
3881            )));
3882        }
3883
3884        let rpc_response: RpcResponse<serde_json::Value> = response
3885            .json()
3886            .await
3887            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
3888
3889        if let Some(error) = rpc_response.error {
3890            return Err(AmpError::rpc(format!(
3891                "RPC error dumping private key: {} (code: {})",
3892                error.message, error.code
3893            )));
3894        }
3895
3896        if let Some(result) = rpc_response.result {
3897            if let Some(private_key) = result.as_str() {
3898                tracing::info!("Successfully exported private key for address: {}", address);
3899                return Ok(private_key.to_string());
3900            }
3901        }
3902
3903        Err(AmpError::rpc(format!(
3904            "Failed to dump private key for address '{address}': unexpected response format"
3905        )))
3906    }
3907
3908    /// Creates a descriptor wallet in Elements
3909    ///
3910    /// # Arguments
3911    /// * `wallet_name` - Name for the new wallet
3912    ///
3913    /// # Errors
3914    /// Returns an error if the RPC call fails
3915    ///
3916    /// # Examples
3917    /// ```no_run
3918    /// # use amp_rs::ElementsRpc;
3919    /// # #[tokio::main]
3920    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3921    /// let rpc = ElementsRpc::from_env()?;
3922    /// rpc.create_descriptor_wallet("test_wallet").await?;
3923    /// # Ok(())
3924    /// # }
3925    /// ```
3926    pub async fn create_descriptor_wallet(&self, wallet_name: &str) -> Result<(), AmpError> {
3927        let params = serde_json::json!([wallet_name, true]); // true enables descriptors
3928
3929        let _result: serde_json::Value = self.rpc_call("createwallet", params).await?;
3930
3931        tracing::info!("Successfully created descriptor wallet: {}", wallet_name);
3932        Ok(())
3933    }
3934
3935    /// Imports a single descriptor into an Elements wallet
3936    ///
3937    /// This method imports a descriptor that enables the wallet to scan and recognize
3938    /// addresses/UTXOs from a mnemonic. For LWK descriptors with `<0;1>/*` format,
3939    /// a single descriptor covers both receive and change addresses.
3940    ///
3941    /// # Arguments
3942    /// * `wallet_name` - Name of the wallet to import descriptor into
3943    /// * `descriptor` - The descriptor to import
3944    ///
3945    /// # Errors
3946    /// Returns an error if the RPC call fails
3947    ///
3948    /// # Examples
3949    /// ```no_run
3950    /// # use amp_rs::ElementsRpc;
3951    /// # #[tokio::main]
3952    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3953    /// let rpc = ElementsRpc::from_env()?;
3954    /// let descriptor = "ct(slip77(...),elwpkh([...]/84h/1h/0h]tpub.../<0;1>/*))#checksum";
3955    /// rpc.import_descriptor("test_wallet", descriptor).await?;
3956    /// # Ok(())
3957    /// # }
3958    /// ```
3959    pub async fn import_descriptor(
3960        &self,
3961        wallet_name: &str,
3962        descriptor: &str,
3963    ) -> Result<(), AmpError> {
3964        tracing::info!("Importing descriptor into wallet: {}", wallet_name);
3965        tracing::debug!("Descriptor: {}", descriptor);
3966
3967        let descriptors = serde_json::json!([
3968            {
3969                "desc": descriptor,
3970                "timestamp": "now",
3971                "active": true,
3972                "internal": false  // For LWK descriptors with <0;1>/*, this covers both chains
3973            }
3974        ]);
3975
3976        // Use -rpcwallet parameter to specify the wallet
3977        let request = RpcRequest {
3978            jsonrpc: "1.0".to_string(),
3979            id: "amp-client".to_string(),
3980            method: "importdescriptors".to_string(),
3981            params: descriptors,
3982        };
3983
3984        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
3985
3986        let response = self
3987            .client
3988            .post(&wallet_url)
3989            .basic_auth(&self.username, Some(&self.password))
3990            .json(&request)
3991            .send()
3992            .await
3993            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
3994
3995        if !response.status().is_success() {
3996            return Err(AmpError::rpc(format!(
3997                "RPC request failed with status: {}",
3998                response.status()
3999            )));
4000        }
4001
4002        let rpc_response: RpcResponse<serde_json::Value> = response
4003            .json()
4004            .await
4005            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
4006
4007        if let Some(error) = rpc_response.error {
4008            return Err(AmpError::rpc(format!(
4009                "RPC error {}: {}",
4010                error.code, error.message
4011            )));
4012        }
4013
4014        let result = rpc_response
4015            .result
4016            .ok_or_else(|| AmpError::rpc("RPC response missing result field".to_string()))?;
4017
4018        // Check if descriptor was imported successfully
4019        if let Some(results) = result.as_array() {
4020            if let Some(result) = results.first() {
4021                if let Some(success) = result.get("success").and_then(serde_json::Value::as_bool) {
4022                    if !success {
4023                        let error_msg = result
4024                            .get("error")
4025                            .and_then(|e| e.get("message"))
4026                            .and_then(|m| m.as_str())
4027                            .unwrap_or("Unknown error");
4028                        return Err(AmpError::rpc(format!(
4029                            "Failed to import descriptor: {error_msg}"
4030                        )));
4031                    }
4032                } else {
4033                    return Err(AmpError::rpc(format!(
4034                        "Invalid response format for descriptor import: {result:?}"
4035                    )));
4036                }
4037            }
4038        } else {
4039            return Err(AmpError::rpc(format!(
4040                "Invalid response format: expected array, got {result:?}"
4041            )));
4042        }
4043
4044        tracing::info!(
4045            "Successfully imported descriptor into wallet: {}",
4046            wallet_name
4047        );
4048        Ok(())
4049    }
4050
4051    /// Imports descriptors into an Elements wallet (legacy method for compatibility)
4052    ///
4053    /// This method imports descriptors that enable the wallet to scan and recognize
4054    /// addresses/UTXOs from a mnemonic. If both descriptors are the same (as with LWK
4055    /// descriptors using `<0;1>/*` format), only one descriptor is imported.
4056    ///
4057    /// # Arguments
4058    /// * `wallet_name` - Name of the wallet to import descriptors into
4059    /// * `receive_descriptor` - The receive descriptor
4060    /// * `change_descriptor` - The change descriptor
4061    ///
4062    /// # Errors
4063    /// Returns an error if the RPC call fails
4064    ///
4065    /// # Examples
4066    /// ```no_run
4067    /// # use amp_rs::ElementsRpc;
4068    /// # #[tokio::main]
4069    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4070    /// let rpc = ElementsRpc::from_env()?;
4071    /// let descriptor = "ct(slip77(...),elwpkh([...]/84h/1h/0h]tpub.../<0;1>/*))#checksum";
4072    /// rpc.import_descriptors("test_wallet", descriptor, descriptor).await?;
4073    /// # Ok(())
4074    /// # }
4075    /// ```
4076    #[allow(clippy::cognitive_complexity)]
4077    pub async fn import_descriptors(
4078        &self,
4079        wallet_name: &str,
4080        receive_descriptor: &str,
4081        change_descriptor: &str,
4082    ) -> Result<(), AmpError> {
4083        // If both descriptors are the same (LWK case), import only once
4084        if receive_descriptor == change_descriptor {
4085            return self
4086                .import_descriptor(wallet_name, receive_descriptor)
4087                .await;
4088        }
4089
4090        tracing::info!(
4091            "Importing separate receive and change descriptors into wallet: {}",
4092            wallet_name
4093        );
4094        tracing::debug!("Receive descriptor: {}", receive_descriptor);
4095        tracing::debug!("Change descriptor: {}", change_descriptor);
4096
4097        let descriptors = serde_json::json!([
4098            {
4099                "desc": receive_descriptor,
4100                "timestamp": "now",
4101                "active": true,
4102                "internal": false
4103            },
4104            {
4105                "desc": change_descriptor,
4106                "timestamp": "now",
4107                "active": true,
4108                "internal": true
4109            }
4110        ]);
4111
4112        // Use -rpcwallet parameter to specify the wallet
4113        let request = RpcRequest {
4114            jsonrpc: "1.0".to_string(),
4115            id: "amp-client".to_string(),
4116            method: "importdescriptors".to_string(),
4117            params: descriptors,
4118        };
4119
4120        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
4121
4122        let response = self
4123            .client
4124            .post(&wallet_url)
4125            .basic_auth(&self.username, Some(&self.password))
4126            .json(&request)
4127            .send()
4128            .await
4129            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
4130
4131        if !response.status().is_success() {
4132            return Err(AmpError::rpc(format!(
4133                "RPC request failed with status: {}",
4134                response.status()
4135            )));
4136        }
4137
4138        let rpc_response: RpcResponse<serde_json::Value> = response
4139            .json()
4140            .await
4141            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
4142
4143        if let Some(error) = rpc_response.error {
4144            return Err(AmpError::rpc(format!(
4145                "RPC error {}: {}",
4146                error.code, error.message
4147            )));
4148        }
4149
4150        let result = rpc_response
4151            .result
4152            .ok_or_else(|| AmpError::rpc("RPC response missing result field".to_string()))?;
4153
4154        // Check if both descriptors were imported successfully
4155        if let Some(results) = result.as_array() {
4156            for (i, result) in results.iter().enumerate() {
4157                if let Some(success) = result.get("success").and_then(serde_json::Value::as_bool) {
4158                    if !success {
4159                        let desc_type = if i == 0 { "receive" } else { "change" };
4160                        let error_msg = result
4161                            .get("error")
4162                            .and_then(|e| e.get("message"))
4163                            .and_then(|m| m.as_str())
4164                            .unwrap_or("Unknown error");
4165                        return Err(AmpError::rpc(format!(
4166                            "Failed to import {desc_type} descriptor: {error_msg}"
4167                        )));
4168                    }
4169                } else {
4170                    return Err(AmpError::rpc(format!(
4171                        "Invalid response format for descriptor import: {result:?}"
4172                    )));
4173                }
4174            }
4175        } else {
4176            return Err(AmpError::rpc(format!(
4177                "Invalid response format: expected array, got {result:?}"
4178            )));
4179        }
4180
4181        tracing::info!(
4182            "Successfully imported descriptors into wallet: {}",
4183            wallet_name
4184        );
4185        Ok(())
4186    }
4187
4188    /// Sets up a wallet with descriptors from a mnemonic
4189    ///
4190    /// This is a convenience method that combines wallet creation and descriptor import.
4191    /// It creates a descriptor wallet and imports the receive and change descriptors
4192    /// generated from the provided mnemonic.
4193    ///
4194    /// # Arguments
4195    /// * `wallet_name` - Name for the new wallet
4196    /// * `receive_descriptor` - The receive descriptor (external chain /0/*)
4197    /// * `change_descriptor` - The change descriptor (internal chain /1/*)
4198    ///
4199    /// # Errors
4200    /// Returns an error if wallet creation or descriptor import fails
4201    ///
4202    /// # Examples
4203    /// ```no_run
4204    /// # use amp_rs::ElementsRpc;
4205    /// # #[tokio::main]
4206    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4207    /// let rpc = ElementsRpc::from_env()?;
4208    /// let receive_desc = "wpkh([d34db33f/84h/1h/0h]xprv.../0/*)#checksum";
4209    /// let change_desc = "wpkh([d34db33f/84h/1h/0h]xprv.../1/*)#checksum";
4210    /// rpc.setup_wallet_with_descriptors("test_wallet", receive_desc, change_desc).await?;
4211    /// # Ok(())
4212    /// # }
4213    /// ```
4214    #[allow(clippy::cognitive_complexity)]
4215    pub async fn setup_wallet_with_descriptors(
4216        &self,
4217        wallet_name: &str,
4218        receive_descriptor: &str,
4219        change_descriptor: &str,
4220    ) -> Result<(), AmpError> {
4221        tracing::info!("Setting up wallet with descriptors: {}", wallet_name);
4222
4223        // Try to create the wallet (may fail if it already exists)
4224        match self.create_descriptor_wallet(wallet_name).await {
4225            Ok(()) => {
4226                tracing::info!("Created new descriptor wallet: {}", wallet_name);
4227            }
4228            Err(e) => {
4229                let error_msg = e.to_string();
4230                if error_msg.contains("already exists")
4231                    || error_msg.contains("Database already exists")
4232                {
4233                    tracing::info!(
4234                        "Wallet {} already exists, proceeding with descriptor import",
4235                        wallet_name
4236                    );
4237                } else {
4238                    return Err(e);
4239                }
4240            }
4241        }
4242
4243        // Import the descriptors
4244        self.import_descriptors(wallet_name, receive_descriptor, change_descriptor)
4245            .await?;
4246
4247        tracing::info!(
4248            "Successfully set up wallet with descriptors: {}",
4249            wallet_name
4250        );
4251        Ok(())
4252    }
4253
4254    /// Exports a wallet to a file using dumpwallet RPC
4255    ///
4256    /// # Arguments
4257    /// * `wallet_name` - Name of the wallet to export
4258    /// * `file_path` - Path where the wallet dump file will be created
4259    ///
4260    /// # Errors
4261    /// Returns an error if the RPC call fails or the wallet cannot be exported
4262    ///
4263    /// # Examples
4264    /// ```no_run
4265    /// # use amp_rs::ElementsRpc;
4266    /// # #[tokio::main]
4267    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4268    /// let rpc = ElementsRpc::from_env()?;
4269    /// rpc.dump_wallet("my_wallet", "/tmp/wallet_export.dat").await?;
4270    /// # Ok(())
4271    /// # }
4272    /// ```
4273    pub async fn dump_wallet(&self, wallet_name: &str, file_path: &str) -> Result<(), AmpError> {
4274        // First load the wallet to ensure it's available
4275        self.load_wallet(wallet_name).await?;
4276
4277        let params = serde_json::json!([file_path]);
4278
4279        // Create RPC request for dumpwallet
4280        let request = RpcRequest {
4281            jsonrpc: "1.0".to_string(),
4282            id: "amp-client".to_string(),
4283            method: "dumpwallet".to_string(),
4284            params,
4285        };
4286
4287        // Use the wallet-specific RPC endpoint
4288        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
4289
4290        let response = self
4291            .client
4292            .post(&wallet_url)
4293            .basic_auth(&self.username, Some(&self.password))
4294            .json(&request)
4295            .send()
4296            .await
4297            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
4298
4299        if !response.status().is_success() {
4300            return Err(AmpError::rpc(format!(
4301                "RPC request failed with status: {}",
4302                response.status()
4303            )));
4304        }
4305
4306        let rpc_response: RpcResponse<serde_json::Value> = response
4307            .json()
4308            .await
4309            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
4310
4311        if let Some(error) = rpc_response.error {
4312            return Err(AmpError::rpc(format!(
4313                "RPC error dumping wallet: {} (code: {})",
4314                error.message, error.code
4315            )));
4316        }
4317
4318        tracing::info!(
4319            "Successfully exported wallet {} to {}",
4320            wallet_name,
4321            file_path
4322        );
4323        Ok(())
4324    }
4325
4326    /// Imports a wallet from a file using importwallet RPC
4327    ///
4328    /// # Arguments
4329    /// * `wallet_name` - Name of the wallet to import into
4330    /// * `file_path` - Path to the wallet dump file to import
4331    ///
4332    /// # Errors
4333    /// Returns an error if the RPC call fails or the wallet cannot be imported
4334    ///
4335    /// # Examples
4336    /// ```no_run
4337    /// # use amp_rs::ElementsRpc;
4338    /// # #[tokio::main]
4339    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4340    /// let rpc = ElementsRpc::from_env()?;
4341    /// rpc.import_wallet("my_wallet", "/tmp/wallet_export.dat").await?;
4342    /// # Ok(())
4343    /// # }
4344    /// ```
4345    pub async fn import_wallet(&self, wallet_name: &str, file_path: &str) -> Result<(), AmpError> {
4346        // First load the wallet to ensure it's available
4347        self.load_wallet(wallet_name).await?;
4348
4349        let params = serde_json::json!([file_path]);
4350
4351        // Create RPC request for importwallet
4352        let request = RpcRequest {
4353            jsonrpc: "1.0".to_string(),
4354            id: "amp-client".to_string(),
4355            method: "importwallet".to_string(),
4356            params,
4357        };
4358
4359        // Use the wallet-specific RPC endpoint
4360        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
4361
4362        let response = self
4363            .client
4364            .post(&wallet_url)
4365            .basic_auth(&self.username, Some(&self.password))
4366            .json(&request)
4367            .send()
4368            .await
4369            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
4370
4371        if !response.status().is_success() {
4372            return Err(AmpError::rpc(format!(
4373                "RPC request failed with status: {}",
4374                response.status()
4375            )));
4376        }
4377
4378        let rpc_response: RpcResponse<serde_json::Value> = response
4379            .json()
4380            .await
4381            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
4382
4383        if let Some(error) = rpc_response.error {
4384            return Err(AmpError::rpc(format!(
4385                "RPC error importing wallet: {} (code: {})",
4386                error.message, error.code
4387            )));
4388        }
4389
4390        tracing::info!(
4391            "Successfully imported wallet {} from {}",
4392            wallet_name,
4393            file_path
4394        );
4395        Ok(())
4396    }
4397
4398    /// Exports a blinding key for a confidential address using dumpblindingkey RPC
4399    ///
4400    /// # Arguments
4401    /// * `wallet_name` - Name of the wallet containing the address
4402    /// * `address` - The confidential address to export the blinding key for
4403    ///
4404    /// # Errors
4405    /// Returns an error if the RPC call fails or the address doesn't have a blinding key
4406    ///
4407    /// # Examples
4408    /// ```no_run
4409    /// # use amp_rs::ElementsRpc;
4410    /// # #[tokio::main]
4411    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4412    /// let rpc = ElementsRpc::from_env()?;
4413    /// let key = rpc.dump_blinding_key("my_wallet", "VTpz...").await?;
4414    /// println!("Blinding key: {}", key);
4415    /// # Ok(())
4416    /// # }
4417    /// ```
4418    pub async fn dump_blinding_key(
4419        &self,
4420        wallet_name: &str,
4421        address: &str,
4422    ) -> Result<String, AmpError> {
4423        // First load the wallet to ensure it's available
4424        self.load_wallet(wallet_name).await?;
4425
4426        let params = serde_json::json!([address]);
4427
4428        // Create RPC request for dumpblindingkey
4429        let request = RpcRequest {
4430            jsonrpc: "1.0".to_string(),
4431            id: "amp-client".to_string(),
4432            method: "dumpblindingkey".to_string(),
4433            params,
4434        };
4435
4436        // Use the wallet-specific RPC endpoint
4437        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
4438
4439        let response = self
4440            .client
4441            .post(&wallet_url)
4442            .basic_auth(&self.username, Some(&self.password))
4443            .json(&request)
4444            .send()
4445            .await
4446            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
4447
4448        if !response.status().is_success() {
4449            return Err(AmpError::rpc(format!(
4450                "RPC request failed with status: {}",
4451                response.status()
4452            )));
4453        }
4454
4455        let rpc_response: RpcResponse<serde_json::Value> = response
4456            .json()
4457            .await
4458            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
4459
4460        if let Some(error) = rpc_response.error {
4461            return Err(AmpError::rpc(format!(
4462                "RPC error dumping blinding key: {} (code: {})",
4463                error.message, error.code
4464            )));
4465        }
4466
4467        if let Some(result) = rpc_response.result {
4468            if let Some(blinding_key) = result.as_str() {
4469                tracing::info!(
4470                    "Successfully exported blinding key for address: {}",
4471                    address
4472                );
4473                return Ok(blinding_key.to_string());
4474            }
4475        }
4476
4477        Err(AmpError::rpc(format!(
4478            "Failed to dump blinding key for address '{address}': unexpected response format"
4479        )))
4480    }
4481
4482    /// Imports a blinding key for a confidential address using importblindingkey RPC
4483    ///
4484    /// # Arguments
4485    /// * `wallet_name` - Name of the wallet to import the blinding key into
4486    /// * `address` - The confidential address to import the blinding key for
4487    /// * `blinding_key` - The blinding key to import
4488    ///
4489    /// # Errors
4490    /// Returns an error if the RPC call fails or the blinding key cannot be imported
4491    ///
4492    /// # Examples
4493    /// ```no_run
4494    /// # use amp_rs::ElementsRpc;
4495    /// # #[tokio::main]
4496    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4497    /// let rpc = ElementsRpc::from_env()?;
4498    /// rpc.import_blinding_key("my_wallet", "VTpz...", "blinding_key_hex").await?;
4499    /// # Ok(())
4500    /// # }
4501    /// ```
4502    pub async fn import_blinding_key(
4503        &self,
4504        wallet_name: &str,
4505        address: &str,
4506        blinding_key: &str,
4507    ) -> Result<(), AmpError> {
4508        // First load the wallet to ensure it's available
4509        self.load_wallet(wallet_name).await?;
4510
4511        let params = serde_json::json!([address, blinding_key]);
4512
4513        // Create RPC request for importblindingkey
4514        let request = RpcRequest {
4515            jsonrpc: "1.0".to_string(),
4516            id: "amp-client".to_string(),
4517            method: "importblindingkey".to_string(),
4518            params,
4519        };
4520
4521        // Use the wallet-specific RPC endpoint
4522        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
4523
4524        let response = self
4525            .client
4526            .post(&wallet_url)
4527            .basic_auth(&self.username, Some(&self.password))
4528            .json(&request)
4529            .send()
4530            .await
4531            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
4532
4533        if !response.status().is_success() {
4534            return Err(AmpError::rpc(format!(
4535                "RPC request failed with status: {}",
4536                response.status()
4537            )));
4538        }
4539
4540        let rpc_response: RpcResponse<serde_json::Value> = response
4541            .json()
4542            .await
4543            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
4544
4545        if let Some(error) = rpc_response.error {
4546            return Err(AmpError::rpc(format!(
4547                "RPC error importing blinding key: {} (code: {})",
4548                error.message, error.code
4549            )));
4550        }
4551
4552        tracing::info!(
4553            "Successfully imported blinding key for address: {}",
4554            address
4555        );
4556        Ok(())
4557    }
4558
4559    /// Gets wallet information using getwalletinfo RPC
4560    ///
4561    /// # Arguments
4562    /// * `wallet_name` - Name of the wallet to get information for
4563    ///
4564    /// # Errors
4565    /// Returns an error if the RPC call fails
4566    ///
4567    /// # Examples
4568    /// ```no_run
4569    /// # use amp_rs::ElementsRpc;
4570    /// # #[tokio::main]
4571    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4572    /// let rpc = ElementsRpc::from_env()?;
4573    /// let info = rpc.get_wallet_info("my_wallet").await?;
4574    /// println!("Wallet info: {:?}", info);
4575    /// # Ok(())
4576    /// # }
4577    /// ```
4578    pub async fn get_wallet_info(&self, wallet_name: &str) -> Result<serde_json::Value, AmpError> {
4579        // First load the wallet to ensure it's available
4580        self.load_wallet(wallet_name).await?;
4581
4582        let params = serde_json::json!([]);
4583
4584        // Create RPC request for getwalletinfo
4585        let request = RpcRequest {
4586            jsonrpc: "1.0".to_string(),
4587            id: "amp-client".to_string(),
4588            method: "getwalletinfo".to_string(),
4589            params,
4590        };
4591
4592        // Use the wallet-specific RPC endpoint
4593        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
4594
4595        let response = self
4596            .client
4597            .post(&wallet_url)
4598            .basic_auth(&self.username, Some(&self.password))
4599            .json(&request)
4600            .send()
4601            .await
4602            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
4603
4604        if !response.status().is_success() {
4605            return Err(AmpError::rpc(format!(
4606                "RPC request failed with status: {}",
4607                response.status()
4608            )));
4609        }
4610
4611        let rpc_response: RpcResponse<serde_json::Value> = response
4612            .json()
4613            .await
4614            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
4615
4616        if let Some(error) = rpc_response.error {
4617            return Err(AmpError::rpc(format!(
4618                "RPC error getting wallet info: {} (code: {})",
4619                error.message, error.code
4620            )));
4621        }
4622
4623        if let Some(result) = rpc_response.result {
4624            tracing::info!("Successfully retrieved wallet info for: {}", wallet_name);
4625            return Ok(result);
4626        }
4627
4628        Err(AmpError::rpc(format!(
4629            "Failed to get wallet info for '{wallet_name}': unexpected response format"
4630        )))
4631    }
4632
4633    /// Gets the unconfidential address for a confidential address
4634    ///
4635    /// # Arguments
4636    /// * `wallet_name` - Name of the wallet
4637    /// * `confidential_address` - The confidential address to convert
4638    ///
4639    /// # Errors
4640    /// Returns an error if the RPC call fails
4641    ///
4642    /// # Examples
4643    /// ```no_run
4644    /// # use amp_rs::ElementsRpc;
4645    /// # #[tokio::main]
4646    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4647    /// let rpc = ElementsRpc::from_env()?;
4648    /// let unconf = rpc.get_unconfidential_address("my_wallet", "VTpz...").await?;
4649    /// println!("Unconfidential address: {}", unconf);
4650    /// # Ok(())
4651    /// # }
4652    /// ```
4653    pub async fn get_unconfidential_address(
4654        &self,
4655        wallet_name: &str,
4656        confidential_address: &str,
4657    ) -> Result<String, AmpError> {
4658        // First load the wallet to ensure it's available
4659        self.load_wallet(wallet_name).await?;
4660
4661        let params = serde_json::json!([confidential_address]);
4662
4663        // Create RPC request for getunconfidentialaddress
4664        let request = RpcRequest {
4665            jsonrpc: "1.0".to_string(),
4666            id: "amp-client".to_string(),
4667            method: "getunconfidentialaddress".to_string(),
4668            params,
4669        };
4670
4671        // Use the wallet-specific RPC endpoint
4672        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
4673
4674        let response = self
4675            .client
4676            .post(&wallet_url)
4677            .basic_auth(&self.username, Some(&self.password))
4678            .json(&request)
4679            .send()
4680            .await
4681            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
4682
4683        if !response.status().is_success() {
4684            return Err(AmpError::rpc(format!(
4685                "RPC request failed with status: {}",
4686                response.status()
4687            )));
4688        }
4689
4690        let rpc_response: RpcResponse<serde_json::Value> = response
4691            .json()
4692            .await
4693            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
4694
4695        if let Some(error) = rpc_response.error {
4696            return Err(AmpError::rpc(format!(
4697                "RPC error getting unconfidential address: {} (code: {})",
4698                error.message, error.code
4699            )));
4700        }
4701
4702        if let Some(result) = rpc_response.result {
4703            if let Some(address) = result.as_str() {
4704                tracing::info!(
4705                    "Successfully got unconfidential address for: {}",
4706                    confidential_address
4707                );
4708                return Ok(address.to_string());
4709            }
4710        }
4711
4712        Err(AmpError::rpc(format!(
4713            "Failed to get unconfidential address for '{confidential_address}': unexpected response format"
4714        )))
4715    }
4716
4717    /// Imports a private key into the wallet using importprivkey RPC
4718    ///
4719    /// # Arguments
4720    /// * `wallet_name` - Name of the wallet to import into
4721    /// * `private_key` - The private key in WIF format
4722    /// * `label` - Optional label for the address
4723    /// * `rescan` - Whether to rescan the blockchain for transactions
4724    ///
4725    /// # Errors
4726    /// Returns an error if the RPC call fails
4727    ///
4728    /// # Examples
4729    /// ```no_run
4730    /// # use amp_rs::ElementsRpc;
4731    /// # #[tokio::main]
4732    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4733    /// let rpc = ElementsRpc::from_env()?;
4734    /// rpc.import_private_key("my_wallet", "cT1...", Some("my_address"), Some(false)).await?;
4735    /// # Ok(())
4736    /// # }
4737    /// ```
4738    pub async fn import_private_key(
4739        &self,
4740        wallet_name: &str,
4741        private_key: &str,
4742        label: Option<&str>,
4743        rescan: Option<bool>,
4744    ) -> Result<(), AmpError> {
4745        // First load the wallet to ensure it's available
4746        self.load_wallet(wallet_name).await?;
4747
4748        let params = serde_json::json!([private_key, label.unwrap_or(""), rescan.unwrap_or(false)]);
4749
4750        // Create RPC request for importprivkey
4751        let request = RpcRequest {
4752            jsonrpc: "1.0".to_string(),
4753            id: "amp-client".to_string(),
4754            method: "importprivkey".to_string(),
4755            params,
4756        };
4757
4758        // Use the wallet-specific RPC endpoint
4759        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
4760
4761        let response = self
4762            .client
4763            .post(&wallet_url)
4764            .basic_auth(&self.username, Some(&self.password))
4765            .json(&request)
4766            .send()
4767            .await
4768            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
4769
4770        if !response.status().is_success() {
4771            return Err(AmpError::rpc(format!(
4772                "RPC request failed with status: {}",
4773                response.status()
4774            )));
4775        }
4776
4777        let rpc_response: RpcResponse<serde_json::Value> = response
4778            .json()
4779            .await
4780            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
4781
4782        if let Some(error) = rpc_response.error {
4783            return Err(AmpError::rpc(format!(
4784                "RPC error importing private key: {} (code: {})",
4785                error.message, error.code
4786            )));
4787        }
4788
4789        tracing::info!("Successfully imported private key");
4790        Ok(())
4791    }
4792
4793    /// Lists all descriptors in a wallet using listdescriptors RPC
4794    ///
4795    /// # Arguments
4796    /// * `wallet_name` - Name of the wallet
4797    /// * `private_keys` - Whether to include private keys in the output
4798    ///
4799    /// # Errors
4800    /// Returns an error if the RPC call fails
4801    ///
4802    /// # Examples
4803    /// ```no_run
4804    /// # use amp_rs::ElementsRpc;
4805    /// # #[tokio::main]
4806    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4807    /// let rpc = ElementsRpc::from_env()?;
4808    /// let descriptors = rpc.list_descriptors("my_wallet", Some(true)).await?;
4809    /// for desc in descriptors {
4810    ///     println!("Descriptor: {}", desc);
4811    /// }
4812    /// # Ok(())
4813    /// # }
4814    /// ```
4815    pub async fn list_descriptors(
4816        &self,
4817        wallet_name: &str,
4818        private_keys: Option<bool>,
4819    ) -> Result<Vec<String>, AmpError> {
4820        // First load the wallet to ensure it's available
4821        self.load_wallet(wallet_name).await?;
4822
4823        let params = serde_json::json!([private_keys.unwrap_or(false)]);
4824
4825        // Create RPC request for listdescriptors
4826        let request = RpcRequest {
4827            jsonrpc: "1.0".to_string(),
4828            id: "amp-client".to_string(),
4829            method: "listdescriptors".to_string(),
4830            params,
4831        };
4832
4833        // Use the wallet-specific RPC endpoint
4834        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
4835
4836        let response = self
4837            .client
4838            .post(&wallet_url)
4839            .basic_auth(&self.username, Some(&self.password))
4840            .json(&request)
4841            .send()
4842            .await
4843            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
4844
4845        if !response.status().is_success() {
4846            return Err(AmpError::rpc(format!(
4847                "RPC request failed with status: {}",
4848                response.status()
4849            )));
4850        }
4851
4852        let rpc_response: RpcResponse<serde_json::Value> = response
4853            .json()
4854            .await
4855            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
4856
4857        if let Some(error) = rpc_response.error {
4858            return Err(AmpError::rpc(format!(
4859                "RPC error listing descriptors: {} (code: {})",
4860                error.message, error.code
4861            )));
4862        }
4863
4864        if let Some(result) = rpc_response.result {
4865            // Result has a "descriptors" array with objects containing "desc" field
4866            if let Some(descriptors_array) = result.get("descriptors").and_then(|v| v.as_array()) {
4867                let descriptors: Vec<String> = descriptors_array
4868                    .iter()
4869                    .filter_map(|d| d.get("desc").and_then(|v| v.as_str()).map(String::from))
4870                    .collect();
4871                tracing::info!(
4872                    "Successfully retrieved {} descriptors for wallet: {}",
4873                    descriptors.len(),
4874                    wallet_name
4875                );
4876                return Ok(descriptors);
4877            }
4878        }
4879
4880        Ok(Vec::new())
4881    }
4882
4883    /// Gets all addresses in a wallet by label using getaddressesbylabel RPC
4884    ///
4885    /// # Arguments
4886    /// * `wallet_name` - Name of the wallet
4887    /// * `label` - Label to filter by (empty string for all addresses)
4888    ///
4889    /// # Errors
4890    /// Returns an error if the RPC call fails
4891    ///
4892    /// # Examples
4893    /// ```no_run
4894    /// # use amp_rs::ElementsRpc;
4895    /// # #[tokio::main]
4896    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4897    /// let rpc = ElementsRpc::from_env()?;
4898    /// let addresses = rpc.get_addresses_by_label("my_wallet", "").await?;
4899    /// for addr in addresses {
4900    ///     println!("Address: {}", addr);
4901    /// }
4902    /// # Ok(())
4903    /// # }
4904    /// ```
4905    pub async fn get_addresses_by_label(
4906        &self,
4907        wallet_name: &str,
4908        label: &str,
4909    ) -> Result<Vec<String>, AmpError> {
4910        // First load the wallet to ensure it's available
4911        self.load_wallet(wallet_name).await?;
4912
4913        let params = serde_json::json!([label]);
4914
4915        // Create RPC request for getaddressesbylabel
4916        let request = RpcRequest {
4917            jsonrpc: "1.0".to_string(),
4918            id: "amp-client".to_string(),
4919            method: "getaddressesbylabel".to_string(),
4920            params,
4921        };
4922
4923        // Use the wallet-specific RPC endpoint
4924        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
4925
4926        let response = self
4927            .client
4928            .post(&wallet_url)
4929            .basic_auth(&self.username, Some(&self.password))
4930            .json(&request)
4931            .send()
4932            .await
4933            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
4934
4935        if !response.status().is_success() {
4936            return Err(AmpError::rpc(format!(
4937                "RPC request failed with status: {}",
4938                response.status()
4939            )));
4940        }
4941
4942        let rpc_response: RpcResponse<serde_json::Value> = response
4943            .json()
4944            .await
4945            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
4946
4947        if let Some(error) = rpc_response.error {
4948            return Err(AmpError::rpc(format!(
4949                "RPC error getting addresses by label: {} (code: {})",
4950                error.message, error.code
4951            )));
4952        }
4953
4954        if let Some(result) = rpc_response.result {
4955            // Result is an object with addresses as keys
4956            if let Some(obj) = result.as_object() {
4957                let addresses: Vec<String> = obj.keys().cloned().collect();
4958                tracing::info!(
4959                    "Successfully retrieved {} addresses for wallet: {}",
4960                    addresses.len(),
4961                    wallet_name
4962                );
4963                return Ok(addresses);
4964            }
4965        }
4966
4967        Ok(Vec::new())
4968    }
4969
4970    /// Lists addresses that have received transactions using listreceivedbyaddress RPC
4971    ///
4972    /// # Arguments
4973    /// * `wallet_name` - Name of the wallet to list addresses for
4974    /// * `min_conf` - Minimum number of confirmations (0 for unconfirmed)
4975    /// * `include_empty` - Whether to include addresses that haven't received payments
4976    ///
4977    /// # Errors
4978    /// Returns an error if the RPC call fails
4979    ///
4980    /// # Examples
4981    /// ```no_run
4982    /// # use amp_rs::ElementsRpc;
4983    /// # #[tokio::main]
4984    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4985    /// let rpc = ElementsRpc::from_env()?;
4986    /// let addresses = rpc.list_received_by_address("my_wallet", 0, true).await?;
4987    /// for addr in addresses {
4988    ///     println!("Address: {:?}", addr);
4989    /// }
4990    /// # Ok(())
4991    /// # }
4992    /// ```
4993    pub async fn list_received_by_address(
4994        &self,
4995        wallet_name: &str,
4996        min_conf: u32,
4997        include_empty: bool,
4998    ) -> Result<Vec<ReceivedByAddress>, AmpError> {
4999        // First load the wallet to ensure it's available
5000        self.load_wallet(wallet_name).await?;
5001
5002        let params = serde_json::json!([min_conf, include_empty]);
5003
5004        // Create RPC request for listreceivedbyaddress
5005        let request = RpcRequest {
5006            jsonrpc: "1.0".to_string(),
5007            id: "amp-client".to_string(),
5008            method: "listreceivedbyaddress".to_string(),
5009            params,
5010        };
5011
5012        // Use the wallet-specific RPC endpoint
5013        let wallet_url = format!("{}/wallet/{}", self.base_url, wallet_name);
5014
5015        let response = self
5016            .client
5017            .post(&wallet_url)
5018            .basic_auth(&self.username, Some(&self.password))
5019            .json(&request)
5020            .send()
5021            .await
5022            .map_err(|e| AmpError::rpc(format!("Failed to send RPC request: {e}")))?;
5023
5024        if !response.status().is_success() {
5025            return Err(AmpError::rpc(format!(
5026                "RPC request failed with status: {}",
5027                response.status()
5028            )));
5029        }
5030
5031        let rpc_response: RpcResponse<Vec<ReceivedByAddress>> = response
5032            .json()
5033            .await
5034            .map_err(|e| AmpError::rpc(format!("Failed to parse RPC response: {e}")))?;
5035
5036        if let Some(error) = rpc_response.error {
5037            return Err(AmpError::rpc(format!(
5038                "RPC error listing received by address: {} (code: {})",
5039                error.message, error.code
5040            )));
5041        }
5042
5043        if let Some(result) = rpc_response.result {
5044            tracing::info!(
5045                "Successfully listed {} addresses for wallet: {}",
5046                result.len(),
5047                wallet_name
5048            );
5049            return Ok(result);
5050        }
5051
5052        Ok(Vec::new())
5053    }
5054}
5055
5056#[cfg(test)]
5057mod elements_rpc_tests {
5058    use super::*;
5059    use httpmock::prelude::*;
5060    use serial_test::serial;
5061    use std::collections::HashMap;
5062
5063    #[test]
5064    fn test_elements_rpc_new() {
5065        let rpc = ElementsRpc::new(
5066            "http://localhost:18884".to_string(),
5067            "user".to_string(),
5068            "pass".to_string(),
5069        );
5070
5071        assert_eq!(rpc.base_url, "http://localhost:18884");
5072        assert_eq!(rpc.username, "user");
5073        assert_eq!(rpc.password, "pass");
5074    }
5075
5076    #[test]
5077    #[serial]
5078    fn test_elements_rpc_from_env_missing_vars() {
5079        // Store original values to restore later
5080        let original_url = env::var("ELEMENTS_RPC_URL").ok();
5081        let original_user = env::var("ELEMENTS_RPC_USER").ok();
5082        let original_password = env::var("ELEMENTS_RPC_PASSWORD").ok();
5083
5084        // Clear environment variables to test error handling
5085        env::remove_var("ELEMENTS_RPC_URL");
5086        env::remove_var("ELEMENTS_RPC_USER");
5087        env::remove_var("ELEMENTS_RPC_PASSWORD");
5088
5089        let result = ElementsRpc::from_env();
5090        assert!(
5091            result.is_err(),
5092            "ElementsRpc::from_env() should fail when env vars are missing"
5093        );
5094
5095        match result.unwrap_err() {
5096            AmpError::Validation(msg) => {
5097                assert!(
5098                    msg.contains("ELEMENTS_RPC_URL"),
5099                    "Error message should mention missing ELEMENTS_RPC_URL"
5100                );
5101            }
5102            _ => panic!("Expected validation error"),
5103        }
5104
5105        // Restore original values or keep removed if they weren't set
5106        if let Some(val) = original_url {
5107            env::set_var("ELEMENTS_RPC_URL", val);
5108        }
5109        if let Some(val) = original_user {
5110            env::set_var("ELEMENTS_RPC_USER", val);
5111        }
5112        if let Some(val) = original_password {
5113            env::set_var("ELEMENTS_RPC_PASSWORD", val);
5114        }
5115    }
5116
5117    #[test]
5118    #[serial]
5119    fn test_elements_rpc_from_env_success() {
5120        // Store original values to restore later
5121        let original_url = env::var("ELEMENTS_RPC_URL").ok();
5122        let original_user = env::var("ELEMENTS_RPC_USER").ok();
5123        let original_password = env::var("ELEMENTS_RPC_PASSWORD").ok();
5124
5125        // Set test values
5126        env::set_var("ELEMENTS_RPC_URL", "http://localhost:18884");
5127        env::set_var("ELEMENTS_RPC_USER", "testuser");
5128        env::set_var("ELEMENTS_RPC_PASSWORD", "testpass");
5129
5130        let result = ElementsRpc::from_env();
5131        assert!(
5132            result.is_ok(),
5133            "ElementsRpc::from_env() should succeed when all env vars are set"
5134        );
5135
5136        let rpc = result.unwrap();
5137        assert_eq!(rpc.base_url, "http://localhost:18884");
5138        assert_eq!(rpc.username, "testuser");
5139        assert_eq!(rpc.password, "testpass");
5140
5141        // Restore original values or remove if they weren't set
5142        match original_url {
5143            Some(val) => env::set_var("ELEMENTS_RPC_URL", val),
5144            None => env::remove_var("ELEMENTS_RPC_URL"),
5145        }
5146        match original_user {
5147            Some(val) => env::set_var("ELEMENTS_RPC_USER", val),
5148            None => env::remove_var("ELEMENTS_RPC_USER"),
5149        }
5150        match original_password {
5151            Some(val) => env::set_var("ELEMENTS_RPC_PASSWORD", val),
5152            None => env::remove_var("ELEMENTS_RPC_PASSWORD"),
5153        }
5154    }
5155
5156    #[test]
5157    fn test_elements_rpc_method_signatures() {
5158        // Test that all new methods have correct signatures and can be called
5159        let rpc = ElementsRpc::new(
5160            "http://localhost:18884".to_string(),
5161            "user".to_string(),
5162            "pass".to_string(),
5163        );
5164
5165        // Test that methods exist and have correct signatures (compilation test)
5166        let _: std::pin::Pin<
5167            Box<dyn std::future::Future<Output = Result<Vec<Unspent>, AmpError>> + Send + '_>,
5168        > = Box::pin(rpc.list_unspent(Some("test_asset")));
5169
5170        let inputs = vec![TxInput {
5171            txid: "test_txid".to_string(),
5172            vout: 0,
5173            sequence: None,
5174        }];
5175        let outputs = std::collections::HashMap::new();
5176        let assets = std::collections::HashMap::new();
5177
5178        let _: std::pin::Pin<
5179            Box<dyn std::future::Future<Output = Result<String, AmpError>> + Send + '_>,
5180        > = Box::pin(rpc.create_raw_transaction(inputs, outputs, assets));
5181
5182        let _: std::pin::Pin<
5183            Box<dyn std::future::Future<Output = Result<String, AmpError>> + Send + '_>,
5184        > = Box::pin(rpc.send_raw_transaction("test_hex"));
5185
5186        let _: std::pin::Pin<
5187            Box<dyn std::future::Future<Output = Result<TransactionDetail, AmpError>> + Send + '_>,
5188        > = Box::pin(rpc.get_transaction("test_txid"));
5189    }
5190
5191    // Mock RPC response tests for UTXO and transaction operations
5192
5193    #[tokio::test]
5194    async fn test_get_network_info_success() {
5195        let server = MockServer::start();
5196
5197        let mock_response = serde_json::json!({
5198            "jsonrpc": "1.0",
5199            "id": "amp-client",
5200            "result": {
5201                "version": 220000,
5202                "subversion": "/Liquid:22.0.0/",
5203                "protocolversion": 70016,
5204                "localservices": "0000000000000409",
5205                "localrelay": true,
5206                "timeoffset": 0,
5207                "networkactive": true,
5208                "connections": 8,
5209                "networks": [],
5210                "relayfee": 0.00001000,
5211                "incrementalfee": 0.00001000,
5212                "localaddresses": [],
5213                "warnings": ""
5214            }
5215        });
5216
5217        let mock = server.mock(|when, then| {
5218            when.method(POST)
5219                .path("/")
5220                .header("authorization", "Basic dXNlcjpwYXNz") // base64 of "user:pass"
5221                .json_body(serde_json::json!({
5222                    "jsonrpc": "1.0",
5223                    "id": "amp-client",
5224                    "method": "getnetworkinfo",
5225                    "params": []
5226                }));
5227            then.status(200)
5228                .header("content-type", "application/json")
5229                .json_body(mock_response);
5230        });
5231
5232        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5233        let result = rpc.get_network_info().await;
5234
5235        assert!(result.is_ok());
5236        let network_info = result.unwrap();
5237        assert_eq!(network_info.version, 220000);
5238        assert_eq!(network_info.subversion, "/Liquid:22.0.0/");
5239        assert_eq!(network_info.connections, 8);
5240
5241        mock.assert();
5242    }
5243
5244    #[tokio::test]
5245    async fn test_get_blockchain_info_success() {
5246        let server = MockServer::start();
5247
5248        let mock_response = serde_json::json!({
5249            "jsonrpc": "1.0",
5250            "id": "amp-client",
5251            "result": {
5252                "chain": "liquidregtest",
5253                "blocks": 12345,
5254                "headers": 12345,
5255                "bestblockhash": "abc123def456789",
5256                "difficulty": 4.656542373906925e-10,
5257                "mediantime": 1640995200,
5258                "verificationprogress": 1.0,
5259                "initialblockdownload": false,
5260                "chainwork": "0000000000000000000000000000000000000000000000000000000000003039",
5261                "size_on_disk": 1234567,
5262                "pruned": false,
5263                "softforks": {},
5264                "warnings": ""
5265            }
5266        });
5267
5268        let mock = server.mock(|when, then| {
5269            when.method(POST)
5270                .path("/")
5271                .header("authorization", "Basic dXNlcjpwYXNz")
5272                .json_body(serde_json::json!({
5273                    "jsonrpc": "1.0",
5274                    "id": "amp-client",
5275                    "method": "getblockchaininfo",
5276                    "params": []
5277                }));
5278            then.status(200)
5279                .header("content-type", "application/json")
5280                .json_body(mock_response);
5281        });
5282
5283        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5284        let result = rpc.get_blockchain_info().await;
5285
5286        assert!(result.is_ok());
5287        let blockchain_info = result.unwrap();
5288        assert_eq!(blockchain_info.chain, "liquidregtest");
5289        assert_eq!(blockchain_info.blocks, 12345);
5290        assert_eq!(blockchain_info.bestblockhash, "abc123def456789");
5291
5292        mock.assert();
5293    }
5294
5295    #[tokio::test]
5296    async fn test_list_unspent_with_asset_filter() {
5297        let server = MockServer::start();
5298
5299        let mock_response = serde_json::json!({
5300            "jsonrpc": "1.0",
5301            "id": "amp-client",
5302            "result": [
5303                {
5304                    "txid": "abc123def456789",
5305                    "vout": 0,
5306                    "amount": 100.0,
5307                    "asset": "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d",
5308                    "address": "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq",
5309                    "spendable": true,
5310                    "confirmations": 6,
5311                    "scriptpubkey": "76a914abc123def456789abc123def456789abc123de88ac"
5312                },
5313                {
5314                    "txid": "def456abc123789",
5315                    "vout": 1,
5316                    "amount": 50.0,
5317                    "asset": "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d",
5318                    "address": "lq1qq3xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq",
5319                    "spendable": true,
5320                    "confirmations": 3
5321                }
5322            ]
5323        });
5324
5325        let asset_id = "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d";
5326
5327        let mock = server.mock(|when, then| {
5328            when.method(POST)
5329                .path("/")
5330                .header("authorization", "Basic dXNlcjpwYXNz")
5331                .json_body(serde_json::json!({
5332                    "jsonrpc": "1.0",
5333                    "id": "amp-client",
5334                    "method": "listunspent",
5335                    "params": [1, 9999999, [], true, {"asset": asset_id}]
5336                }));
5337            then.status(200)
5338                .header("content-type", "application/json")
5339                .json_body(mock_response);
5340        });
5341
5342        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5343        let result = rpc.list_unspent(Some(asset_id)).await;
5344
5345        assert!(result.is_ok());
5346        let utxos = result.unwrap();
5347        assert_eq!(utxos.len(), 2);
5348        assert_eq!(utxos[0].txid, "abc123def456789");
5349        assert_eq!(utxos[0].amount, 100.0);
5350        assert_eq!(utxos[0].asset, asset_id);
5351        assert_eq!(utxos[1].txid, "def456abc123789");
5352        assert_eq!(utxos[1].amount, 50.0);
5353
5354        mock.assert();
5355    }
5356
5357    #[tokio::test]
5358    async fn test_list_unspent_without_filter() {
5359        let server = MockServer::start();
5360
5361        let mock_response = serde_json::json!({
5362            "jsonrpc": "1.0",
5363            "id": "amp-client",
5364            "result": [
5365                {
5366                    "txid": "ghi789jkl012345",
5367                    "vout": 0,
5368                    "amount": 25.0,
5369                    "asset": "different_asset_id",
5370                    "address": "lq1qq4xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq",
5371                    "spendable": true,
5372                    "confirmations": 10
5373                }
5374            ]
5375        });
5376
5377        let mock = server.mock(|when, then| {
5378            when.method(POST)
5379                .path("/")
5380                .header("authorization", "Basic dXNlcjpwYXNz")
5381                .json_body(serde_json::json!({
5382                    "jsonrpc": "1.0",
5383                    "id": "amp-client",
5384                    "method": "listunspent",
5385                    "params": [1, 9999999, [], true]
5386                }));
5387            then.status(200)
5388                .header("content-type", "application/json")
5389                .json_body(mock_response);
5390        });
5391
5392        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5393        let result = rpc.list_unspent(None).await;
5394
5395        assert!(result.is_ok());
5396        let utxos = result.unwrap();
5397        assert_eq!(utxos.len(), 1);
5398        assert_eq!(utxos[0].txid, "ghi789jkl012345");
5399        assert_eq!(utxos[0].amount, 25.0);
5400
5401        mock.assert();
5402    }
5403
5404    #[tokio::test]
5405    async fn test_create_raw_transaction_success() {
5406        let server = MockServer::start();
5407
5408        let mock_response = serde_json::json!({
5409            "jsonrpc": "1.0",
5410            "id": "amp-client",
5411            "result": "0200000000010abc123def456789abc123def456789abc123def456789abc123def456789abc123def456789000000006b483045022100..."
5412        });
5413
5414        let mock = server.mock(|when, then| {
5415            when.method(POST)
5416                .path("/")
5417                .header("authorization", "Basic dXNlcjpwYXNz")
5418                .json_body(serde_json::json!({
5419                    "jsonrpc": "1.0",
5420                    "id": "amp-client",
5421                    "method": "createrawtransaction",
5422                    "params": [
5423                        [
5424                            {
5425                                "txid": "input_txid_123",
5426                                "vout": 0,
5427                                "sequence": 4294967295u32
5428                            }
5429                        ],
5430                        {
5431                            "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq": 100.0
5432                        },
5433                        0,
5434                        false,
5435                        {
5436                            "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq": "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d"
5437                        }
5438                    ]
5439                }));
5440            then.status(200)
5441                .header("content-type", "application/json")
5442                .json_body(mock_response);
5443        });
5444
5445        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5446
5447        let inputs = vec![TxInput {
5448            txid: "input_txid_123".to_string(),
5449            vout: 0,
5450            sequence: Some(0xffffffff),
5451        }];
5452
5453        let mut outputs = HashMap::new();
5454        outputs.insert(
5455            "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
5456            100.0,
5457        );
5458
5459        let mut assets = HashMap::new();
5460        assets.insert(
5461            "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
5462            "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d".to_string(),
5463        );
5464
5465        let result = rpc.create_raw_transaction(inputs, outputs, assets).await;
5466
5467        assert!(result.is_ok());
5468        let raw_tx = result.unwrap();
5469        assert!(raw_tx.starts_with("0200000000010abc123def456789"));
5470
5471        mock.assert();
5472    }
5473
5474    #[tokio::test]
5475    async fn test_send_raw_transaction_success() {
5476        let server = MockServer::start();
5477
5478        let mock_response = serde_json::json!({
5479            "jsonrpc": "1.0",
5480            "id": "amp-client",
5481            "result": "abc123def456789abc123def456789abc123def456789abc123def456789abc123de"
5482        });
5483
5484        let signed_tx_hex = "0200000000010abc123def456789abc123def456789abc123def456789abc123def456789abc123def456789000000006b483045022100...";
5485
5486        let mock = server.mock(|when, then| {
5487            when.method(POST)
5488                .path("/")
5489                .header("authorization", "Basic dXNlcjpwYXNz")
5490                .json_body(serde_json::json!({
5491                    "jsonrpc": "1.0",
5492                    "id": "amp-client",
5493                    "method": "sendrawtransaction",
5494                    "params": [signed_tx_hex]
5495                }));
5496            then.status(200)
5497                .header("content-type", "application/json")
5498                .json_body(mock_response);
5499        });
5500
5501        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5502        let result = rpc.send_raw_transaction(signed_tx_hex).await;
5503
5504        assert!(result.is_ok());
5505        let txid = result.unwrap();
5506        assert_eq!(
5507            txid,
5508            "abc123def456789abc123def456789abc123def456789abc123def456789abc123de"
5509        );
5510
5511        mock.assert();
5512    }
5513
5514    #[tokio::test]
5515    async fn test_get_transaction_success() {
5516        let server = MockServer::start();
5517
5518        let mock_response = serde_json::json!({
5519            "jsonrpc": "1.0",
5520            "id": "amp-client",
5521            "result": {
5522                "txid": "abc123def456789abc123def456789abc123def456789abc123def456789abc123de",
5523                "confirmations": 6,
5524                "blockheight": 12345,
5525                "hex": "0200000000010abc123def456789...",
5526                "blockhash": "def456abc123789def456abc123789def456abc123789def456abc123789def456ab",
5527                "blocktime": 1640995200,
5528                "time": 1640995200,
5529                "timereceived": 1640995180
5530            }
5531        });
5532
5533        let txid = "abc123def456789abc123def456789abc123def456789abc123def456789abc123de";
5534
5535        let mock = server.mock(|when, then| {
5536            when.method(POST)
5537                .path("/")
5538                .header("authorization", "Basic dXNlcjpwYXNz")
5539                .json_body(serde_json::json!({
5540                    "jsonrpc": "1.0",
5541                    "id": "amp-client",
5542                    "method": "gettransaction",
5543                    "params": [txid, true]
5544                }));
5545            then.status(200)
5546                .header("content-type", "application/json")
5547                .json_body(mock_response);
5548        });
5549
5550        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5551        let result = rpc.get_transaction(txid).await;
5552
5553        assert!(result.is_ok());
5554        let tx_detail = result.unwrap();
5555        assert_eq!(tx_detail.txid, txid);
5556        assert_eq!(tx_detail.confirmations, 6);
5557        assert_eq!(tx_detail.blockheight, Some(12345));
5558        assert_eq!(tx_detail.blocktime, Some(1640995200));
5559
5560        mock.assert();
5561    }
5562
5563    // Error handling tests
5564
5565    #[tokio::test]
5566    async fn test_rpc_call_network_failure() {
5567        // Use an invalid URL to simulate network failure
5568        let rpc = ElementsRpc::new(
5569            "http://invalid-host:99999".to_string(),
5570            "user".to_string(),
5571            "pass".to_string(),
5572        );
5573
5574        let result = rpc.get_network_info().await;
5575        assert!(result.is_err());
5576
5577        match result.unwrap_err() {
5578            AmpError::Rpc(msg) => {
5579                assert!(msg.contains("Failed to send RPC request"));
5580            }
5581            _ => panic!("Expected RPC error for network failure"),
5582        }
5583    }
5584
5585    #[tokio::test]
5586    async fn test_rpc_call_http_error_status() {
5587        let server = MockServer::start();
5588
5589        let mock = server.mock(|when, then| {
5590            when.method(POST).path("/");
5591            then.status(500)
5592                .header("content-type", "application/json")
5593                .body("Internal Server Error");
5594        });
5595
5596        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5597        let result = rpc.get_network_info().await;
5598
5599        assert!(result.is_err());
5600        match result.unwrap_err() {
5601            AmpError::Rpc(msg) => {
5602                assert!(msg.contains("RPC request failed with status: 500"));
5603            }
5604            _ => panic!("Expected RPC error for HTTP error status"),
5605        }
5606
5607        mock.assert();
5608    }
5609
5610    #[tokio::test]
5611    async fn test_rpc_call_invalid_json_response() {
5612        let server = MockServer::start();
5613
5614        let mock = server.mock(|when, then| {
5615            when.method(POST).path("/");
5616            then.status(200)
5617                .header("content-type", "application/json")
5618                .body("invalid json response");
5619        });
5620
5621        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5622        let result = rpc.get_network_info().await;
5623
5624        assert!(result.is_err());
5625        match result.unwrap_err() {
5626            AmpError::Rpc(msg) => {
5627                assert!(msg.contains("Failed to parse RPC response"));
5628            }
5629            _ => panic!("Expected RPC error for invalid JSON"),
5630        }
5631
5632        mock.assert();
5633    }
5634
5635    #[tokio::test]
5636    async fn test_rpc_call_error_response() {
5637        let server = MockServer::start();
5638
5639        let mock_response = serde_json::json!({
5640            "jsonrpc": "1.0",
5641            "id": "amp-client",
5642            "result": null,
5643            "error": {
5644                "code": -32601,
5645                "message": "Method not found"
5646            }
5647        });
5648
5649        let mock = server.mock(|when, then| {
5650            when.method(POST).path("/");
5651            then.status(200)
5652                .header("content-type", "application/json")
5653                .json_body(mock_response);
5654        });
5655
5656        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5657        let result = rpc.get_network_info().await;
5658
5659        assert!(result.is_err());
5660        match result.unwrap_err() {
5661            AmpError::Rpc(msg) => {
5662                assert!(msg.contains("RPC error -32601: Method not found"));
5663            }
5664            _ => panic!("Expected RPC error for error response"),
5665        }
5666
5667        mock.assert();
5668    }
5669
5670    #[tokio::test]
5671    async fn test_rpc_call_missing_result() {
5672        let server = MockServer::start();
5673
5674        let mock_response = serde_json::json!({
5675            "jsonrpc": "1.0",
5676            "id": "amp-client",
5677            "result": null,
5678            "error": null
5679        });
5680
5681        let mock = server.mock(|when, then| {
5682            when.method(POST).path("/");
5683            then.status(200)
5684                .header("content-type", "application/json")
5685                .json_body(mock_response);
5686        });
5687
5688        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5689        let result = rpc.get_network_info().await;
5690
5691        assert!(result.is_err());
5692        match result.unwrap_err() {
5693            AmpError::Rpc(msg) => {
5694                assert!(msg.contains("RPC response missing result field"));
5695            }
5696            _ => panic!("Expected RPC error for missing result"),
5697        }
5698
5699        mock.assert();
5700    }
5701
5702    // Authentication tests
5703
5704    #[tokio::test]
5705    async fn test_rpc_authentication_headers() {
5706        let server = MockServer::start();
5707
5708        let mock_response = serde_json::json!({
5709            "jsonrpc": "1.0",
5710            "id": "amp-client",
5711            "result": {
5712                "version": 220000,
5713                "subversion": "/Liquid:22.0.0/",
5714                "protocolversion": 70016,
5715                "localservices": "0000000000000409",
5716                "localrelay": true,
5717                "timeoffset": 0,
5718                "networkactive": true,
5719                "connections": 8,
5720                "networks": [],
5721                "relayfee": 0.00001000,
5722                "incrementalfee": 0.00001000,
5723                "localaddresses": [],
5724                "warnings": ""
5725            }
5726        });
5727
5728        // Test with custom username and password
5729        let mock = server.mock(|when, then| {
5730            when.method(POST)
5731                .path("/")
5732                .header("authorization", "Basic dGVzdHVzZXI6dGVzdHBhc3M=") // base64 of "testuser:testpass"
5733                .json_body(serde_json::json!({
5734                    "jsonrpc": "1.0",
5735                    "id": "amp-client",
5736                    "method": "getnetworkinfo",
5737                    "params": []
5738                }));
5739            then.status(200)
5740                .header("content-type", "application/json")
5741                .json_body(mock_response);
5742        });
5743
5744        let rpc = ElementsRpc::new(
5745            server.url("/"),
5746            "testuser".to_string(),
5747            "testpass".to_string(),
5748        );
5749        let result = rpc.get_network_info().await;
5750
5751        assert!(result.is_ok());
5752        mock.assert();
5753    }
5754
5755    // Wallet passphrase tests
5756
5757    #[tokio::test]
5758    async fn test_wallet_passphrase_success() {
5759        let server = MockServer::start();
5760
5761        let mock_response = serde_json::json!({
5762            "jsonrpc": "1.0",
5763            "id": "amp-client",
5764            "result": null
5765        });
5766
5767        let mock = server.mock(|when, then| {
5768            when.method(POST)
5769                .path("/")
5770                .header("authorization", "Basic dXNlcjpwYXNz")
5771                .json_body(serde_json::json!({
5772                    "jsonrpc": "1.0",
5773                    "id": "amp-client",
5774                    "method": "walletpassphrase",
5775                    "params": ["my_passphrase", 300]
5776                }));
5777            then.status(200)
5778                .header("content-type", "application/json")
5779                .json_body(mock_response);
5780        });
5781
5782        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5783        let result = rpc.wallet_passphrase("my_passphrase", 300).await;
5784
5785        assert!(result.is_ok());
5786        mock.assert();
5787    }
5788
5789    // Connection validation tests
5790
5791    #[tokio::test]
5792    async fn test_validate_connection_success() {
5793        let server = MockServer::start();
5794
5795        let mock_response = serde_json::json!({
5796            "jsonrpc": "1.0",
5797            "id": "amp-client",
5798            "result": {
5799                "version": 220000,
5800                "subversion": "/Liquid:22.0.0/",
5801                "protocolversion": 70016,
5802                "localservices": "0000000000000409",
5803                "localrelay": true,
5804                "timeoffset": 0,
5805                "networkactive": true,
5806                "connections": 8,
5807                "networks": [],
5808                "relayfee": 0.00001000,
5809                "incrementalfee": 0.00001000,
5810                "localaddresses": [],
5811                "warnings": ""
5812            }
5813        });
5814
5815        let mock = server.mock(|when, then| {
5816            when.method(POST).path("/");
5817            then.status(200)
5818                .header("content-type", "application/json")
5819                .json_body(mock_response);
5820        });
5821
5822        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5823        let result = rpc.validate_connection().await;
5824
5825        assert!(result.is_ok());
5826        mock.assert();
5827    }
5828
5829    #[tokio::test]
5830    async fn test_get_node_status_success() {
5831        let server = MockServer::start();
5832
5833        let network_mock_response = serde_json::json!({
5834            "jsonrpc": "1.0",
5835            "id": "amp-client",
5836            "result": {
5837                "version": 220000,
5838                "subversion": "/Liquid:22.0.0/",
5839                "protocolversion": 70016,
5840                "localservices": "0000000000000409",
5841                "localrelay": true,
5842                "timeoffset": 0,
5843                "networkactive": true,
5844                "connections": 8,
5845                "networks": [],
5846                "relayfee": 0.00001000,
5847                "incrementalfee": 0.00001000,
5848                "localaddresses": [],
5849                "warnings": ""
5850            }
5851        });
5852
5853        let blockchain_mock_response = serde_json::json!({
5854            "jsonrpc": "1.0",
5855            "id": "amp-client",
5856            "result": {
5857                "chain": "liquidregtest",
5858                "blocks": 12345,
5859                "headers": 12345,
5860                "bestblockhash": "abc123def456789",
5861                "difficulty": 4.656542373906925e-10,
5862                "mediantime": 1640995200,
5863                "verificationprogress": 1.0,
5864                "initialblockdownload": false,
5865                "chainwork": "0000000000000000000000000000000000000000000000000000000000003039",
5866                "size_on_disk": 1234567,
5867                "pruned": false,
5868                "softforks": {},
5869                "warnings": ""
5870            }
5871        });
5872
5873        let network_mock = server.mock(|when, then| {
5874            when.method(POST).path("/").json_body(serde_json::json!({
5875                "jsonrpc": "1.0",
5876                "id": "amp-client",
5877                "method": "getnetworkinfo",
5878                "params": []
5879            }));
5880            then.status(200)
5881                .header("content-type", "application/json")
5882                .json_body(network_mock_response);
5883        });
5884
5885        let blockchain_mock = server.mock(|when, then| {
5886            when.method(POST).path("/").json_body(serde_json::json!({
5887                "jsonrpc": "1.0",
5888                "id": "amp-client",
5889                "method": "getblockchaininfo",
5890                "params": []
5891            }));
5892            then.status(200)
5893                .header("content-type", "application/json")
5894                .json_body(blockchain_mock_response);
5895        });
5896
5897        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
5898        let result = rpc.get_node_status().await;
5899
5900        assert!(result.is_ok());
5901        let (network_info, blockchain_info) = result.unwrap();
5902        assert_eq!(network_info.version, 220000);
5903        assert_eq!(blockchain_info.blocks, 12345);
5904
5905        network_mock.assert();
5906        blockchain_mock.assert();
5907    }
5908
5909    // Tests for UTXO selection and transaction building logic
5910
5911    #[tokio::test]
5912    async fn test_build_distribution_transaction_zero_amount() {
5913        let rpc = ElementsRpc::new(
5914            "http://localhost:18884".to_string(),
5915            "user".to_string(),
5916            "pass".to_string(),
5917        );
5918
5919        let address_amounts = HashMap::new(); // Empty distribution
5920
5921        let result = rpc
5922            .build_distribution_transaction(
5923                "test_wallet",
5924                "asset_id",
5925                address_amounts,
5926                "change_address",
5927                1.0,
5928            )
5929            .await;
5930
5931        assert!(result.is_err());
5932        match result.unwrap_err() {
5933            AmpError::Validation(msg) => {
5934                assert!(msg.contains("Total distribution amount must be greater than zero"));
5935            }
5936            _ => panic!("Expected validation error for zero distribution amount"),
5937        }
5938    }
5939
5940    #[tokio::test]
5941    async fn test_sign_transaction_validation() {
5942        let rpc = ElementsRpc::new(
5943            "http://localhost:18884".to_string(),
5944            "user".to_string(),
5945            "pass".to_string(),
5946        );
5947
5948        // Mock signer for testing
5949        struct MockSigner {
5950            should_fail: bool,
5951            return_value: String,
5952        }
5953
5954        #[async_trait::async_trait]
5955        impl crate::signer::Signer for MockSigner {
5956            async fn sign_transaction(
5957                &self,
5958                _unsigned_tx: &str,
5959            ) -> Result<String, crate::signer::SignerError> {
5960                if self.should_fail {
5961                    Err(crate::signer::SignerError::Lwk(
5962                        "Mock signing failure".to_string(),
5963                    ))
5964                } else {
5965                    // Return a longer hex string to simulate signed transaction (20+ bytes when decoded)
5966                    Ok(format!(
5967                        "{}deadbeefcafebabe1234567890abcdef",
5968                        self.return_value
5969                    ))
5970                }
5971            }
5972
5973            fn as_any(&self) -> &dyn std::any::Any {
5974                self
5975            }
5976        }
5977
5978        // Test empty transaction hex
5979        let mock_signer = MockSigner {
5980            should_fail: false,
5981            return_value: "".to_string(),
5982        };
5983        let result = rpc.sign_transaction("", &mock_signer).await;
5984        assert!(result.is_err());
5985        assert!(result.unwrap_err().to_string().contains("cannot be empty"));
5986
5987        // Test odd length hex
5988        let result = rpc.sign_transaction("abc", &mock_signer).await;
5989        assert!(result.is_err());
5990        assert!(result.unwrap_err().to_string().contains("even length"));
5991
5992        // Test invalid hex characters
5993        let result = rpc.sign_transaction("abcg", &mock_signer).await;
5994        assert!(result.is_err());
5995        assert!(result
5996            .unwrap_err()
5997            .to_string()
5998            .contains("invalid hex characters"));
5999
6000        // Test signer failure
6001        let mock_signer = MockSigner {
6002            should_fail: true,
6003            return_value: "".to_string(),
6004        };
6005        let result = rpc.sign_transaction("abcd", &mock_signer).await;
6006        assert!(result.is_err());
6007        assert!(result
6008            .unwrap_err()
6009            .to_string()
6010            .contains("Mock signing failure"));
6011
6012        // Test successful signing
6013        let mock_signer = MockSigner {
6014            should_fail: false,
6015            return_value: "abcd".to_string(),
6016        };
6017        let result = rpc.sign_transaction("abcd", &mock_signer).await;
6018        if result.is_err() {
6019            println!("Error: {}", result.as_ref().unwrap_err());
6020        }
6021        assert!(result.is_ok());
6022        assert_eq!(result.unwrap(), "abcddeadbeefcafebabe1234567890abcdef");
6023    }
6024
6025    #[tokio::test]
6026    async fn test_sign_transaction_validation_edge_cases() {
6027        let rpc = ElementsRpc::new(
6028            "http://localhost:18884".to_string(),
6029            "user".to_string(),
6030            "pass".to_string(),
6031        );
6032
6033        // Mock signer that returns invalid responses
6034        struct BadMockSigner {
6035            return_empty: bool,
6036            return_odd_length: bool,
6037            return_invalid_hex: bool,
6038            return_shorter: bool,
6039        }
6040
6041        #[async_trait::async_trait]
6042        impl crate::signer::Signer for BadMockSigner {
6043            async fn sign_transaction(
6044                &self,
6045                unsigned_tx: &str,
6046            ) -> Result<String, crate::signer::SignerError> {
6047                if self.return_empty {
6048                    Ok("".to_string())
6049                } else if self.return_odd_length {
6050                    Ok("abc".to_string())
6051                } else if self.return_invalid_hex {
6052                    Ok("abcg".to_string())
6053                } else if self.return_shorter {
6054                    Ok("ab".to_string()) // Shorter than input "abcd"
6055                } else {
6056                    Ok(format!("{}deadbeef", unsigned_tx))
6057                }
6058            }
6059
6060            fn as_any(&self) -> &dyn std::any::Any {
6061                self
6062            }
6063        }
6064
6065        // Test signer returning empty string
6066        let bad_signer = BadMockSigner {
6067            return_empty: true,
6068            return_odd_length: false,
6069            return_invalid_hex: false,
6070            return_shorter: false,
6071        };
6072        let result = rpc.sign_transaction("abcd", &bad_signer).await;
6073        assert!(result.is_err());
6074        assert!(result.unwrap_err().to_string().contains("cannot be empty"));
6075
6076        // Test signer returning odd length hex
6077        let bad_signer = BadMockSigner {
6078            return_empty: false,
6079            return_odd_length: true,
6080            return_invalid_hex: false,
6081            return_shorter: false,
6082        };
6083        let result = rpc.sign_transaction("abcd", &bad_signer).await;
6084        assert!(result.is_err());
6085        assert!(result.unwrap_err().to_string().contains("even length"));
6086
6087        // Test signer returning invalid hex
6088        let bad_signer = BadMockSigner {
6089            return_empty: false,
6090            return_odd_length: false,
6091            return_invalid_hex: true,
6092            return_shorter: false,
6093        };
6094        let result = rpc.sign_transaction("abcd", &bad_signer).await;
6095        assert!(result.is_err());
6096        assert!(result
6097            .unwrap_err()
6098            .to_string()
6099            .contains("invalid hex characters"));
6100
6101        // Test signer returning shorter transaction (invalid)
6102        let bad_signer = BadMockSigner {
6103            return_empty: false,
6104            return_odd_length: false,
6105            return_invalid_hex: false,
6106            return_shorter: true,
6107        };
6108        let result = rpc.sign_transaction("abcd", &bad_signer).await;
6109        assert!(result.is_err());
6110        assert!(result
6111            .unwrap_err()
6112            .to_string()
6113            .contains("shorter than unsigned transaction"));
6114    }
6115
6116    #[tokio::test]
6117    async fn test_sign_transaction_minimum_size_validation() {
6118        let rpc = ElementsRpc::new(
6119            "http://localhost:18884".to_string(),
6120            "user".to_string(),
6121            "pass".to_string(),
6122        );
6123
6124        // Mock signer that returns very small transactions
6125        struct TinyMockSigner;
6126
6127        #[async_trait::async_trait]
6128        impl crate::signer::Signer for TinyMockSigner {
6129            async fn sign_transaction(
6130                &self,
6131                _unsigned_tx: &str,
6132            ) -> Result<String, crate::signer::SignerError> {
6133                Ok("abcd".to_string()) // Only 2 bytes when decoded
6134            }
6135
6136            fn as_any(&self) -> &dyn std::any::Any {
6137                self
6138            }
6139        }
6140
6141        let tiny_signer = TinyMockSigner;
6142        let result = rpc.sign_transaction("abcd", &tiny_signer).await;
6143        assert!(result.is_err());
6144        let error_msg = result.unwrap_err().to_string();
6145        assert!(error_msg.contains("minimum size"));
6146        assert!(error_msg.contains("minimum is 10 bytes"));
6147    }
6148
6149    #[tokio::test]
6150    async fn test_sign_transaction_success_case() {
6151        let rpc = ElementsRpc::new(
6152            "http://localhost:18884".to_string(),
6153            "user".to_string(),
6154            "pass".to_string(),
6155        );
6156
6157        // Mock signer that returns a valid signed transaction
6158        struct GoodMockSigner;
6159
6160        #[async_trait::async_trait]
6161        impl crate::signer::Signer for GoodMockSigner {
6162            async fn sign_transaction(
6163                &self,
6164                unsigned_tx: &str,
6165            ) -> Result<String, crate::signer::SignerError> {
6166                // Return a longer valid hex string (20+ bytes when decoded)
6167                Ok(format!("{}deadbeefcafebabe1234567890abcdef", unsigned_tx))
6168            }
6169
6170            fn as_any(&self) -> &dyn std::any::Any {
6171                self
6172            }
6173        }
6174
6175        let good_signer = GoodMockSigner;
6176
6177        // Test with a reasonable sized unsigned transaction
6178        let unsigned_tx = "0200000000010123456789abcdef"; // 14 bytes when decoded
6179        let result = rpc.sign_transaction(unsigned_tx, &good_signer).await;
6180
6181        assert!(result.is_ok());
6182        let signed_tx = result.unwrap();
6183        assert!(signed_tx.starts_with(unsigned_tx));
6184        assert!(signed_tx.len() > unsigned_tx.len());
6185        assert!(signed_tx.contains("deadbeefcafebabe"));
6186    }
6187
6188    #[tokio::test]
6189    async fn test_sign_and_broadcast_transaction_mock() {
6190        // Create a mock server for testing the broadcast part
6191        let server = MockServer::start();
6192
6193        // Mock the RPC response for sendrawtransaction
6194        let mock = server.mock(|when, then| {
6195            when.method(POST).path("/").json_body(serde_json::json!({
6196                "jsonrpc": "1.0",
6197                "id": "amp-client",
6198                "method": "sendrawtransaction",
6199                "params": ["0200000000010123456789abcdefdeadbeefcafebabe1234567890abcdef"]
6200            }));
6201            then.status(200).json_body(serde_json::json!({
6202                "jsonrpc": "1.0",
6203                "id": "amp-client",
6204                "result": "abc123def456789",
6205                "error": null
6206            }));
6207        });
6208
6209        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
6210
6211        // Mock signer for testing
6212        struct TestMockSigner;
6213
6214        #[async_trait::async_trait]
6215        impl crate::signer::Signer for TestMockSigner {
6216            async fn sign_transaction(
6217                &self,
6218                unsigned_tx: &str,
6219            ) -> Result<String, crate::signer::SignerError> {
6220                Ok(format!("{}deadbeefcafebabe1234567890abcdef", unsigned_tx))
6221            }
6222
6223            fn as_any(&self) -> &dyn std::any::Any {
6224                self
6225            }
6226        }
6227
6228        let signer = TestMockSigner;
6229        let unsigned_tx = "0200000000010123456789abcdef";
6230
6231        let result = rpc
6232            .sign_and_broadcast_transaction(unsigned_tx, &signer)
6233            .await;
6234
6235        assert!(result.is_ok());
6236        assert_eq!(result.unwrap(), "abc123def456789");
6237
6238        // Verify the mock was called
6239        mock.assert();
6240    }
6241
6242    #[tokio::test]
6243    async fn test_sign_and_broadcast_transaction_signing_failure() {
6244        let rpc = ElementsRpc::new(
6245            "http://localhost:18884".to_string(),
6246            "user".to_string(),
6247            "pass".to_string(),
6248        );
6249
6250        // Mock signer that fails
6251        struct FailingSigner;
6252
6253        #[async_trait::async_trait]
6254        impl crate::signer::Signer for FailingSigner {
6255            async fn sign_transaction(
6256                &self,
6257                _unsigned_tx: &str,
6258            ) -> Result<String, crate::signer::SignerError> {
6259                Err(crate::signer::SignerError::Lwk(
6260                    "Signing failed".to_string(),
6261                ))
6262            }
6263
6264            fn as_any(&self) -> &dyn std::any::Any {
6265                self
6266            }
6267        }
6268
6269        let failing_signer = FailingSigner;
6270        let result = rpc
6271            .sign_and_broadcast_transaction("abcd", &failing_signer)
6272            .await;
6273
6274        assert!(result.is_err());
6275        let error_msg = result.unwrap_err().to_string();
6276        // The error should be a Signer error containing the original failure message
6277        assert!(error_msg.contains("Signer error"));
6278        assert!(error_msg.contains("Signing failed"));
6279    }
6280
6281    #[tokio::test]
6282    async fn test_sign_and_broadcast_transaction_broadcast_failure() {
6283        // Create a mock server that returns an error for broadcast
6284        let server = MockServer::start();
6285
6286        let mock = server.mock(|when, then| {
6287            when.method(POST).path("/");
6288            then.status(200).json_body(serde_json::json!({
6289                "jsonrpc": "1.0",
6290                "id": "amp-client",
6291                "result": null,
6292                "error": {
6293                    "code": -26,
6294                    "message": "Transaction rejected"
6295                }
6296            }));
6297        });
6298
6299        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
6300
6301        // Mock signer that succeeds
6302        struct WorkingSigner;
6303
6304        #[async_trait::async_trait]
6305        impl crate::signer::Signer for WorkingSigner {
6306            async fn sign_transaction(
6307                &self,
6308                unsigned_tx: &str,
6309            ) -> Result<String, crate::signer::SignerError> {
6310                Ok(format!("{}deadbeefcafebabe1234567890abcdef", unsigned_tx))
6311            }
6312
6313            fn as_any(&self) -> &dyn std::any::Any {
6314                self
6315            }
6316        }
6317
6318        let working_signer = WorkingSigner;
6319        let unsigned_tx = "0200000000010123456789abcdef";
6320
6321        let result = rpc
6322            .sign_and_broadcast_transaction(unsigned_tx, &working_signer)
6323            .await;
6324
6325        assert!(result.is_err());
6326        let error_msg = result.unwrap_err().to_string();
6327        assert!(error_msg.contains("Failed during transaction broadcast phase"));
6328        assert!(error_msg.contains("Transaction rejected"));
6329
6330        mock.assert();
6331    }
6332
6333    #[tokio::test]
6334    async fn test_wait_for_confirmations_success() {
6335        let server = MockServer::start();
6336
6337        let txid = "abc123def456789abc123def456789abc123def456789abc123def456789abc123de";
6338
6339        // First call returns 1 confirmation (not enough)
6340        let _mock_response_1 = serde_json::json!({
6341            "jsonrpc": "1.0",
6342            "id": "amp-client",
6343            "result": {
6344                "txid": txid,
6345                "confirmations": 1,
6346                "blockheight": 12345,
6347                "hex": "0200000000010abc123def456789...",
6348                "blockhash": "def456abc123789def456abc123789def456abc123789def456abc123789def456ab",
6349                "blocktime": 1640995200,
6350                "time": 1640995200,
6351                "timereceived": 1640995180
6352            }
6353        });
6354
6355        // Second call returns 2 confirmations (sufficient)
6356        let mock_response_2 = serde_json::json!({
6357            "jsonrpc": "1.0",
6358            "id": "amp-client",
6359            "result": {
6360                "txid": txid,
6361                "confirmations": 2,
6362                "blockheight": 12345,
6363                "hex": "0200000000010abc123def456789...",
6364                "blockhash": "def456abc123789def456abc123789def456abc123789def456abc123789def456ab",
6365                "blocktime": 1640995200,
6366                "time": 1640995200,
6367                "timereceived": 1640995180
6368            }
6369        });
6370
6371        // Create a mock that returns 2 confirmations immediately (simpler test)
6372        let mock = server.mock(|when, then| {
6373            when.method(POST)
6374                .path("/")
6375                .header("authorization", "Basic dXNlcjpwYXNz")
6376                .json_body(serde_json::json!({
6377                    "jsonrpc": "1.0",
6378                    "id": "amp-client",
6379                    "method": "gettransaction",
6380                    "params": [txid, true]
6381                }));
6382            then.status(200)
6383                .header("content-type", "application/json")
6384                .json_body(mock_response_2); // Return sufficient confirmations immediately
6385        });
6386
6387        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
6388
6389        // Use fast polling (1 second) for testing
6390        let result = rpc
6391            .wait_for_confirmations_with_interval(txid, Some(2), Some(1), Some(1))
6392            .await;
6393
6394        assert!(result.is_ok());
6395        let tx_detail = result.unwrap();
6396        assert_eq!(tx_detail.confirmations, 2);
6397        assert_eq!(tx_detail.txid, txid);
6398
6399        // Mock should have been called once
6400        mock.assert();
6401    }
6402
6403    #[tokio::test]
6404    async fn test_wait_for_confirmations_timeout() {
6405        let server = MockServer::start();
6406
6407        let txid = "abc123def456789abc123def456789abc123def456789abc123def456789abc123de";
6408
6409        // Always return insufficient confirmations
6410        let mock_response = serde_json::json!({
6411            "jsonrpc": "1.0",
6412            "id": "amp-client",
6413            "result": {
6414                "txid": txid,
6415                "confirmations": 1,
6416                "blockheight": 12345,
6417                "hex": "0200000000010abc123def456789...",
6418                "blockhash": null,
6419                "blocktime": null,
6420                "time": null,
6421                "timereceived": null
6422            }
6423        });
6424
6425        let _mock = server.mock(|when, then| {
6426            when.method(POST)
6427                .path("/")
6428                .header("authorization", "Basic dXNlcjpwYXNz")
6429                .json_body(serde_json::json!({
6430                    "jsonrpc": "1.0",
6431                    "id": "amp-client",
6432                    "method": "gettransaction",
6433                    "params": [txid, true]
6434                }));
6435            then.status(200)
6436                .header("content-type", "application/json")
6437                .json_body(mock_response);
6438        });
6439
6440        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
6441
6442        // Use a very short timeout for testing (0 = 3 seconds) and fast polling (1 second)
6443        let result = rpc
6444            .wait_for_confirmations_with_interval(txid, Some(2), Some(0), Some(1))
6445            .await;
6446
6447        assert!(result.is_err());
6448        match result.unwrap_err() {
6449            AmpError::Timeout(msg) => {
6450                assert!(msg.contains("Timeout waiting for confirmations"));
6451                assert!(msg.contains(txid));
6452                assert!(msg.contains("retry confirmation"));
6453            }
6454            _ => panic!("Expected timeout error"),
6455        }
6456
6457        // Mock will be called multiple times during the timeout period
6458        // We don't assert on the exact number since it depends on timing
6459    }
6460
6461    #[tokio::test]
6462    async fn test_wait_for_confirmations_immediate_success() {
6463        let server = MockServer::start();
6464
6465        let txid = "abc123def456789abc123def456789abc123def456789abc123def456789abc123de";
6466
6467        // Transaction already has sufficient confirmations
6468        let mock_response = serde_json::json!({
6469            "jsonrpc": "1.0",
6470            "id": "amp-client",
6471            "result": {
6472                "txid": txid,
6473                "confirmations": 5,
6474                "blockheight": 12345,
6475                "hex": "0200000000010abc123def456789...",
6476                "blockhash": "def456abc123789def456abc123789def456abc123789def456abc123789def456ab",
6477                "blocktime": 1640995200,
6478                "time": 1640995200,
6479                "timereceived": 1640995180
6480            }
6481        });
6482
6483        let mock = server.mock(|when, then| {
6484            when.method(POST)
6485                .path("/")
6486                .header("authorization", "Basic dXNlcjpwYXNz")
6487                .json_body(serde_json::json!({
6488                    "jsonrpc": "1.0",
6489                    "id": "amp-client",
6490                    "method": "gettransaction",
6491                    "params": [txid, true]
6492                }));
6493            then.status(200)
6494                .header("content-type", "application/json")
6495                .json_body(mock_response);
6496        });
6497
6498        let rpc = ElementsRpc::new(server.url("/"), "user".to_string(), "pass".to_string());
6499
6500        let result = rpc.wait_for_confirmations(txid, Some(2), Some(10)).await;
6501
6502        assert!(result.is_ok());
6503        let tx_detail = result.unwrap();
6504        assert_eq!(tx_detail.confirmations, 5);
6505        assert_eq!(tx_detail.txid, txid);
6506
6507        // Should only need one call since confirmations are already sufficient
6508        mock.assert();
6509    }
6510}
6511
6512/// Configuration for retry behavior in API requests
6513#[derive(Debug, Clone)]
6514pub struct RetryConfig {
6515    /// Maximum number of retry attempts
6516    pub max_attempts: u32,
6517    /// Base delay in milliseconds for exponential backoff
6518    pub base_delay_ms: u64,
6519    /// Maximum delay in milliseconds to cap exponential backoff
6520    pub max_delay_ms: u64,
6521    /// Request timeout in seconds
6522    pub timeout_seconds: u64,
6523}
6524
6525impl Default for RetryConfig {
6526    fn default() -> Self {
6527        Self {
6528            max_attempts: 3,
6529            base_delay_ms: 1000,
6530            max_delay_ms: 30_000,
6531            timeout_seconds: 10,
6532        }
6533    }
6534}
6535
6536impl RetryConfig {
6537    /// Creates a `RetryConfig` from environment variables with default fallbacks
6538    ///
6539    /// Environment variables:
6540    /// - `API_RETRY_MAX_ATTEMPTS`: Maximum retry attempts (default: 3)
6541    /// - `API_RETRY_BASE_DELAY_MS`: Base delay in milliseconds (default: 1000)
6542    /// - `API_RETRY_MAX_DELAY_MS`: Maximum delay in milliseconds (default: 30000)
6543    /// - `API_REQUEST_TIMEOUT_SECONDS`: Request timeout in seconds (default: 10)
6544    ///
6545    /// # Errors
6546    ///
6547    /// Returns an error if any environment variable contains an invalid value
6548    pub fn from_env() -> Result<Self, Error> {
6549        let max_attempts = match env::var("API_RETRY_MAX_ATTEMPTS") {
6550            Ok(val) => val.parse::<u32>().map_err(|e| {
6551                Error::InvalidRetryConfig(format!("Invalid API_RETRY_MAX_ATTEMPTS: {e}"))
6552            })?,
6553            Err(_) => 3,
6554        };
6555
6556        let base_delay_ms = match env::var("API_RETRY_BASE_DELAY_MS") {
6557            Ok(val) => val.parse::<u64>().map_err(|e| {
6558                Error::InvalidRetryConfig(format!("Invalid API_RETRY_BASE_DELAY_MS: {e}"))
6559            })?,
6560            Err(_) => 1000,
6561        };
6562
6563        let max_delay_ms = match env::var("API_RETRY_MAX_DELAY_MS") {
6564            Ok(val) => val.parse::<u64>().map_err(|e| {
6565                Error::InvalidRetryConfig(format!("Invalid API_RETRY_MAX_DELAY_MS: {e}"))
6566            })?,
6567            Err(_) => 30_000,
6568        };
6569
6570        let timeout_seconds = match env::var("API_REQUEST_TIMEOUT_SECONDS") {
6571            Ok(val) => val.parse::<u64>().map_err(|e| {
6572                Error::InvalidRetryConfig(format!("Invalid API_REQUEST_TIMEOUT_SECONDS: {e}"))
6573            })?,
6574            Err(_) => 10,
6575        };
6576
6577        // Validate configuration
6578        if max_attempts == 0 {
6579            return Err(Error::InvalidRetryConfig(
6580                "max_attempts must be greater than 0".to_string(),
6581            ));
6582        }
6583        if base_delay_ms == 0 {
6584            return Err(Error::InvalidRetryConfig(
6585                "base_delay_ms must be greater than 0".to_string(),
6586            ));
6587        }
6588        if max_delay_ms < base_delay_ms {
6589            return Err(Error::InvalidRetryConfig(
6590                "max_delay_ms must be greater than or equal to base_delay_ms".to_string(),
6591            ));
6592        }
6593        if timeout_seconds == 0 {
6594            return Err(Error::InvalidRetryConfig(
6595                "timeout_seconds must be greater than 0".to_string(),
6596            ));
6597        }
6598
6599        Ok(Self {
6600            max_attempts,
6601            base_delay_ms,
6602            max_delay_ms,
6603            timeout_seconds,
6604        })
6605    }
6606
6607    /// Creates a `RetryConfig` optimized for test environments
6608    ///
6609    /// Uses reduced values for faster test execution:
6610    /// - 2 retry attempts
6611    /// - 500ms base delay
6612    /// - 5000ms max delay
6613    /// - 5 second timeout
6614    #[must_use]
6615    pub const fn for_tests() -> Self {
6616        Self {
6617            max_attempts: 2,
6618            base_delay_ms: 500,
6619            max_delay_ms: 5000,
6620            timeout_seconds: 5,
6621        }
6622    }
6623
6624    /// Sets a custom timeout value
6625    #[must_use]
6626    pub const fn with_timeout(mut self, timeout_seconds: u64) -> Self {
6627        self.timeout_seconds = timeout_seconds;
6628        self
6629    }
6630
6631    /// Sets custom max attempts
6632    #[must_use]
6633    pub const fn with_max_attempts(mut self, max_attempts: u32) -> Self {
6634        self.max_attempts = max_attempts;
6635        self
6636    }
6637
6638    /// Sets custom base delay
6639    #[must_use]
6640    pub const fn with_base_delay_ms(mut self, base_delay_ms: u64) -> Self {
6641        self.base_delay_ms = base_delay_ms;
6642        self
6643    }
6644
6645    /// Sets custom max delay
6646    #[must_use]
6647    pub const fn with_max_delay_ms(mut self, max_delay_ms: u64) -> Self {
6648        self.max_delay_ms = max_delay_ms;
6649        self
6650    }
6651}
6652
6653/// HTTP client with sophisticated retry logic and exponential backoff
6654#[derive(Debug, Clone)]
6655pub struct RetryClient {
6656    client: Client,
6657    config: RetryConfig,
6658}
6659
6660impl RetryClient {
6661    /// Creates a new `RetryClient` with the given configuration
6662    #[must_use]
6663    pub fn new(config: RetryConfig) -> Self {
6664        Self {
6665            client: Client::new(),
6666            config,
6667        }
6668    }
6669
6670    /// Creates a new `RetryClient` with default configuration
6671    #[must_use]
6672    pub fn with_default_config() -> Self {
6673        Self::new(RetryConfig::default())
6674    }
6675
6676    /// Creates a new `RetryClient` with test-optimized configuration
6677    #[must_use]
6678    pub fn for_tests() -> Self {
6679        Self::new(RetryConfig::for_tests())
6680    }
6681
6682    /// Executes an HTTP request with retry logic and exponential backoff
6683    ///
6684    /// # Arguments
6685    /// * `request_builder` - A function that creates the request builder
6686    ///
6687    /// # Returns
6688    /// The response if successful, or an error after all retries are exhausted
6689    ///
6690    /// # Errors
6691    /// Returns `TokenError::Timeout` if the request times out
6692    /// Returns `TokenError::RateLimited` if rate limited and retries are exhausted
6693    /// Returns `TokenError::ObtainFailed` if all retry attempts fail
6694    #[allow(clippy::cognitive_complexity)]
6695    pub async fn execute_with_retry<F>(
6696        &self,
6697        request_builder: F,
6698    ) -> Result<reqwest::Response, TokenError>
6699    where
6700        F: Fn() -> reqwest::RequestBuilder + Send + Sync,
6701    {
6702        let mut last_error = String::new();
6703        let mut attempt = 0;
6704
6705        while attempt < self.config.max_attempts {
6706            attempt += 1;
6707
6708            // Create the request with timeout
6709            let request =
6710                request_builder().timeout(StdDuration::from_secs(self.config.timeout_seconds));
6711
6712            // Execute the request
6713            match request.send().await {
6714                Ok(response) => {
6715                    let status = response.status();
6716
6717                    // Handle rate limiting (429 Too Many Requests)
6718                    if status == reqwest::StatusCode::TOO_MANY_REQUESTS {
6719                        let retry_after = Self::extract_retry_after(&response).unwrap_or(60);
6720
6721                        tracing::warn!(
6722                            "Rate limited (429) on attempt {}/{}. Retry after {} seconds",
6723                            attempt,
6724                            self.config.max_attempts,
6725                            retry_after
6726                        );
6727
6728                        // If this is our last attempt, return the rate limit error
6729                        if attempt >= self.config.max_attempts {
6730                            return Err(TokenError::rate_limited(retry_after));
6731                        }
6732
6733                        // Wait for the rate limit period (or our max delay, whichever is smaller)
6734                        let delay_ms = std::cmp::min(retry_after * 1000, self.config.max_delay_ms);
6735                        sleep(StdDuration::from_millis(delay_ms)).await;
6736                        continue;
6737                    }
6738
6739                    // Handle other client errors (4xx) - these are generally not retryable
6740                    if status.is_client_error() && status != reqwest::StatusCode::TOO_MANY_REQUESTS
6741                    {
6742                        last_error = format!("Client error: {status}");
6743                        tracing::error!("Non-retryable client error: {}", status);
6744                        break;
6745                    }
6746
6747                    // Handle server errors (5xx) - these are retryable
6748                    if status.is_server_error() {
6749                        last_error = format!("Server error: {status}");
6750                        tracing::warn!(
6751                            "Server error {} on attempt {}/{}",
6752                            status,
6753                            attempt,
6754                            self.config.max_attempts
6755                        );
6756
6757                        if attempt < self.config.max_attempts {
6758                            let delay = self.calculate_backoff_delay(attempt);
6759                            sleep(delay).await;
6760                            continue;
6761                        }
6762                        break;
6763                    }
6764
6765                    // Success case
6766                    return Ok(response);
6767                }
6768                Err(e) => {
6769                    last_error = e.to_string();
6770
6771                    // Check if this is a timeout error
6772                    if e.is_timeout() {
6773                        tracing::warn!(
6774                            "Request timeout on attempt {}/{}",
6775                            attempt,
6776                            self.config.max_attempts
6777                        );
6778
6779                        if attempt >= self.config.max_attempts {
6780                            return Err(TokenError::timeout(self.config.timeout_seconds));
6781                        }
6782                    } else {
6783                        tracing::warn!(
6784                            "Request failed on attempt {}/{}: {}",
6785                            attempt,
6786                            self.config.max_attempts,
6787                            e
6788                        );
6789                    }
6790
6791                    // If we have more attempts, wait and retry
6792                    if attempt < self.config.max_attempts {
6793                        let delay = self.calculate_backoff_delay(attempt);
6794                        sleep(delay).await;
6795                    }
6796                }
6797            }
6798        }
6799
6800        // All retries exhausted
6801        Err(TokenError::obtain_failed(attempt, last_error))
6802    }
6803
6804    /// Calculates the delay for exponential backoff with jitter
6805    ///
6806    /// Uses the formula: `min(base_delay * 2^(attempt-1) + jitter, max_delay)`
6807    /// where jitter is a random value between 0 and `base_delay/2`
6808    pub fn calculate_backoff_delay(&self, attempt: u32) -> StdDuration {
6809        use rand::Rng;
6810
6811        let base_delay = self.config.base_delay_ms;
6812        let max_delay = self.config.max_delay_ms;
6813
6814        // Calculate exponential backoff: base_delay * 2^(attempt-1)
6815        let exponential_delay = base_delay * 2_u64.pow(attempt.saturating_sub(1));
6816
6817        // Add jitter (random value between 0 and base_delay/2)
6818        let jitter = rand::thread_rng().gen_range(0..=base_delay / 2);
6819        let total_delay = exponential_delay + jitter;
6820
6821        // Cap at max_delay
6822        let final_delay = std::cmp::min(total_delay, max_delay);
6823
6824        tracing::debug!(
6825            "Calculated backoff delay for attempt {}: {}ms (exponential: {}ms, jitter: {}ms, capped at: {}ms)",
6826            attempt,
6827            final_delay,
6828            exponential_delay,
6829            jitter,
6830            max_delay
6831        );
6832
6833        StdDuration::from_millis(final_delay)
6834    }
6835
6836    /// Extracts the Retry-After header value from a 429 response
6837    ///
6838    /// Returns the number of seconds to wait, or None if the header is not present
6839    /// or cannot be parsed
6840    fn extract_retry_after(response: &reqwest::Response) -> Option<u64> {
6841        response
6842            .headers()
6843            .get("retry-after")
6844            .and_then(|value| value.to_str().ok())
6845            .and_then(|s| s.parse::<u64>().ok())
6846    }
6847
6848    /// Gets the underlying reqwest client
6849    #[must_use]
6850    pub const fn client(&self) -> &Client {
6851        &self.client
6852    }
6853
6854    /// Gets the retry configuration
6855    #[must_use]
6856    pub const fn config(&self) -> &RetryConfig {
6857        &self.config
6858    }
6859}
6860
6861/// Singleton instance of the `TokenManager` for shared token storage across all `ApiClient` instances
6862static GLOBAL_TOKEN_MANAGER: OnceCell<Arc<TokenManager>> = OnceCell::const_new();
6863
6864/// Core token manager with proactive refresh and secure storage
6865#[derive(Debug)]
6866pub struct TokenManager {
6867    pub token_data: Arc<Mutex<Option<TokenData>>>,
6868    pub retry_client: RetryClient,
6869    base_url: Url,
6870    /// Semaphore to ensure only one token operation (obtain/refresh) happens at a time
6871    /// This prevents race conditions where multiple threads try to refresh/obtain simultaneously
6872    token_operation_semaphore: Arc<Semaphore>,
6873}
6874
6875impl TokenManager {
6876    /// Gets the global singleton instance of `TokenManager`
6877    ///
6878    /// This ensures all `ApiClient` instances share the same token storage,
6879    /// preventing multiple token acquisition attempts in concurrent tests.
6880    ///
6881    /// # Errors
6882    /// Returns an error if the `TokenManager` cannot be initialized
6883    pub async fn get_global_instance() -> Result<Arc<Self>, Error> {
6884        let manager = GLOBAL_TOKEN_MANAGER
6885            .get_or_try_init(|| async {
6886                let config = RetryConfig::from_env()?;
6887                let base_url = get_amp_api_base_url()?;
6888                let manager = Self::with_config_and_base_url(config, base_url).await?;
6889                Ok::<Arc<Self>, Error>(Arc::new(manager))
6890            })
6891            .await?;
6892
6893        Ok(manager.clone())
6894    }
6895
6896    /// Creates a new `TokenManager` with default configuration
6897    ///
6898    /// # Errors
6899    /// Returns an error if the base URL cannot be obtained from environment variables
6900    pub async fn new() -> Result<Self, Error> {
6901        let config = RetryConfig::from_env()?;
6902        Self::with_config(config).await
6903    }
6904
6905    /// Creates a new `TokenManager` with the specified retry configuration
6906    ///
6907    /// # Errors
6908    /// Returns an error if the base URL cannot be obtained from environment variables
6909    pub async fn with_config(config: RetryConfig) -> Result<Self, Error> {
6910        let base_url = get_amp_api_base_url()?;
6911        Self::with_config_and_base_url(config, base_url).await
6912    }
6913
6914    /// Creates a new `TokenManager` with the specified configuration and base URL (for testing)
6915    ///
6916    /// # Errors
6917    /// This method is infallible but returns Result for API consistency
6918    pub async fn with_config_and_base_url(
6919        config: RetryConfig,
6920        base_url: Url,
6921    ) -> Result<Self, Error> {
6922        let manager = Self {
6923            token_data: Arc::new(Mutex::new(None)),
6924            retry_client: RetryClient::new(config),
6925            base_url,
6926            token_operation_semaphore: Arc::new(Semaphore::new(1)),
6927        };
6928
6929        // Load token from disk if persistence is enabled
6930        if Self::should_persist_tokens() {
6931            if let Ok(Some(token_data)) = manager.load_token_from_disk().await {
6932                *manager.token_data.lock().await = Some(token_data);
6933                tracing::info!("Token loaded from disk during initialization");
6934            }
6935        }
6936
6937        Ok(manager)
6938    }
6939
6940    /// Creates a new `TokenManager` with a pre-set mock token (for testing)
6941    ///
6942    /// # Errors
6943    /// This method is infallible but returns Result for API consistency
6944    pub fn with_mock_token(
6945        config: RetryConfig,
6946        base_url: Url,
6947        mock_token: String,
6948    ) -> Result<Self, Error> {
6949        let expires_at = Utc::now() + Duration::hours(24); // Mock token valid for 24 hours
6950        let token_data = TokenData::new(mock_token, expires_at);
6951
6952        let manager = Self {
6953            token_data: Arc::new(Mutex::new(Some(token_data))),
6954            retry_client: RetryClient::new(config),
6955            base_url,
6956            token_operation_semaphore: Arc::new(Semaphore::new(1)),
6957        };
6958
6959        Ok(manager)
6960    }
6961
6962    /// Gets a valid authentication token with proactive refresh logic
6963    ///
6964    /// This method implements thread-safe token management logic:
6965    /// 1. Check if a valid token exists and is not expiring soon (within 5 minutes)
6966    /// 2. If token needs refresh/obtain, acquire semaphore to prevent concurrent operations
6967    /// 3. Double-check token state after acquiring semaphore (another thread may have updated it)
6968    /// 4. Perform atomic token update operations
6969    /// 5. Return the valid token
6970    ///
6971    /// # Thread Safety
6972    /// This method is fully thread-safe and prevents race conditions by:
6973    /// - Using a semaphore to ensure only one token operation at a time
6974    /// - Double-checking token state after acquiring the semaphore
6975    /// - Performing atomic token updates within the critical section
6976    ///
6977    /// # Errors
6978    /// Returns a `TokenError` if token acquisition or refresh fails after all retries
6979    pub async fn get_token(&self) -> Result<String, Error> {
6980        // Fast path: check if we have a valid token without acquiring semaphore
6981        if let Some(token) = self.check_existing_token().await? {
6982            return Ok(token);
6983        }
6984
6985        // Slow path: token needs refresh/obtain, acquire semaphore for thread safety
6986        let _permit = self.acquire_token_semaphore().await?;
6987
6988        // Double-check token state after acquiring semaphore - another thread may have updated it
6989        if let Some(token) = self.check_existing_token().await? {
6990            tracing::debug!("Token was updated by another thread, using existing valid token");
6991            return Ok(token);
6992        }
6993
6994        // At this point, we need to refresh or obtain a new token
6995        self.handle_token_refresh_or_obtain().await
6996    }
6997
6998    /// Checks if we have a valid existing token that doesn't expire soon
6999    async fn check_existing_token(&self) -> Result<Option<String>, Error> {
7000        let token_guard = self.token_data.lock().await;
7001        if let Some(ref token_data) = *token_guard {
7002            if !token_data.expires_soon(Duration::minutes(5)) {
7003                tracing::debug!("Using existing valid token");
7004                let token = token_data.token.expose_secret().clone();
7005                drop(token_guard);
7006                return Ok(Some(token));
7007            }
7008        }
7009        drop(token_guard);
7010        Ok(None)
7011    }
7012
7013    /// Acquires the token operation semaphore for thread-safe operations
7014    async fn acquire_token_semaphore(&self) -> Result<tokio::sync::SemaphorePermit<'_>, Error> {
7015        let permit = self
7016            .token_operation_semaphore
7017            .acquire()
7018            .await
7019            .map_err(|e| {
7020                Error::Token(TokenError::storage(format!(
7021                    "Failed to acquire token operation semaphore: {e}"
7022                )))
7023            })?;
7024
7025        tracing::debug!("Acquired token operation semaphore for thread-safe token management");
7026        Ok(permit)
7027    }
7028
7029    /// Handles the token refresh or obtain logic
7030    async fn handle_token_refresh_or_obtain(&self) -> Result<String, Error> {
7031        let needs_refresh = self.determine_token_operation().await;
7032
7033        if needs_refresh {
7034            match self.refresh_token_internal().await {
7035                Ok(token) => {
7036                    tracing::info!("Token refreshed successfully");
7037                    return Ok(token);
7038                }
7039                Err(e) => {
7040                    tracing::warn!("Token refresh failed, falling back to obtain: {e}");
7041                    // Fall through to obtain new token
7042                }
7043            }
7044        }
7045
7046        // Either we needed to obtain from the start, or refresh failed
7047        self.obtain_token_internal().await
7048    }
7049
7050    /// Determines whether we need to refresh or obtain a new token
7051    async fn determine_token_operation(&self) -> bool {
7052        let token_guard = self.token_data.lock().await;
7053        token_guard.as_ref().map_or_else(
7054            || {
7055                tracing::info!("No token exists, will obtain new token");
7056                false
7057            },
7058            |token_data| {
7059                if token_data.is_expired() {
7060                    tracing::info!("Token is expired, will obtain new token");
7061                    false
7062                } else {
7063                    tracing::info!("Token expires soon, will attempt refresh");
7064                    true
7065                }
7066            },
7067        )
7068    }
7069
7070    /// Obtains a new authentication token using environment credentials with retry logic
7071    ///
7072    /// This method:
7073    /// 1. Reads credentials from environment variables
7074    /// 2. Makes a token request with retry logic
7075    /// 3. Stores the new token with 24-hour expiry
7076    /// 4. Returns the token string
7077    ///
7078    /// # Thread Safety
7079    /// This method acquires the token operation semaphore to ensure thread-safe operation.
7080    /// For internal use within already-synchronized contexts, use `obtain_token_internal()`.
7081    ///
7082    /// # Errors
7083    /// Returns an error if:
7084    /// - Environment variables are missing
7085    /// - All retry attempts fail
7086    /// - Response parsing fails
7087    pub async fn obtain_token(&self) -> Result<String, Error> {
7088        let _permit = self
7089            .token_operation_semaphore
7090            .acquire()
7091            .await
7092            .map_err(|e| {
7093                Error::Token(TokenError::storage(format!(
7094                    "Failed to acquire token operation semaphore: {e}"
7095                )))
7096            })?;
7097
7098        self.obtain_token_internal().await
7099    }
7100
7101    /// Internal method to obtain a new authentication token without acquiring semaphore
7102    ///
7103    /// This method should only be called from contexts where the token operation semaphore
7104    /// has already been acquired (e.g., from within `get_token()`).
7105    ///
7106    /// # Errors
7107    /// Returns an error if:
7108    /// - Environment variables are missing
7109    /// - All retry attempts fail
7110    /// - Response parsing fails
7111    async fn obtain_token_internal(&self) -> Result<String, Error> {
7112        tracing::debug!("Obtaining new authentication token");
7113
7114        let request_payload = Self::get_credentials_from_env()?;
7115        let url = self.build_obtain_token_url();
7116        let response = self.execute_token_request(&url, &request_payload).await?;
7117        let token_response = self.parse_token_response(response).await?;
7118
7119        self.store_token_data(&token_response.token).await;
7120
7121        tracing::info!("New authentication token obtained successfully");
7122        Ok(token_response.token)
7123    }
7124
7125    /// Gets credentials from environment variables
7126    fn get_credentials_from_env() -> Result<TokenRequest, Error> {
7127        let username = env::var("AMP_USERNAME")
7128            .map_err(|_| Error::MissingEnvVar("AMP_USERNAME".to_string()))?;
7129        let password = env::var("AMP_PASSWORD")
7130            .map_err(|_| Error::MissingEnvVar("AMP_PASSWORD".to_string()))?;
7131
7132        Ok(TokenRequest { username, password })
7133    }
7134
7135    /// Builds the URL for token obtain endpoint
7136    fn build_obtain_token_url(&self) -> Url {
7137        let mut url = self.base_url.clone();
7138        url.path_segments_mut()
7139            .unwrap()
7140            .push("user")
7141            .push("obtain_token");
7142        url
7143    }
7144
7145    /// Executes the token request with retry logic
7146    async fn execute_token_request(
7147        &self,
7148        url: &Url,
7149        request_payload: &TokenRequest,
7150    ) -> Result<reqwest::Response, Error> {
7151        let response = self
7152            .retry_client
7153            .execute_with_retry(|| {
7154                self.retry_client
7155                    .client()
7156                    .post(url.clone())
7157                    .json(request_payload)
7158            })
7159            .await
7160            .map_err(Error::Token)?;
7161
7162        if !response.status().is_success() {
7163            let status = response.status();
7164            let error_text = response
7165                .text()
7166                .await
7167                .unwrap_or_else(|_| "Unknown error".to_string());
7168            return Err(Error::TokenRequestFailed { status, error_text });
7169        }
7170
7171        Ok(response)
7172    }
7173
7174    /// Parses the token response from the API
7175    async fn parse_token_response(
7176        &self,
7177        response: reqwest::Response,
7178    ) -> Result<TokenResponse, Error> {
7179        response
7180            .json()
7181            .await
7182            .map_err(|e| Error::ResponseParsingFailed(e.to_string()))
7183    }
7184
7185    /// Stores the token data with 24-hour expiry and optional disk persistence
7186    async fn store_token_data(&self, token: &str) {
7187        let expires_at = Utc::now() + Duration::days(1);
7188        let token_data = TokenData::new(token.to_string(), expires_at);
7189
7190        // Atomic token update - hold the lock for the minimal time needed
7191        *self.token_data.lock().await = Some(token_data.clone());
7192        tracing::debug!("Token data updated atomically in storage");
7193
7194        // Save to disk if persistence is enabled
7195        if Self::should_persist_tokens() {
7196            if let Err(e) = self.save_token_to_disk(&token_data).await {
7197                tracing::warn!("Failed to save token to disk: {e}");
7198            }
7199        }
7200    }
7201
7202    /// Refreshes the current authentication token with fallback to obtain on failure
7203    ///
7204    /// This method:
7205    /// 1. Uses the existing token to request a refresh
7206    /// 2. Updates the stored token data on success
7207    /// 3. Falls back to obtaining a new token if refresh fails
7208    ///
7209    /// # Thread Safety
7210    /// This method acquires the token operation semaphore to ensure thread-safe operation.
7211    /// For internal use within already-synchronized contexts, use `refresh_token_internal()`.
7212    ///
7213    /// # Errors
7214    /// Returns an error if both refresh and obtain operations fail
7215    pub async fn refresh_token(&self) -> Result<String, Error> {
7216        let _permit = self
7217            .token_operation_semaphore
7218            .acquire()
7219            .await
7220            .map_err(|e| {
7221                Error::Token(TokenError::storage(format!(
7222                    "Failed to acquire token operation semaphore: {e}"
7223                )))
7224            })?;
7225
7226        self.refresh_token_internal().await
7227    }
7228
7229    /// Internal method to refresh the current authentication token without acquiring semaphore
7230    ///
7231    /// This method should only be called from contexts where the token operation semaphore
7232    /// has already been acquired (e.g., from within `get_token()`).
7233    ///
7234    /// # Errors
7235    /// Returns an error if both refresh and obtain operations fail
7236    #[allow(clippy::cognitive_complexity)]
7237    async fn refresh_token_internal(&self) -> Result<String, Error> {
7238        tracing::debug!("Refreshing authentication token");
7239
7240        let Some(current_token) = self.get_current_token_for_refresh().await else {
7241            tracing::warn!("No token available for refresh, obtaining new token");
7242            return self.obtain_token_internal().await;
7243        };
7244
7245        let url = self.build_refresh_token_url();
7246        let response = self.execute_refresh_request(&url, &current_token).await;
7247
7248        match response {
7249            Ok(resp) => self.handle_refresh_response(resp).await,
7250            Err(e) => {
7251                tracing::warn!("Token refresh request failed: {e}, falling back to obtain");
7252                self.obtain_token_internal().await
7253            }
7254        }
7255    }
7256
7257    /// Gets the current token for refresh operations
7258    async fn get_current_token_for_refresh(&self) -> Option<String> {
7259        let token_guard = self.token_data.lock().await;
7260        token_guard
7261            .as_ref()
7262            .map(|token_data| token_data.token.expose_secret().clone())
7263    }
7264
7265    /// Builds the URL for token refresh endpoint
7266    fn build_refresh_token_url(&self) -> Url {
7267        let mut url = self.base_url.clone();
7268        url.path_segments_mut()
7269            .unwrap()
7270            .push("user")
7271            .push("refresh_token");
7272        url
7273    }
7274
7275    /// Executes the refresh request with retry logic
7276    async fn execute_refresh_request(
7277        &self,
7278        url: &Url,
7279        current_token: &str,
7280    ) -> Result<reqwest::Response, TokenError> {
7281        self.retry_client
7282            .execute_with_retry(|| {
7283                self.retry_client
7284                    .client()
7285                    .post(url.clone())
7286                    .header(AUTHORIZATION, format!("token {current_token}"))
7287            })
7288            .await
7289    }
7290
7291    /// Handles the refresh response, either storing the new token or falling back to obtain
7292    async fn handle_refresh_response(&self, resp: reqwest::Response) -> Result<String, Error> {
7293        if !resp.status().is_success() {
7294            let status = resp.status();
7295            let error_text = resp
7296                .text()
7297                .await
7298                .unwrap_or_else(|_| "Unknown error".to_string());
7299
7300            tracing::warn!("Token refresh failed with status {status}: {error_text}");
7301            return self.obtain_token_internal().await;
7302        }
7303
7304        let token_response: TokenResponse = resp
7305            .json()
7306            .await
7307            .map_err(|e| Error::ResponseParsingFailed(e.to_string()))?;
7308
7309        self.store_token_data(&token_response.token).await;
7310        tracing::info!("Authentication token refreshed successfully");
7311        Ok(token_response.token)
7312    }
7313
7314    /// Gets current token information for debugging and monitoring
7315    ///
7316    /// Returns detailed information about the current token including:
7317    /// - Expiry time and remaining duration
7318    /// - Token age since acquisition
7319    /// - Expiry status flags
7320    ///
7321    /// # Returns
7322    /// `Some(TokenInfo)` if a token exists, `None` if no token is stored
7323    ///
7324    /// # Errors
7325    /// Returns an error if token information retrieval fails
7326    pub async fn get_token_info(&self) -> Result<Option<TokenInfo>, Error> {
7327        tracing::debug!("Retrieving token information for debugging");
7328
7329        let token_info = self.token_data.lock().await.as_ref().map(TokenInfo::from);
7330
7331        match &token_info {
7332            Some(info) => {
7333                tracing::debug!(
7334                    "Token info retrieved - expires_at: {}, age: {:?}, expires_in: {:?}, is_expired: {}, expires_soon: {}",
7335                    info.expires_at,
7336                    info.age,
7337                    info.expires_in,
7338                    info.is_expired,
7339                    info.expires_soon
7340                );
7341            }
7342            None => {
7343                tracing::debug!("No token information available - no token stored");
7344            }
7345        }
7346
7347        Ok(token_info)
7348    }
7349
7350    /// Clears the stored token (useful for testing scenarios)
7351    ///
7352    /// This method removes the current token from storage, forcing the next
7353    /// `get_token()` call to obtain a fresh token.
7354    ///
7355    /// # Errors
7356    /// Returns an error if token clearing fails
7357    pub async fn clear_token(&self) -> Result<(), Error> {
7358        tracing::debug!("Clearing stored token from memory and disk");
7359
7360        let had_token = self.clear_token_from_memory().await;
7361        self.clear_token_from_disk_if_enabled().await;
7362        Self::log_token_clear_result(had_token);
7363
7364        Ok(())
7365    }
7366
7367    /// Clears the token from memory and returns whether a token was present
7368    async fn clear_token_from_memory(&self) -> bool {
7369        let mut token_guard = self.token_data.lock().await;
7370        let had_token = token_guard.is_some();
7371        *token_guard = None;
7372        drop(token_guard);
7373        had_token
7374    }
7375
7376    /// Clears the token from disk if persistence is enabled
7377    async fn clear_token_from_disk_if_enabled(&self) {
7378        if Self::should_persist_tokens() {
7379            if let Err(e) = self.remove_token_from_disk().await {
7380                tracing::warn!("Failed to remove token from disk: {e}");
7381            }
7382        }
7383    }
7384
7385    /// Logs the result of the token clearing operation
7386    fn log_token_clear_result(had_token: bool) {
7387        if had_token {
7388            tracing::info!("Token successfully cleared from memory and disk - next get_token() will obtain fresh token");
7389        } else {
7390            tracing::debug!("No token was stored to clear");
7391        }
7392    }
7393
7394    /// Forces a token refresh regardless of current token status
7395    ///
7396    /// This method bypasses the normal proactive refresh logic and immediately
7397    /// attempts to refresh the current token. If no token exists or refresh fails,
7398    /// it falls back to obtaining a new token.
7399    ///
7400    /// # Thread Safety
7401    /// This method is fully thread-safe and uses the same semaphore-based synchronization
7402    /// as other token operations to prevent race conditions.
7403    ///
7404    /// # Errors
7405    /// Returns an error if both refresh and obtain operations fail
7406    pub async fn force_refresh(&self) -> Result<String, Error> {
7407        tracing::info!("Forcing token refresh - bypassing normal proactive refresh logic");
7408
7409        let _permit = self.acquire_token_semaphore().await?;
7410        self.log_token_status_for_refresh().await;
7411        self.execute_forced_refresh().await
7412    }
7413
7414    /// Logs the current token status for forced refresh operation
7415    async fn log_token_status_for_refresh(&self) {
7416        let has_token = {
7417            let token_guard = self.token_data.lock().await;
7418            token_guard.is_some()
7419        };
7420
7421        if has_token {
7422            tracing::debug!("Existing token found, attempting forced refresh");
7423        } else {
7424            tracing::debug!("No existing token found, will obtain new token");
7425        }
7426    }
7427
7428    /// Executes the forced refresh operation
7429    async fn execute_forced_refresh(&self) -> Result<String, Error> {
7430        match self.refresh_token_internal().await {
7431            Ok(token) => {
7432                tracing::info!("Forced token refresh completed successfully");
7433                Ok(token)
7434            }
7435            Err(e) => {
7436                tracing::error!("Forced token refresh failed: {e}");
7437                Err(e)
7438            }
7439        }
7440    }
7441
7442    /// Determines if token persistence is enabled based on environment variables
7443    ///
7444    /// Token persistence is enabled when:
7445    /// - `AMP_TESTS=live` (for live API testing)
7446    /// - `AMP_TOKEN_PERSISTENCE=true` is set
7447    /// - NOT in mock test environments (to prevent test pollution)
7448    fn should_persist_tokens() -> bool {
7449        // Use the new environment detection logic
7450        let environment = TokenEnvironment::detect();
7451
7452        // Never persist tokens in mock environments to prevent test pollution
7453        if environment.is_mock() {
7454            tracing::debug!("Token persistence disabled - mock environment detected");
7455            return false;
7456        }
7457
7458        // Check if explicitly enabled
7459        if env::var("AMP_TOKEN_PERSISTENCE").unwrap_or_default() == "true" {
7460            tracing::debug!("Token persistence enabled - AMP_TOKEN_PERSISTENCE=true");
7461            return true;
7462        }
7463
7464        // Use environment-based persistence setting
7465        let should_persist = environment.should_persist_tokens();
7466        tracing::debug!(
7467            "Token persistence setting from environment: {}",
7468            should_persist
7469        );
7470        should_persist
7471    }
7472
7473    /// Loads token data from disk if it exists and is valid
7474    async fn load_token_from_disk(&self) -> Result<Option<TokenData>, Error> {
7475        let token_file = "token.json";
7476
7477        if !self.token_file_exists(token_file).await {
7478            return Ok(None);
7479        }
7480
7481        let content = self.read_token_file(token_file).await?;
7482        self.parse_and_validate_token(token_file, &content).await
7483    }
7484
7485    /// Checks if the token file exists on disk
7486    async fn token_file_exists(&self, token_file: &str) -> bool {
7487        tokio::fs::try_exists(token_file).await.map_or_else(
7488            |_| {
7489                tracing::debug!("Error checking token file existence: {}", token_file);
7490                false
7491            },
7492            |exists| {
7493                if !exists {
7494                    tracing::debug!("Token file does not exist: {}", token_file);
7495                }
7496                exists
7497            },
7498        )
7499    }
7500
7501    /// Reads the token file content from disk
7502    async fn read_token_file(&self, token_file: &str) -> Result<String, Error> {
7503        use tokio::fs;
7504
7505        match fs::read_to_string(token_file).await {
7506            Ok(content) => Ok(content),
7507            Err(e) => {
7508                tracing::warn!("Failed to read token file: {e}");
7509                Err(Error::Token(TokenError::storage(format!(
7510                    "Failed to read token file: {e}"
7511                ))))
7512            }
7513        }
7514    }
7515
7516    /// Parses token content and validates expiration
7517    async fn parse_and_validate_token(
7518        &self,
7519        token_file: &str,
7520        content: &str,
7521    ) -> Result<Option<TokenData>, Error> {
7522        match serde_json::from_str::<TokenData>(content) {
7523            Ok(token_data) => self.handle_parsed_token(token_file, token_data).await,
7524            Err(e) => self.handle_parse_error(token_file, e).await,
7525        }
7526    }
7527
7528    /// Handles successfully parsed token data, checking expiration
7529    async fn handle_parsed_token(
7530        &self,
7531        token_file: &str,
7532        token_data: TokenData,
7533    ) -> Result<Option<TokenData>, Error> {
7534        if token_data.is_expired() {
7535            tracing::info!("Token loaded from disk is expired, removing file");
7536            let _ = tokio::fs::remove_file(token_file).await;
7537            Ok(None)
7538        } else {
7539            tracing::info!("Valid token loaded from disk");
7540            Ok(Some(token_data))
7541        }
7542    }
7543
7544    /// Handles token parsing errors by cleaning up the invalid file
7545    async fn handle_parse_error(
7546        &self,
7547        token_file: &str,
7548        e: serde_json::Error,
7549    ) -> Result<Option<TokenData>, Error> {
7550        tracing::warn!("Failed to parse token file, removing: {e}");
7551        let _ = tokio::fs::remove_file(token_file).await;
7552        Err(Error::Token(TokenError::serialization(format!(
7553            "Failed to parse token file: {e}"
7554        ))))
7555    }
7556
7557    /// Saves token data to disk
7558    async fn save_token_to_disk(&self, token_data: &TokenData) -> Result<(), Error> {
7559        use tokio::fs;
7560
7561        let token_file = "token.json";
7562
7563        match serde_json::to_string_pretty(token_data) {
7564            Ok(json) => match fs::write(token_file, json).await {
7565                Ok(()) => {
7566                    tracing::debug!("Token saved to disk: {}", token_file);
7567                    Ok(())
7568                }
7569                Err(e) => {
7570                    tracing::error!("Failed to write token file: {e}");
7571                    Err(Error::Token(TokenError::storage(format!(
7572                        "Failed to write token file: {e}"
7573                    ))))
7574                }
7575            },
7576            Err(e) => {
7577                tracing::error!("Failed to serialize token data: {e}");
7578                Err(Error::Token(TokenError::serialization(format!(
7579                    "Failed to serialize token data: {e}"
7580                ))))
7581            }
7582        }
7583    }
7584
7585    /// Removes the token file from disk
7586    async fn remove_token_from_disk(&self) -> Result<(), Error> {
7587        use tokio::fs;
7588
7589        let token_file = "token.json";
7590
7591        match fs::remove_file(token_file).await {
7592            Ok(()) => {
7593                tracing::debug!("Token file removed from disk: {}", token_file);
7594                Ok(())
7595            }
7596            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
7597                tracing::debug!("Token file does not exist, nothing to remove");
7598                Ok(())
7599            }
7600            Err(e) => {
7601                tracing::warn!("Failed to remove token file: {e}");
7602                Err(Error::Token(TokenError::storage(format!(
7603                    "Failed to remove token file: {e}"
7604                ))))
7605            }
7606        }
7607    }
7608
7609    /// Forces cleanup of token persistence files (useful for testing)
7610    /// This method removes token files regardless of persistence settings
7611    ///
7612    /// # Errors
7613    /// Returns an error if:
7614    /// - File system permissions prevent deletion of the token file
7615    /// - I/O errors occur during file deletion operations
7616    /// - The token file is locked by another process
7617    pub async fn force_cleanup_token_files() -> Result<(), Error> {
7618        use tokio::fs;
7619
7620        let token_file = "token.json";
7621
7622        match fs::remove_file(token_file).await {
7623            Ok(()) => {
7624                tracing::debug!("Token file forcefully removed: {}", token_file);
7625                Ok(())
7626            }
7627            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
7628                tracing::debug!("No token file to clean up");
7629                Ok(())
7630            }
7631            Err(e) => {
7632                tracing::warn!("Failed to force cleanup token file: {e}");
7633                Err(Error::Token(TokenError::storage(format!(
7634                    "Failed to force cleanup token file: {e}"
7635                ))))
7636            }
7637        }
7638    }
7639
7640    /// Resets the global `TokenManager` singleton (useful for testing)
7641    ///
7642    /// This method clears the global singleton instance, forcing the next
7643    /// call to `get_global_instance()` to create a fresh `TokenManager`.
7644    /// Primarily intended for test scenarios where a clean state is needed.
7645    ///
7646    /// # Errors
7647    /// Returns an error if:
7648    /// - Token clearing operations fail during the reset process
7649    /// - File system errors occur when clearing persistent token data
7650    /// - The global instance is in an invalid state that prevents cleanup
7651    pub async fn reset_global_instance() -> Result<(), Error> {
7652        // Clear any existing token from the current global instance
7653        if let Some(manager) = GLOBAL_TOKEN_MANAGER.get() {
7654            let _ = manager.clear_token().await;
7655        }
7656
7657        // Reset the OnceCell to allow a new instance to be created
7658        // Note: OnceCell doesn't have a reset method, so we can't actually reset it
7659        // The best we can do is clear the token from the existing instance
7660        tracing::debug!("Global TokenManager instance token cleared for testing");
7661        Ok(())
7662    }
7663}
7664
7665#[derive(Debug)]
7666pub struct ApiClient {
7667    client: Client,
7668    base_url: Url,
7669    token_strategy: Box<dyn TokenStrategy>,
7670}
7671
7672#[allow(clippy::missing_errors_doc, clippy::missing_panics_doc)]
7673impl ApiClient {
7674    /// Creates a new API client with the base URL from environment variables.
7675    ///
7676    /// Automatically selects the appropriate token strategy based on environment detection:
7677    /// - Mock strategy for mock environments (no persistence, isolated tokens)
7678    /// - Live strategy for live environments (full token management with persistence)
7679    ///
7680    /// # Errors
7681    ///
7682    /// Returns an error if:
7683    /// - The `AMP_API_BASE_URL` environment variable contains an invalid URL
7684    /// - Token strategy initialization fails
7685    ///
7686    /// # Examples
7687    /// ```no_run
7688    /// # use amp_rs::ApiClient;
7689    /// # #[tokio::main]
7690    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
7691    /// // Create a new client - automatically detects environment
7692    /// let client = ApiClient::new().await?;
7693    ///
7694    /// // Client is ready to use
7695    /// let assets = client.get_assets().await?;
7696    /// println!("Found {} assets", assets.len());
7697    /// # Ok(())
7698    /// # }
7699    /// ```
7700    pub async fn new() -> Result<Self, Error> {
7701        let base_url = get_amp_api_base_url()?;
7702        let client = Client::new();
7703
7704        // Automatic strategy selection based on environment
7705        let token_strategy = TokenEnvironment::create_auto_strategy(None).await?;
7706
7707        tracing::info!(
7708            "Created ApiClient with {} strategy for base URL: {}",
7709            token_strategy.strategy_type(),
7710            base_url
7711        );
7712
7713        Ok(Self {
7714            client,
7715            base_url,
7716            token_strategy,
7717        })
7718    }
7719
7720    /// Creates a new API client with the specified base URL.
7721    ///
7722    /// Automatically selects the appropriate token strategy based on environment detection.
7723    ///
7724    /// # Errors
7725    ///
7726    /// Returns an error if token strategy initialization fails.
7727    ///
7728    /// # Examples
7729    /// ```no_run
7730    /// # use amp_rs::ApiClient;
7731    /// # use reqwest::Url;
7732    /// # #[tokio::main]
7733    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
7734    /// let base_url = Url::parse("https://amp-test.blockstream.com/api")?;
7735    /// let client = ApiClient::with_base_url(base_url).await?;
7736    ///
7737    /// // Client is ready to use with the specified URL
7738    /// let assets = client.get_assets().await?;
7739    /// # Ok(())
7740    /// # }
7741    /// ```
7742    pub async fn with_base_url(base_url: Url) -> Result<Self, Error> {
7743        let client = Client::new();
7744
7745        // Automatic strategy selection based on environment
7746        let token_strategy = TokenEnvironment::create_auto_strategy(None).await?;
7747
7748        tracing::info!(
7749            "Created ApiClient with {} strategy for base URL: {}",
7750            token_strategy.strategy_type(),
7751            base_url
7752        );
7753
7754        Ok(Self {
7755            client,
7756            base_url,
7757            token_strategy,
7758        })
7759    }
7760
7761    /// Creates a new API client with a custom token strategy (useful for testing).
7762    ///
7763    /// # Errors
7764    ///
7765    /// Returns an error if the base URL cannot be obtained from environment variables.
7766    pub fn with_token_strategy(token_strategy: Box<dyn TokenStrategy>) -> Result<Self, Error> {
7767        let base_url = get_amp_api_base_url()?;
7768
7769        tracing::info!(
7770            "Created ApiClient with explicit {} strategy for base URL: {}",
7771            token_strategy.strategy_type(),
7772            base_url
7773        );
7774
7775        Ok(Self {
7776            client: Client::new(),
7777            base_url,
7778            token_strategy,
7779        })
7780    }
7781
7782    /// Creates a new API client with a custom token manager (useful for testing).
7783    ///
7784    /// # Errors
7785    ///
7786    /// Returns an error if the base URL cannot be obtained from environment variables.
7787    pub fn with_token_manager(token_manager: Arc<TokenManager>) -> Result<Self, Error> {
7788        let base_url = get_amp_api_base_url()?;
7789        let token_strategy: Box<dyn TokenStrategy> =
7790            Box::new(LiveTokenStrategy::with_token_manager(token_manager));
7791
7792        tracing::info!(
7793            "Created ApiClient with custom token manager for base URL: {}",
7794            base_url
7795        );
7796
7797        Ok(Self {
7798            client: Client::new(),
7799            base_url,
7800            token_strategy,
7801        })
7802    }
7803
7804    /// Creates a new API client for testing with a mock token strategy that always returns a fixed token.
7805    /// This bypasses all token acquisition and management logic and uses complete isolation.
7806    ///
7807    /// # Errors
7808    ///
7809    /// This method is infallible but returns Result for API consistency.
7810    ///
7811    /// # Examples
7812    /// ```
7813    /// # use amp_rs::ApiClient;
7814    /// # use reqwest::Url;
7815    /// # #[tokio::main]
7816    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
7817    /// let base_url = Url::parse("http://localhost:8080/api")?;
7818    /// let client = ApiClient::with_mock_token(base_url, "test_token".to_string())?;
7819    ///
7820    /// // Client will always use "test_token" for authentication
7821    /// let token = client.get_token().await?;
7822    /// assert_eq!(token, "test_token");
7823    /// # Ok(())
7824    /// # }
7825    /// ```
7826    pub fn with_mock_token(base_url: Url, mock_token: String) -> Result<Self, Error> {
7827        let client = Client::new();
7828        let token_strategy: Box<dyn TokenStrategy> = Box::new(MockTokenStrategy::new(mock_token));
7829
7830        tracing::info!(
7831            "Created ApiClient with explicit mock token strategy for base URL: {}",
7832            base_url
7833        );
7834
7835        Ok(Self {
7836            client,
7837            base_url,
7838            token_strategy,
7839        })
7840    }
7841
7842    /// Obtains a new authentication token from the AMP API.
7843    ///
7844    /// **Note**: This method is deprecated in favor of the automatic token management
7845    /// provided by `get_token()`. The `TokenManager` handles token acquisition internally
7846    /// with enhanced retry logic and error handling.
7847    ///
7848    /// # Errors
7849    ///
7850    /// Returns an error if:
7851    /// - The `AMP_USERNAME` or `AMP_PASSWORD` environment variables are not set
7852    /// - The HTTP request fails
7853    /// - The token request is rejected by the server
7854    /// - The response cannot be parsed
7855    #[deprecated(note = "Use get_token() instead - it provides automatic token management")]
7856    pub async fn obtain_amp_token(&self) -> Result<String, Error> {
7857        // Delegate to get_token for backward compatibility
7858        self.get_token().await
7859    }
7860
7861    /// Gets current token information for debugging and monitoring.
7862    ///
7863    /// Returns detailed information about the current token including:
7864    /// - Expiry time and remaining duration
7865    /// - Token age since acquisition
7866    /// - Expiry status flags
7867    ///
7868    /// Note: Mock strategies may return limited or no token information.
7869    ///
7870    /// # Returns
7871    /// `Some(TokenInfo)` if a token exists, `None` if no token is stored or strategy doesn't support info
7872    ///
7873    /// # Errors
7874    /// Returns an error if token information retrieval fails
7875    ///
7876    /// # Examples
7877    /// ```no_run
7878    /// # use amp_rs::ApiClient;
7879    /// # #[tokio::main]
7880    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
7881    /// let client = ApiClient::new().await?;
7882    ///
7883    /// if let Some(token_info) = client.get_token_info().await? {
7884    ///     println!("Token expires at: {}", token_info.expires_at);
7885    ///     println!("Token is expired: {}", token_info.is_expired);
7886    /// } else {
7887    ///     println!("No token stored or mock strategy in use");
7888    /// }
7889    /// # Ok(())
7890    /// # }
7891    /// ```
7892    pub async fn get_token_info(&self) -> Result<Option<TokenInfo>, Error> {
7893        // Only live strategies support detailed token information
7894        if let Some(live_strategy) = self
7895            .token_strategy
7896            .as_any()
7897            .downcast_ref::<LiveTokenStrategy>()
7898        {
7899            live_strategy.get_token_info().await
7900        } else {
7901            // Mock strategies don't provide detailed token information
7902            tracing::debug!(
7903                "Token info not available for {} strategy",
7904                self.token_strategy.strategy_type()
7905            );
7906            Ok(None)
7907        }
7908    }
7909
7910    /// Clears the stored token (useful for testing scenarios).
7911    ///
7912    /// This method removes the current token from storage, forcing the next
7913    /// `get_token()` call to obtain a fresh token.
7914    ///
7915    /// # Errors
7916    /// Returns an error if token clearing fails
7917    ///
7918    /// # Examples
7919    /// ```no_run
7920    /// # use amp_rs::ApiClient;
7921    /// # #[tokio::main]
7922    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
7923    /// let client = ApiClient::new().await?;
7924    ///
7925    /// // Clear any existing token
7926    /// client.clear_token().await?;
7927    ///
7928    /// // Next get_token() call will obtain a fresh token
7929    /// let token = client.get_token().await?;
7930    /// # Ok(())
7931    /// # }
7932    /// ```
7933    pub async fn clear_token(&self) -> Result<(), Error> {
7934        self.token_strategy.clear_token().await
7935    }
7936
7937    /// Forces a token refresh regardless of current token status.
7938    ///
7939    /// This method bypasses the normal proactive refresh logic and immediately
7940    /// attempts to refresh the current token. If no token exists or refresh fails,
7941    /// it falls back to obtaining a new token.
7942    ///
7943    /// # Errors
7944    /// Returns an error if both refresh and obtain operations fail
7945    pub async fn force_refresh(&self) -> Result<String, Error> {
7946        // Clear current token and get a fresh one
7947        self.token_strategy.clear_token().await?;
7948        self.token_strategy.get_token().await
7949    }
7950
7951    /// Resets the global `TokenManager` singleton (useful for testing).
7952    ///
7953    /// This method clears the token from the global `TokenManager` instance.
7954    /// Primarily intended for test scenarios where a clean token state is needed.
7955    ///
7956    /// # Errors
7957    /// Returns an error if the reset operation fails
7958    pub async fn reset_global_token_manager() -> Result<(), Error> {
7959        TokenManager::reset_global_instance().await
7960    }
7961
7962    /// Gets a valid authentication token with automatic token management.
7963    ///
7964    /// This method uses the integrated `TokenManager` to handle:
7965    /// - Proactive token refresh (5 minutes before expiry)
7966    /// - Automatic fallback from refresh to obtain on failure
7967    /// - Retry logic with exponential backoff
7968    /// - Thread-safe token storage
7969    ///
7970    /// # Errors
7971    ///
7972    /// Returns an error if token acquisition or refresh fails after all retries.
7973    ///
7974    /// # Examples
7975    /// ```no_run
7976    /// # use amp_rs::ApiClient;
7977    /// # #[tokio::main]
7978    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
7979    /// let client = ApiClient::new().await?;
7980    ///
7981    /// // Get a valid token - automatically handles refresh if needed
7982    /// let token = client.get_token().await?;
7983    /// println!("Got token: {}", &token[..10]); // Print first 10 chars
7984    /// # Ok(())
7985    /// # }
7986    /// ```
7987    pub async fn get_token(&self) -> Result<String, Error> {
7988        self.token_strategy.get_token().await
7989    }
7990
7991    /// Returns the type of token strategy currently in use
7992    ///
7993    /// This is useful for debugging and testing to verify the correct strategy is selected.
7994    ///
7995    /// # Returns
7996    /// A string indicating the strategy type: "mock" or "live"
7997    #[must_use]
7998    pub fn get_strategy_type(&self) -> &'static str {
7999        self.token_strategy.strategy_type()
8000    }
8001
8002    /// Returns whether the current strategy persists tokens
8003    ///
8004    /// This is useful for understanding the token management behavior.
8005    ///
8006    /// # Returns
8007    /// `true` if tokens are persisted to disk, `false` for in-memory only
8008    #[must_use]
8009    pub fn should_persist_tokens(&self) -> bool {
8010        self.token_strategy.should_persist()
8011    }
8012
8013    /// Force cleanup of token files (for test cleanup)
8014    ///
8015    /// This is a static method that can be used to cleanup token files
8016    /// without needing an `ApiClient` instance. Useful for test teardown.
8017    ///
8018    /// # Errors
8019    /// Returns an error if token file cleanup fails
8020    pub async fn force_cleanup_token_files() -> Result<(), Error> {
8021        // Only cleanup if we're not in a live test environment
8022        let environment = TokenEnvironment::detect();
8023        if !environment.is_live() || environment.is_mock() {
8024            TokenManager::force_cleanup_token_files().await?;
8025            tracing::debug!("Token files cleaned up for non-live environment");
8026        } else {
8027            tracing::debug!("Skipping token file cleanup in live environment");
8028        }
8029        Ok(())
8030    }
8031
8032    async fn request_raw(
8033        &self,
8034        method: Method,
8035        path: &[&str],
8036        body: Option<impl serde::Serialize>,
8037    ) -> Result<reqwest::Response, Error> {
8038        let debug_logging = std::env::var("AMP_DEBUG").is_ok();
8039
8040        if debug_logging {
8041            eprintln!("🌐 HTTP Request: {} /{}", method, path.join("/"));
8042        }
8043
8044        let token = self.get_token().await?;
8045        let mut url = self.base_url.clone();
8046        url.path_segments_mut().unwrap().extend(path);
8047
8048        if debug_logging {
8049            eprintln!("🔗 Full URL: {url}");
8050        }
8051
8052        // Retry logic for network issues
8053        let max_retries = 3;
8054        let mut last_error = None;
8055
8056        for attempt in 1..=max_retries {
8057            if debug_logging && attempt > 1 {
8058                eprintln!("🔄 Retry attempt {attempt} of {max_retries}");
8059            }
8060
8061            let mut request_builder = self
8062                .client
8063                .request(method.clone(), url.clone())
8064                .header(AUTHORIZATION, format!("token {token}"))
8065                .timeout(std::time::Duration::from_secs(60)); // Increase timeout to 60 seconds
8066
8067            if let Some(ref body) = body {
8068                if debug_logging && attempt == 1 {
8069                    if let Ok(json_body) = serde_json::to_string_pretty(&body) {
8070                        eprintln!(
8071                            "📤 Request body ({} bytes):\n{}",
8072                            json_body.len(),
8073                            json_body
8074                        );
8075                    } else {
8076                        eprintln!("📤 Request body: [serialization failed]");
8077                    }
8078                }
8079                request_builder = request_builder.json(&body);
8080            } else if debug_logging && attempt == 1 {
8081                eprintln!("📤 Request body: [empty]");
8082            }
8083
8084            if debug_logging {
8085                eprintln!("🚀 Sending HTTP request (attempt {attempt})...");
8086            }
8087
8088            match request_builder.send().await {
8089                Ok(response) => {
8090                    let status = response.status();
8091
8092                    if debug_logging {
8093                        eprintln!("📥 Response status: {status}");
8094                    }
8095
8096                    if !status.is_success() {
8097                        let error_text = response
8098                            .text()
8099                            .await
8100                            .unwrap_or_else(|_| "Unknown error".to_string());
8101
8102                        if debug_logging {
8103                            eprintln!("❌ Error response body: {error_text}");
8104                        }
8105
8106                        return Err(Error::RequestFailed(format!(
8107                            "Request to {path:?} failed with status {status}: {error_text}"
8108                        )));
8109                    }
8110
8111                    if debug_logging {
8112                        eprintln!("✅ HTTP request successful");
8113                    }
8114
8115                    return Ok(response);
8116                }
8117                Err(e) => {
8118                    if debug_logging {
8119                        eprintln!("❌ HTTP request failed (attempt {attempt}): {e:?}");
8120                        eprintln!("   Error kind: {:?}", e.is_timeout());
8121                        eprintln!("   Is connect error: {}", e.is_connect());
8122                        eprintln!("   Is request error: {}", e.is_request());
8123                    }
8124
8125                    last_error = Some(e);
8126
8127                    // Only retry on network/connection errors, not on client errors
8128                    if attempt < max_retries {
8129                        #[allow(clippy::cast_sign_loss)] // attempt is always positive (1-3)
8130                        let delay = std::time::Duration::from_millis((attempt as u64) * 1000);
8131                        if debug_logging {
8132                            eprintln!("⏳ Waiting {}ms before retry...", delay.as_millis());
8133                        }
8134                        tokio::time::sleep(delay).await;
8135                    }
8136                }
8137            }
8138        }
8139
8140        // If we get here, all retries failed
8141        if debug_logging {
8142            eprintln!("❌ All {max_retries} retry attempts failed");
8143        }
8144
8145        Err(Error::Reqwest(last_error.unwrap()))
8146    }
8147
8148    async fn request_json<T: DeserializeOwned>(
8149        &self,
8150        method: Method,
8151        path: &[&str],
8152        body: Option<impl serde::Serialize>,
8153    ) -> Result<T, Error> {
8154        let response = self.request_raw(method, path, body).await?;
8155        response
8156            .json()
8157            .await
8158            .map_err(|e| Error::ResponseParsingFailed(e.to_string()))
8159    }
8160
8161    async fn request_empty(
8162        &self,
8163        method: Method,
8164        path: &[&str],
8165        body: Option<impl serde::Serialize>,
8166    ) -> Result<(), Error> {
8167        self.request_raw(method, path, body).await?;
8168        Ok(())
8169    }
8170
8171    /// Gets the API changelog.
8172    ///
8173    /// # Errors
8174    ///
8175    /// Returns an error if:
8176    /// - Authentication fails
8177    /// - The HTTP request fails
8178    /// - The server returns an error status
8179    /// - The response cannot be parsed as JSON
8180    pub async fn get_changelog(&self) -> Result<serde_json::Value, Error> {
8181        self.request_json(Method::GET, &["changelog"], None::<&()>)
8182            .await
8183    }
8184
8185    /// Changes the user's password.
8186    ///
8187    /// # Errors
8188    ///
8189    /// Returns an error if:
8190    /// - Authentication fails
8191    /// - The HTTP request fails
8192    /// - The server rejects the password change
8193    /// - The response cannot be parsed
8194    pub async fn user_change_password(
8195        &self,
8196        password: Secret<String>,
8197    ) -> Result<ChangePasswordResponse, Error> {
8198        let request = ChangePasswordRequest {
8199            password: Secret::new(Password(password.expose_secret().clone())),
8200        };
8201        self.request_json(Method::POST, &["user", "change_password"], Some(request))
8202            .await
8203    }
8204
8205    /// Gets a list of all assets.
8206    ///
8207    /// # Errors
8208    ///
8209    /// Returns an error if:
8210    /// - Authentication fails
8211    /// - The HTTP request fails
8212    /// - The server returns an error status
8213    /// - The response cannot be parsed
8214    ///
8215    /// # Examples
8216    /// ```no_run
8217    /// # use amp_rs::ApiClient;
8218    /// # #[tokio::main]
8219    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
8220    /// let client = ApiClient::new().await?;
8221    ///
8222    /// let assets = client.get_assets().await?;
8223    /// for asset in assets {
8224    ///     println!("Asset: {} ({})", asset.name, asset.ticker.unwrap_or_default());
8225    /// }
8226    /// # Ok(())
8227    /// # }
8228    /// ```
8229    pub async fn get_assets(&self) -> Result<Vec<Asset>, Error> {
8230        self.request_json(Method::GET, &["assets"], None::<&()>)
8231            .await
8232    }
8233
8234    /// Gets a specific asset by UUID.
8235    ///
8236    /// # Errors
8237    ///
8238    /// Returns an error if:
8239    /// - Authentication fails
8240    /// - The HTTP request fails
8241    /// - The asset does not exist
8242    /// - The response cannot be parsed
8243    ///
8244    /// # Examples
8245    /// ```no_run
8246    /// # use amp_rs::ApiClient;
8247    /// # #[tokio::main]
8248    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
8249    /// let client = ApiClient::new().await?;
8250    ///
8251    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
8252    /// let asset = client.get_asset(asset_uuid).await?;
8253    ///
8254    /// println!("Asset: {} ({})", asset.name, asset.ticker.unwrap_or_default());
8255    /// println!("Registered: {}, Locked: {}", asset.is_registered, asset.is_locked);
8256    /// # Ok(())
8257    /// # }
8258    /// ```
8259    pub async fn get_asset(&self, asset_uuid: &str) -> Result<Asset, Error> {
8260        self.request_json(Method::GET, &["assets", asset_uuid], None::<&()>)
8261            .await
8262    }
8263
8264    /// Issues a new asset.
8265    ///
8266    /// # Errors
8267    ///
8268    /// Returns an error if:
8269    /// - Authentication fails
8270    /// - The HTTP request fails
8271    /// - The issuance request is invalid
8272    /// - The response cannot be parsed
8273    ///
8274    /// # Examples
8275    /// ```no_run
8276    /// # use amp_rs::{ApiClient, model::IssuanceRequest};
8277    /// # #[tokio::main]
8278    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
8279    /// let client = ApiClient::new().await?;
8280    ///
8281    /// let issuance_request = IssuanceRequest {
8282    ///     name: "My Token".to_string(),
8283    ///     amount: 1000000,
8284    ///     destination_address: "vjU2i2EM2viGEzSywpStMPkTX9U9QSDsLSN63kJJYVpxKJZuxaph8v5r5Jf11aqnfBVdjSbrvcJ2pw26".to_string(),
8285    ///     domain: "example.com".to_string(),
8286    ///     ticker: "MYTKN".to_string(),
8287    ///     pubkey: "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798".to_string(),
8288    ///     precision: Some(8),
8289    ///     is_confidential: Some(true),
8290    ///     is_reissuable: Some(false),
8291    ///     reissuance_amount: None,
8292    ///     reissuance_address: None,
8293    ///     transfer_restricted: Some(false),
8294    /// };
8295    ///
8296    /// let response = client.issue_asset(&issuance_request).await?;
8297    /// println!("Issued asset with UUID: {}", response.asset_uuid);
8298    /// # Ok(())
8299    /// # }
8300    /// ```
8301    pub async fn issue_asset(
8302        &self,
8303        issuance_request: &IssuanceRequest,
8304    ) -> Result<IssuanceResponse, Error> {
8305        self.request_json(Method::POST, &["assets", "issue"], Some(issuance_request))
8306            .await
8307    }
8308
8309    /// Edits an existing asset.
8310    ///
8311    /// # Errors
8312    ///
8313    /// Returns an error if:
8314    /// - Authentication fails
8315    /// - The HTTP request fails
8316    /// - The asset does not exist
8317    /// - The edit request is invalid
8318    /// - The response cannot be parsed
8319    pub async fn edit_asset(
8320        &self,
8321        asset_uuid: &str,
8322        edit_asset_request: &EditAssetRequest,
8323    ) -> Result<Asset, Error> {
8324        self.request_json(
8325            Method::PUT,
8326            &["assets", asset_uuid, "edit"],
8327            Some(edit_asset_request),
8328        )
8329        .await
8330    }
8331
8332    /// Registers an asset with the Blockstream Asset Registry.
8333    ///
8334    /// This method publishes an asset to the public registry, making it discoverable
8335    /// and verifiable by other users and applications. The asset must already exist
8336    /// in the AMP system before it can be registered.
8337    ///
8338    /// # Arguments
8339    ///
8340    /// * `asset_uuid` - The unique identifier of the asset to register
8341    ///
8342    /// # Returns
8343    ///
8344    /// Returns a `RegisterAssetResponse` containing:
8345    /// - `success`: Boolean indicating whether the registration was successful
8346    /// - `message`: Optional status message from the API
8347    /// - `asset_id`: The registered asset identifier (hex string)
8348    ///
8349    /// # Errors
8350    ///
8351    /// Returns an error if:
8352    /// - The asset does not exist or cannot be found (404)
8353    /// - Authentication fails or token is invalid (401)
8354    /// - The asset is already registered (returns success with appropriate message)
8355    /// - Network connectivity issues occur
8356    /// - The server returns an error status (5xx)
8357    /// - The response cannot be parsed
8358    ///
8359    /// # Examples
8360    ///
8361    /// ```no_run
8362    /// # use amp_rs::ApiClient;
8363    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
8364    /// let client = ApiClient::from_env().await?;
8365    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
8366    ///
8367    /// let response = client.register_asset(asset_uuid).await?;
8368    /// if response.success {
8369    ///     println!("Asset registered successfully!");
8370    ///     println!("Asset ID: {}", response.asset_id);
8371    ///     if let Some(message) = response.message {
8372    ///         println!("Message: {}", message);
8373    ///     }
8374    /// }
8375    /// # Ok(())
8376    /// # }
8377    /// ```
8378    pub async fn register_asset(&self, asset_uuid: &str) -> Result<RegisterAssetResponse, Error> {
8379        // Make HTTP request directly to handle both success and error responses
8380        let token = self.get_token().await?;
8381        let mut url = self.base_url.clone();
8382        url.path_segments_mut()
8383            .unwrap()
8384            .extend(&["assets", asset_uuid, "register"]);
8385
8386        let response = self
8387            .client
8388            .request(Method::GET, url)
8389            .header(AUTHORIZATION, format!("token {token}"))
8390            .timeout(std::time::Duration::from_secs(60))
8391            .send()
8392            .await
8393            .map_err(|e| Error::RequestFailed(format!("HTTP request failed: {e}")))?;
8394
8395        let status = response.status();
8396        let response_text = response.text().await.map_err(|e| {
8397            Error::ResponseParsingFailed(format!("Failed to read response body: {e}"))
8398        })?;
8399
8400        // Handle HTTP 200 - success case
8401        if status == reqwest::StatusCode::OK {
8402            // Try to parse as Asset (full registration response)
8403            if let Ok(asset) = serde_json::from_str::<Asset>(&response_text) {
8404                return Ok(RegisterAssetResponse {
8405                    success: true,
8406                    message: Some("Asset registered successfully".to_string()),
8407                    asset_data: Some(asset),
8408                });
8409            }
8410
8411            // If parsing as Asset fails, return success with raw message
8412            return Ok(RegisterAssetResponse {
8413                success: true,
8414                message: Some(response_text),
8415                asset_data: None,
8416            });
8417        }
8418
8419        // Handle error responses
8420        // Try to parse error response as JSON
8421        if let Ok(error_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
8422            // Check for "already registered" error
8423            if let Some(error_msg) = error_json.get("Error").and_then(|e| e.as_str()) {
8424                let error_msg_lower = error_msg.to_lowercase();
8425                if error_msg_lower.contains("already registered") {
8426                    return Ok(RegisterAssetResponse {
8427                        success: true,
8428                        message: Some("Asset is already registered".to_string()),
8429                        asset_data: None,
8430                    });
8431                }
8432
8433                // Other errors - return as error
8434                return Err(Error::RequestFailed(format!(
8435                    "Request to [\"assets\", \"{asset_uuid}\", \"register\"] failed with status {status}: {error_msg}"
8436                )));
8437            }
8438        }
8439
8440        // Fallback error for non-JSON or unexpected responses
8441        Err(Error::RequestFailed(format!(
8442            "Request to [\"assets\", \"{asset_uuid}\", \"register\"] failed with status {status}: {response_text}"
8443        )))
8444    }
8445
8446    /// # Errors
8447    /// Returns an error if:
8448    /// - The asset does not exist or cannot be found
8449    /// - Authentication fails or token is invalid
8450    /// - Network connectivity issues occur
8451    /// - The server returns an error status
8452    pub async fn delete_asset(&self, asset_uuid: &str) -> Result<(), Error> {
8453        self.request_empty(
8454            Method::DELETE,
8455            &["assets", asset_uuid, "delete"],
8456            None::<&()>,
8457        )
8458        .await
8459    }
8460
8461    /// # Errors
8462    /// Returns an error if:
8463    /// - The transaction ID is invalid or not found
8464    /// - Authentication fails or token is invalid
8465    /// - Network connectivity issues occur
8466    /// - The server returns an error status
8467    /// - The response cannot be parsed
8468    pub async fn get_broadcast_status(&self, txid: &str) -> Result<BroadcastResponse, Error> {
8469        self.request_json(Method::GET, &["tx", "broadcast", txid], None::<&()>)
8470            .await
8471    }
8472
8473    /// # Errors
8474    /// Returns an error if:
8475    /// - The transaction hex is invalid or malformed
8476    /// - The transaction is rejected by the network
8477    /// - Authentication fails or token is invalid
8478    /// - Network connectivity issues occur
8479    /// - The server returns an error status
8480    /// - The response cannot be parsed
8481    pub async fn broadcast_transaction(&self, tx_hex: &str) -> Result<BroadcastResponse, Error> {
8482        self.request_json(Method::POST, &["tx", "broadcast"], Some(tx_hex))
8483            .await
8484    }
8485
8486    /// # Errors
8487    /// Returns an error if:
8488    /// - The asset UUID is invalid or not found
8489    /// - The user lacks authorization to register the asset
8490    /// - The asset is already registered
8491    /// - Authentication fails or token is invalid
8492    /// - Network connectivity issues occur
8493    /// - The server returns an error status
8494    /// - The response cannot be parsed
8495    pub async fn register_asset_authorized(&self, asset_uuid: &str) -> Result<Asset, Error> {
8496        self.request_json(
8497            Method::GET,
8498            &["assets", asset_uuid, "register-authorized"],
8499            None::<&()>,
8500        )
8501        .await
8502    }
8503
8504    /// # Errors
8505    /// Returns an error if:
8506    /// - The asset UUID is invalid or not found
8507    /// - The asset is already locked
8508    /// - The user lacks permission to lock the asset
8509    /// - Authentication fails or token is invalid
8510    /// - Network connectivity issues occur
8511    /// - The server returns an error status
8512    /// - The response cannot be parsed
8513    pub async fn lock_asset(&self, asset_uuid: &str) -> Result<Asset, Error> {
8514        self.request_json(Method::PUT, &["assets", asset_uuid, "lock"], None::<&()>)
8515            .await
8516    }
8517
8518    /// # Errors
8519    /// Returns an error if:
8520    /// - The asset UUID is invalid or not found
8521    /// - The asset is not currently locked
8522    /// - The user lacks permission to unlock the asset
8523    /// - Authentication fails or token is invalid
8524    /// - Network connectivity issues occur
8525    /// - The server returns an error status
8526    /// - The response cannot be parsed
8527    pub async fn unlock_asset(&self, asset_uuid: &str) -> Result<Asset, Error> {
8528        self.request_json(Method::PUT, &["assets", asset_uuid, "unlock"], None::<&()>)
8529            .await
8530    }
8531
8532    /// # Errors
8533    /// Returns an error if:
8534    /// - The asset UUID is invalid or not found
8535    /// - The activity parameters are invalid
8536    /// - Authentication fails or token is invalid
8537    /// - Network connectivity issues occur
8538    /// - The server returns an error status
8539    /// - The response cannot be parsed
8540    pub async fn get_asset_activities(
8541        &self,
8542        asset_uuid: &str,
8543        params: &AssetActivityParams,
8544    ) -> Result<Vec<Activity>, Error> {
8545        self.request_json(
8546            Method::GET,
8547            &["assets", asset_uuid, "activities"],
8548            Some(params),
8549        )
8550        .await
8551    }
8552
8553    /// # Errors
8554    /// Returns an error if:
8555    /// - The asset UUID is invalid or not found
8556    /// - The specified height is invalid or out of range
8557    /// - Authentication fails or token is invalid
8558    /// - Network connectivity issues occur
8559    /// - The server returns an error status
8560    /// - The response cannot be parsed
8561    pub async fn get_asset_ownerships(
8562        &self,
8563        asset_uuid: &str,
8564        height: Option<i64>,
8565    ) -> Result<Vec<Ownership>, Error> {
8566        let mut path = vec!["assets", asset_uuid, "ownerships"];
8567        let height_str;
8568        if let Some(h) = height {
8569            height_str = h.to_string();
8570            path.push(&height_str);
8571        }
8572        self.request_json(Method::GET, &path, None::<&()>).await
8573    }
8574
8575    /// # Errors
8576    /// Returns an error if:
8577    /// - The asset UUID is invalid or not found
8578    /// - Authentication fails or token is invalid
8579    /// - Network connectivity issues occur
8580    /// - The server returns an error status
8581    /// - The response cannot be parsed
8582    pub async fn get_asset_balance(&self, asset_uuid: &str) -> Result<Balance, Error> {
8583        self.request_json(Method::GET, &["assets", asset_uuid, "balance"], None::<&()>)
8584            .await
8585    }
8586
8587    /// # Errors
8588    /// Returns an error if:
8589    /// - The asset UUID is invalid or not found
8590    /// - Authentication fails or token is invalid
8591    /// - Network connectivity issues occur
8592    /// - The server returns an error status
8593    /// - The response cannot be parsed
8594    pub async fn get_asset_summary(&self, asset_uuid: &str) -> Result<AssetSummary, Error> {
8595        self.request_json(Method::GET, &["assets", asset_uuid, "summary"], None::<&()>)
8596            .await
8597    }
8598
8599    /// # Errors
8600    /// Returns an error if:
8601    /// - The asset UUID is invalid or not found
8602    /// - Authentication fails or token is invalid
8603    /// - Network connectivity issues occur
8604    /// - The server returns an error status
8605    /// - The response cannot be parsed
8606    pub async fn get_asset_utxos(&self, asset_uuid: &str) -> Result<Vec<Utxo>, Error> {
8607        self.request_json(Method::GET, &["assets", asset_uuid, "utxos"], None::<&()>)
8608            .await
8609    }
8610
8611    /// Gets the memo for a specific asset.
8612    ///
8613    /// # Arguments
8614    /// * `asset_uuid` - The UUID of the asset to retrieve the memo for
8615    ///
8616    /// # Returns
8617    /// The memo string associated with the asset
8618    ///
8619    /// # Errors
8620    /// Returns an error if:
8621    /// - Authentication fails
8622    /// - The HTTP request fails
8623    /// - The server returns an error status
8624    /// - The asset does not exist
8625    /// - The response cannot be parsed
8626    pub async fn get_asset_memo(&self, asset_uuid: &str) -> Result<String, Error> {
8627        self.request_json(Method::GET, &["assets", asset_uuid, "memo"], None::<&()>)
8628            .await
8629    }
8630
8631    /// Sets a memo for the specified asset.
8632    ///
8633    /// # Arguments
8634    /// * `asset_uuid` - The UUID of the asset to set the memo for
8635    /// * `memo` - The memo string to associate with the asset
8636    ///
8637    /// # Returns
8638    /// Returns `Ok(())` on success.
8639    ///
8640    /// # Errors
8641    /// Returns an error if:
8642    /// - Authentication fails
8643    /// - The HTTP request fails
8644    /// - The server returns an error status
8645    /// - The asset does not exist
8646    /// - The memo cannot be set due to validation errors
8647    ///
8648    /// # Example
8649    /// ```rust
8650    /// # use amp_rs::ApiClient;
8651    /// # async fn example(client: &ApiClient) -> Result<(), Box<dyn std::error::Error>> {
8652    /// client.set_asset_memo("asset-uuid-123", "This is a memo for the asset").await?;
8653    /// # Ok(())
8654    /// # }
8655    /// ```
8656    pub async fn set_asset_memo(&self, asset_uuid: &str, memo: &str) -> Result<(), Error> {
8657        let token = self.get_token().await?;
8658        let mut url = self.base_url.clone();
8659        url.path_segments_mut()
8660            .unwrap()
8661            .extend(&["assets", asset_uuid, "memo", "set"]);
8662
8663        let response = self
8664            .client
8665            .request(Method::POST, url)
8666            .header(AUTHORIZATION, format!("token {token}"))
8667            .header("content-type", "application/json")
8668            .body(format!("\"{}\"", memo.replace('"', "\\\"")))
8669            .send()
8670            .await?;
8671
8672        if !response.status().is_success() {
8673            let status = response.status();
8674            let error_text = response
8675                .text()
8676                .await
8677                .unwrap_or_else(|_| "Unknown error".to_string());
8678            return Err(Error::RequestFailed(format!(
8679                "Request to [\"assets\", \"{asset_uuid}\", \"memo\", \"set\"] failed with status {status}: {error_text}"
8680            )));
8681        }
8682
8683        Ok(())
8684    }
8685
8686    /// Blacklists specific UTXOs for an asset to prevent them from being used in transactions.
8687    ///
8688    /// This method adds the specified UTXOs to the asset's blacklist, preventing them from being
8689    /// used in future transactions. This is typically used for security purposes when UTXOs are
8690    /// suspected to be compromised or need to be temporarily disabled.
8691    ///
8692    /// # Arguments
8693    /// * `asset_uuid` - The UUID of the asset to blacklist UTXOs for
8694    /// * `utxos` - A slice of `Outpoint` structs representing the UTXOs to blacklist
8695    ///
8696    /// # Returns
8697    /// Returns a vector of `Utxo` structs representing the blacklisted UTXOs with their updated status.
8698    ///
8699    /// # Errors
8700    /// Returns an error if:
8701    /// - Authentication fails or insufficient permissions
8702    /// - The asset UUID is invalid or does not exist
8703    /// - One or more UTXOs are invalid or already blacklisted
8704    /// - The HTTP request fails
8705    /// - The server returns an error status
8706    /// - The response cannot be parsed
8707    ///
8708    /// # Examples
8709    /// ```no_run
8710    /// # use amp_rs::{ApiClient, model::Outpoint};
8711    /// # #[tokio::main]
8712    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
8713    /// let client = ApiClient::new().await?;
8714    ///
8715    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
8716    /// let utxos = vec![
8717    ///     Outpoint {
8718    ///         txid: "abc123...".to_string(),
8719    ///         vout: 0,
8720    ///     },
8721    ///     Outpoint {
8722    ///         txid: "def456...".to_string(),
8723    ///         vout: 1,
8724    ///     },
8725    /// ];
8726    ///
8727    /// let blacklisted_utxos = client.blacklist_asset_utxos(asset_uuid, &utxos).await?;
8728    /// println!("Blacklisted {} UTXOs", blacklisted_utxos.len());
8729    /// # Ok(())
8730    /// # }
8731    /// ```
8732    ///
8733    /// # Related Methods
8734    /// - [`whitelist_asset_utxos`](Self::whitelist_asset_utxos) - Remove UTXOs from blacklist
8735    /// - [`get_asset`](Self::get_asset) - Get asset information including UTXO status
8736    pub async fn blacklist_asset_utxos(
8737        &self,
8738        asset_uuid: &str,
8739        utxos: &[Outpoint],
8740    ) -> Result<Vec<Utxo>, Error> {
8741        self.request_json(
8742            Method::POST,
8743            &["assets", asset_uuid, "utxos", "blacklist"],
8744            Some(utxos),
8745        )
8746        .await
8747    }
8748
8749    /// Removes UTXOs from the asset's blacklist, allowing them to be used in transactions again.
8750    ///
8751    /// This method removes the specified UTXOs from the asset's blacklist, restoring their ability
8752    /// to be used in transactions. This is the reverse operation of blacklisting UTXOs.
8753    ///
8754    /// # Arguments
8755    /// * `asset_uuid` - The UUID of the asset to whitelist UTXOs for
8756    /// * `utxos` - A slice of `Outpoint` structs representing the UTXOs to remove from blacklist
8757    ///
8758    /// # Returns
8759    /// Returns a vector of `Utxo` structs representing the whitelisted UTXOs with their updated status.
8760    ///
8761    /// # Errors
8762    /// Returns an error if:
8763    /// - Authentication fails or insufficient permissions
8764    /// - The asset UUID is invalid or does not exist
8765    /// - One or more UTXOs are invalid or not currently blacklisted
8766    /// - The HTTP request fails
8767    /// - The server returns an error status
8768    /// - The response cannot be parsed
8769    ///
8770    /// # Examples
8771    /// ```no_run
8772    /// # use amp_rs::{ApiClient, model::Outpoint};
8773    /// # #[tokio::main]
8774    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
8775    /// let client = ApiClient::new().await?;
8776    ///
8777    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
8778    /// let utxos = vec![
8779    ///     Outpoint {
8780    ///         txid: "abc123...".to_string(),
8781    ///         vout: 0,
8782    ///     },
8783    /// ];
8784    ///
8785    /// let whitelisted_utxos = client.whitelist_asset_utxos(asset_uuid, &utxos).await?;
8786    /// println!("Whitelisted {} UTXOs", whitelisted_utxos.len());
8787    /// # Ok(())
8788    /// # }
8789    /// ```
8790    ///
8791    /// # Related Methods
8792    /// - [`blacklist_asset_utxos`](Self::blacklist_asset_utxos) - Add UTXOs to blacklist
8793    /// - [`get_asset`](Self::get_asset) - Get asset information including UTXO status
8794    pub async fn whitelist_asset_utxos(
8795        &self,
8796        asset_uuid: &str,
8797        utxos: &[Outpoint],
8798    ) -> Result<Vec<Utxo>, Error> {
8799        self.request_json(
8800            Method::POST,
8801            &["assets", asset_uuid, "utxos", "whitelist"],
8802            Some(utxos),
8803        )
8804        .await
8805    }
8806
8807    /// Gets the treasury addresses for a specific asset
8808    ///
8809    /// # Arguments
8810    /// * `asset_uuid` - The UUID of the asset to get treasury addresses for
8811    ///
8812    /// # Returns
8813    /// A vector of treasury addresses as strings
8814    ///
8815    /// # Errors
8816    /// Returns an error if:
8817    /// - The asset does not exist
8818    /// - The request fails
8819    /// - The response cannot be parsed
8820    pub async fn get_asset_treasury_addresses(
8821        &self,
8822        asset_uuid: &str,
8823    ) -> Result<Vec<String>, Error> {
8824        self.request_json(
8825            Method::GET,
8826            &["assets", asset_uuid, "treasury-addresses"],
8827            None::<&()>,
8828        )
8829        .await
8830    }
8831
8832    /// Adds treasury addresses to a specific asset
8833    ///
8834    /// # Arguments
8835    /// * `asset_uuid` - The UUID of the asset to add treasury addresses to
8836    /// * `addresses` - A slice of address strings to add as treasury addresses
8837    ///
8838    /// # Returns
8839    /// Returns `Ok(())` on success
8840    ///
8841    /// # Errors
8842    /// Returns an error if:
8843    /// - The asset does not exist
8844    /// - The addresses are invalid
8845    /// - The request fails
8846    /// - Insufficient permissions
8847    pub async fn add_asset_treasury_addresses(
8848        &self,
8849        asset_uuid: &str,
8850        addresses: &[String],
8851    ) -> Result<(), Error> {
8852        self.request_empty(
8853            Method::POST,
8854            &["assets", asset_uuid, "treasury-addresses", "add"],
8855            Some(addresses),
8856        )
8857        .await
8858    }
8859
8860    /// Removes treasury addresses from a specific asset.
8861    ///
8862    /// This method removes the specified addresses from the asset's treasury address list.
8863    /// Treasury addresses are special addresses that can be used for asset management operations
8864    /// such as reissuance and burning.
8865    ///
8866    /// # Arguments
8867    /// * `asset_uuid` - The UUID of the asset to remove treasury addresses from
8868    /// * `addresses` - A slice of address strings to remove from the treasury addresses
8869    ///
8870    /// # Returns
8871    /// Returns `Ok(())` on successful removal.
8872    ///
8873    /// # Errors
8874    /// Returns an error if:
8875    /// - Authentication fails or insufficient permissions
8876    /// - The asset UUID is invalid or does not exist
8877    /// - One or more addresses are invalid or not currently treasury addresses
8878    /// - The HTTP request fails
8879    /// - The server returns an error status
8880    /// - Attempting to remove the last treasury address (if not allowed)
8881    ///
8882    /// # Examples
8883    /// ```no_run
8884    /// # use amp_rs::ApiClient;
8885    /// # #[tokio::main]
8886    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
8887    /// let client = ApiClient::new().await?;
8888    ///
8889    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
8890    /// let addresses = vec![
8891    ///     "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh".to_string(),
8892    ///     "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4".to_string(),
8893    /// ];
8894    ///
8895    /// client.delete_asset_treasury_addresses(asset_uuid, &addresses).await?;
8896    /// println!("Removed {} treasury addresses", addresses.len());
8897    /// # Ok(())
8898    /// # }
8899    /// ```
8900    ///
8901    /// # Related Methods
8902    /// - [`add_asset_treasury_addresses`](Self::add_asset_treasury_addresses) - Add treasury addresses
8903    /// - [`get_asset_treasury_addresses`](Self::get_asset_treasury_addresses) - Get current treasury addresses
8904    /// - [`reissue_asset`](Self::reissue_asset) - Reissue assets using treasury addresses
8905    pub async fn delete_asset_treasury_addresses(
8906        &self,
8907        asset_uuid: &str,
8908        addresses: &[String],
8909    ) -> Result<(), Error> {
8910        self.request_empty(
8911            Method::DELETE,
8912            &["assets", asset_uuid, "treasury-addresses", "delete"],
8913            Some(addresses),
8914        )
8915        .await
8916    }
8917
8918    /// Gets a list of all registered users.
8919    ///
8920    /// # Errors
8921    /// Returns an error if:
8922    /// - Authentication fails
8923    /// - The HTTP request fails
8924    /// - The server returns an error status
8925    /// - The response cannot be parsed
8926    ///
8927    /// # Examples
8928    /// ```no_run
8929    /// # use amp_rs::ApiClient;
8930    /// # #[tokio::main]
8931    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
8932    /// let client = ApiClient::new().await?;
8933    ///
8934    /// let users = client.get_registered_users().await?;
8935    /// for user in users {
8936    ///     println!("User: {} (ID: {})", user.name, user.id);
8937    /// }
8938    /// # Ok(())
8939    /// # }
8940    /// ```
8941    pub async fn get_registered_users(
8942        &self,
8943    ) -> Result<Vec<crate::model::RegisteredUserResponse>, Error> {
8944        self.request_json(Method::GET, &["registered_users"], None::<&()>)
8945            .await
8946    }
8947
8948    /// Gets a specific registered user by ID.
8949    ///
8950    /// # Arguments
8951    /// * `user_id` - The ID of the registered user to retrieve
8952    ///
8953    /// # Errors
8954    /// Returns an error if:
8955    /// - Authentication fails
8956    /// - The HTTP request fails
8957    /// - The user ID does not exist
8958    /// - The response cannot be parsed
8959    ///
8960    /// # Examples
8961    /// ```no_run
8962    /// # use amp_rs::ApiClient;
8963    /// # #[tokio::main]
8964    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
8965    /// let client = ApiClient::new().await?;
8966    ///
8967    /// let user = client.get_registered_user(1).await?;
8968    /// println!("User: {} (ID: {})", user.name, user.id);
8969    /// # Ok(())
8970    /// # }
8971    /// ```
8972    pub async fn get_registered_user(
8973        &self,
8974        user_id: i64,
8975    ) -> Result<crate::model::RegisteredUserResponse, Error> {
8976        self.request_json(
8977            Method::GET,
8978            &["registered_users", &user_id.to_string()],
8979            None::<&()>,
8980        )
8981        .await
8982    }
8983
8984    /// Creates a new registered user in the AMP system.
8985    ///
8986    /// This method creates a new registered user with the provided information. Registered users
8987    /// can be associated with GAIDs, assigned to categories, and receive asset assignments.
8988    ///
8989    /// # Arguments
8990    /// * `new_user` - A `RegisteredUserAdd` struct containing the user information to create
8991    ///
8992    /// # Returns
8993    /// Returns a `RegisteredUserResponse` containing the created user's information including
8994    /// the assigned user ID.
8995    ///
8996    /// # Errors
8997    /// Returns an error if:
8998    /// - Authentication fails or insufficient permissions
8999    /// - The user data is invalid (e.g., missing required fields, invalid email format)
9000    /// - A user with the same identifier already exists
9001    /// - The HTTP request fails
9002    /// - The server returns an error status
9003    /// - The response cannot be parsed
9004    ///
9005    /// # Examples
9006    /// ```no_run
9007    /// # use amp_rs::{ApiClient, model::RegisteredUserAdd};
9008    /// # #[tokio::main]
9009    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9010    /// let client = ApiClient::new().await?;
9011    ///
9012    /// let new_user = RegisteredUserAdd {
9013    ///     name: "John Doe".to_string(),
9014    ///     gaid: Some("GAbYScu6jkWUND2jo3L4KJxyvo55d".to_string()),
9015    ///     is_company: false,
9016    /// };
9017    ///
9018    /// let created_user = client.add_registered_user(&new_user).await?;
9019    /// println!("Created user: {} with ID {}", created_user.name, created_user.id);
9020    /// # Ok(())
9021    /// # }
9022    /// ```
9023    ///
9024    /// # Related Methods
9025    /// - [`get_registered_users`](Self::get_registered_users) - List all registered users
9026    /// - [`edit_registered_user`](Self::edit_registered_user) - Update user information
9027    /// - [`delete_registered_user`](Self::delete_registered_user) - Remove a user
9028    pub async fn add_registered_user(
9029        &self,
9030        new_user: &crate::model::RegisteredUserAdd,
9031    ) -> Result<crate::model::RegisteredUserResponse, Error> {
9032        self.request_json(Method::POST, &["registered_users", "add"], Some(new_user))
9033            .await
9034    }
9035
9036    /// Removes a registered user from the AMP system.
9037    ///
9038    /// This method permanently deletes a registered user and all associated data. This operation
9039    /// cannot be undone. Any GAIDs associated with the user will be disassociated, and any
9040    /// pending assignments may be affected.
9041    ///
9042    /// # Arguments
9043    /// * `user_id` - The ID of the registered user to delete
9044    ///
9045    /// # Returns
9046    /// Returns `Ok(())` on successful deletion.
9047    ///
9048    /// # Errors
9049    /// Returns an error if:
9050    /// - Authentication fails or insufficient permissions
9051    /// - The user ID is invalid or does not exist
9052    /// - The user has active assignments that prevent deletion
9053    /// - The HTTP request fails
9054    /// - The server returns an error status
9055    ///
9056    /// # Examples
9057    /// ```no_run
9058    /// # use amp_rs::ApiClient;
9059    /// # #[tokio::main]
9060    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9061    /// let client = ApiClient::new().await?;
9062    ///
9063    /// let user_id = 123;
9064    /// client.delete_registered_user(user_id).await?;
9065    /// println!("Successfully deleted user with ID {}", user_id);
9066    /// # Ok(())
9067    /// # }
9068    /// ```
9069    ///
9070    /// # Related Methods
9071    /// - [`get_registered_user`](Self::get_registered_user) - Get user information before deletion
9072    /// - [`add_registered_user`](Self::add_registered_user) - Create a new user
9073    /// - [`get_registered_user_summary`](Self::get_registered_user_summary) - Check user's assignments
9074    pub async fn delete_registered_user(&self, user_id: i64) -> Result<(), Error> {
9075        self.request_empty(
9076            Method::DELETE,
9077            &["registered_users", &user_id.to_string(), "delete"],
9078            None::<&()>,
9079        )
9080        .await
9081    }
9082
9083    /// Updates registered user information.
9084    ///
9085    /// This method allows you to modify the information of an existing registered user.
9086    /// Only the fields provided in the edit data will be updated; other fields remain unchanged.
9087    ///
9088    /// # Arguments
9089    /// * `registered_user_id` - The ID of the registered user to update
9090    /// * `edit_data` - A `RegisteredUserEdit` struct containing the fields to update
9091    ///
9092    /// # Returns
9093    /// Returns a `RegisteredUserResponse` containing the updated user information.
9094    ///
9095    /// # Errors
9096    /// Returns an error if:
9097    /// - Authentication fails or insufficient permissions
9098    /// - The user ID is invalid or does not exist
9099    /// - The edit data contains invalid values (e.g., invalid email format)
9100    /// - The HTTP request fails
9101    /// - The server returns an error status
9102    /// - The response cannot be parsed
9103    ///
9104    /// # Examples
9105    /// ```no_run
9106    /// # use amp_rs::{ApiClient, model::RegisteredUserEdit};
9107    /// # #[tokio::main]
9108    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9109    /// let client = ApiClient::new().await?;
9110    ///
9111    /// let user_id = 123;
9112    /// let edit_data = RegisteredUserEdit {
9113    ///     name: Some("Jane Doe".to_string()),
9114    /// };
9115    ///
9116    /// let updated_user = client.edit_registered_user(user_id, &edit_data).await?;
9117    /// println!("Updated user: {}", updated_user.name);
9118    /// # Ok(())
9119    /// # }
9120    /// ```
9121    ///
9122    /// # Related Methods
9123    /// - [`get_registered_user`](Self::get_registered_user) - Get current user information
9124    /// - [`add_registered_user`](Self::add_registered_user) - Create a new user
9125    /// - [`delete_registered_user`](Self::delete_registered_user) - Remove a user
9126    pub async fn edit_registered_user(
9127        &self,
9128        registered_user_id: i64,
9129        edit_data: &crate::model::RegisteredUserEdit,
9130    ) -> Result<crate::model::RegisteredUserResponse, Error> {
9131        self.request_json(
9132            Method::PUT,
9133            &["registered_users", &registered_user_id.to_string(), "edit"],
9134            Some(edit_data),
9135        )
9136        .await
9137    }
9138
9139    /// Gets comprehensive summary information for a registered user including assets and distributions.
9140    ///
9141    /// This method retrieves detailed summary information about a registered user, including
9142    /// their basic information, associated assets, assignment history, and distribution records.
9143    /// This provides a complete overview of the user's activity and holdings in the system.
9144    ///
9145    /// # Arguments
9146    /// * `registered_user_id` - The ID of the registered user to get summary for
9147    ///
9148    /// # Returns
9149    /// Returns a `RegisteredUserSummary` containing:
9150    /// - Basic user information (name, email, etc.)
9151    /// - List of associated GAIDs
9152    /// - Asset assignments and their status
9153    /// - Distribution history
9154    /// - Balance information
9155    /// - Activity timestamps
9156    ///
9157    /// # Errors
9158    /// Returns an error if:
9159    /// - Authentication fails or insufficient permissions
9160    /// - The user ID is invalid or does not exist
9161    /// - The HTTP request fails
9162    /// - The server returns an error status
9163    /// - The response cannot be parsed
9164    ///
9165    /// # Examples
9166    /// ```no_run
9167    /// # use amp_rs::ApiClient;
9168    /// # #[tokio::main]
9169    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9170    /// let client = ApiClient::new().await?;
9171    ///
9172    /// let user_id = 123;
9173    /// let summary = client.get_registered_user_summary(user_id).await?;
9174    ///
9175    /// println!("Asset UUID: {}", summary.asset_uuid);
9176    /// println!("Asset ID: {}", summary.asset_id);
9177    /// println!("Asset assignments: {}", summary.assignments.len());
9178    /// println!("Distributions received: {}", summary.distributions.len());
9179    /// # Ok(())
9180    /// # }
9181    /// ```
9182    ///
9183    /// # Related Methods
9184    /// - [`get_registered_user`](Self::get_registered_user) - Get basic user information
9185    /// - [`get_registered_user_gaids`](Self::get_registered_user_gaids) - Get only GAIDs
9186    /// - [`get_asset_assignments`](Self::get_asset_assignments) - Get assignments for specific asset
9187    pub async fn get_registered_user_summary(
9188        &self,
9189        registered_user_id: i64,
9190    ) -> Result<crate::model::RegisteredUserSummary, Error> {
9191        self.request_json(
9192            Method::GET,
9193            &[
9194                "registered_users",
9195                &registered_user_id.to_string(),
9196                "summary",
9197            ],
9198            None::<&()>,
9199        )
9200        .await
9201    }
9202
9203    /// Gets all GAIDs (Green Address IDs) associated with a registered user.
9204    ///
9205    /// This method retrieves a list of all GAIDs that are currently associated with the specified
9206    /// registered user. GAIDs are unique identifiers that can be used to receive assets and
9207    /// track ownership.
9208    ///
9209    /// # Arguments
9210    /// * `registered_user_id` - The ID of the registered user to get GAIDs for
9211    ///
9212    /// # Returns
9213    /// Returns a vector of GAID strings associated with the user.
9214    ///
9215    /// # Errors
9216    /// Returns an error if:
9217    /// - Authentication fails or insufficient permissions
9218    /// - The user ID is invalid or does not exist
9219    /// - The HTTP request fails
9220    /// - The server returns an error status
9221    /// - The response cannot be parsed
9222    ///
9223    /// # Examples
9224    /// ```no_run
9225    /// # use amp_rs::ApiClient;
9226    /// # #[tokio::main]
9227    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9228    /// let client = ApiClient::new().await?;
9229    ///
9230    /// let user_id = 123;
9231    /// let gaids = client.get_registered_user_gaids(user_id).await?;
9232    ///
9233    /// println!("User {} has {} associated GAIDs:", user_id, gaids.len());
9234    /// for gaid in gaids {
9235    ///     println!("  - {}", gaid);
9236    /// }
9237    /// # Ok(())
9238    /// # }
9239    /// ```
9240    ///
9241    /// # Related Methods
9242    /// - [`add_gaid_to_registered_user`](Self::add_gaid_to_registered_user) - Associate a GAID with user
9243    /// - [`set_default_gaid_for_registered_user`](Self::set_default_gaid_for_registered_user) - Set default GAID
9244    /// - [`get_gaid_registered_user`](Self::get_gaid_registered_user) - Find user by GAID
9245    /// - [`validate_gaid`](Self::validate_gaid) - Validate GAID format
9246    pub async fn get_registered_user_gaids(
9247        &self,
9248        registered_user_id: i64,
9249    ) -> Result<Vec<String>, Error> {
9250        self.request_json(
9251            Method::GET,
9252            &["registered_users", &registered_user_id.to_string(), "gaids"],
9253            None::<&()>,
9254        )
9255        .await
9256    }
9257
9258    /// Associates a GAID with a registered user.
9259    ///
9260    /// # Arguments
9261    /// * `registered_user_id` - The ID of the registered user
9262    /// * `gaid` - The GAID to associate with the user
9263    ///
9264    /// # Errors
9265    ///
9266    /// Returns an error if:
9267    /// - Authentication fails
9268    /// - The HTTP request fails
9269    /// - The server returns an error status
9270    /// - The registered user ID is invalid
9271    /// - The GAID is invalid or already associated
9272    pub async fn add_gaid_to_registered_user(
9273        &self,
9274        registered_user_id: i64,
9275        gaid: &str,
9276    ) -> Result<(), Error> {
9277        let request = GaidRequest {
9278            gaid: gaid.to_string(),
9279        };
9280
9281        self.request_empty(
9282            Method::POST,
9283            &[
9284                "registered_users",
9285                &registered_user_id.to_string(),
9286                "gaids",
9287                "add",
9288            ],
9289            Some(request),
9290        )
9291        .await
9292    }
9293
9294    /// Sets an existing GAID as the default for a registered user.
9295    ///
9296    /// This method allows you to designate a specific GAID as the primary/default
9297    /// GAID for a registered user. The GAID must already be associated with the user.
9298    ///
9299    /// # Arguments
9300    /// * `registered_user_id` - The ID of the registered user
9301    /// * `gaid` - The GAID to set as default
9302    ///
9303    /// # Returns
9304    /// Returns `Ok(())` if the operation is successful.
9305    ///
9306    /// # Errors
9307    /// Returns an error if:
9308    /// - Authentication fails
9309    /// - The HTTP request fails
9310    /// - The server returns an error status
9311    /// - The registered user ID is invalid
9312    /// - The GAID is not associated with the user
9313    pub async fn set_default_gaid_for_registered_user(
9314        &self,
9315        registered_user_id: i64,
9316        gaid: &str,
9317    ) -> Result<(), Error> {
9318        let request = GaidRequest {
9319            gaid: gaid.to_string(),
9320        };
9321
9322        self.request_empty(
9323            Method::POST,
9324            &[
9325                "registered_users",
9326                &registered_user_id.to_string(),
9327                "gaids",
9328                "set-default",
9329            ],
9330            Some(request),
9331        )
9332        .await
9333    }
9334
9335    /// Retrieves the registered user associated with a GAID
9336    ///
9337    /// # Arguments
9338    /// * `gaid` - The GAID to look up
9339    ///
9340    /// # Returns
9341    /// Returns the registered user data if the GAID is associated with a user
9342    ///
9343    /// # Errors
9344    /// This function will return an error if:
9345    /// - The GAID has no associated user
9346    /// - The GAID is invalid
9347    /// - Network or authentication errors occur
9348    pub async fn get_gaid_registered_user(
9349        &self,
9350        gaid: &str,
9351    ) -> Result<crate::model::RegisteredUserResponse, Error> {
9352        self.request_json(
9353            Method::GET,
9354            &["gaids", gaid, "registered_user"],
9355            None::<&()>,
9356        )
9357        .await
9358    }
9359
9360    /// Gets the balance information for a specific GAID.
9361    ///
9362    /// This method retrieves all asset balances associated with the given GAID,
9363    /// including confirmed balances and any lost outputs.
9364    ///
9365    /// # Arguments
9366    /// * `gaid` - The GAID to query balance for
9367    ///
9368    /// # Returns
9369    /// Returns a `Balance` struct containing confirmed balances and lost outputs
9370    ///
9371    /// # Errors
9372    /// Returns an error if:
9373    /// - The GAID is invalid
9374    /// - Network or authentication errors occur
9375    /// - The response cannot be parsed
9376    ///
9377    /// # Examples
9378    /// ```no_run
9379    /// # use amp_rs::ApiClient;
9380    /// # #[tokio::main]
9381    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9382    /// let client = ApiClient::new().await?;
9383    ///
9384    /// let gaid = "GAbYScu6jkWUND2jo3L4KJxyvo55d";
9385    /// let balance = client.get_gaid_balance(gaid).await?;
9386    ///
9387    /// println!("GAID {} has {} balance entries", gaid, balance.len());
9388    /// for entry in balance {
9389    ///     println!("Asset {}: {} units", entry.asset_id, entry.balance);
9390    /// }
9391    /// # Ok(())
9392    /// # }
9393    /// ```
9394    pub async fn get_gaid_balance(&self, gaid: &str) -> Result<Balance, Error> {
9395        self.request_json(Method::GET, &["gaids", gaid, "balance"], None::<&()>)
9396            .await
9397    }
9398
9399    /// Retrieves the specific asset balance for a GAID
9400    ///
9401    /// # Arguments
9402    /// * `gaid` - The GAID to query
9403    /// * `asset_uuid` - The UUID of the asset to query
9404    ///
9405    /// # Returns
9406    /// Returns the specific asset balance information
9407    ///
9408    /// # Errors
9409    /// Returns an error if:
9410    /// - The GAID is invalid
9411    /// - The asset UUID is invalid
9412    /// - Network or authentication errors occur
9413    /// - The response cannot be parsed
9414    pub async fn get_gaid_asset_balance(
9415        &self,
9416        gaid: &str,
9417        asset_uuid: &str,
9418    ) -> Result<Ownership, Error> {
9419        // Try to get the response as a GaidBalanceEntry first, then convert to Ownership
9420        let balance_entry: GaidBalanceEntry = self
9421            .request_json(
9422                Method::GET,
9423                &["gaids", gaid, "balance", asset_uuid],
9424                None::<&()>,
9425            )
9426            .await?;
9427
9428        // Convert GaidBalanceEntry to Ownership format
9429        Ok(Ownership {
9430            owner: gaid.to_string(),
9431            amount: balance_entry.balance,
9432            gaid: Some(gaid.to_string()),
9433        })
9434    }
9435
9436    /// Gets a list of all categories.
9437    ///
9438    /// # Returns
9439    /// Returns a vector of `CategoryResponse` objects
9440    ///
9441    /// # Errors
9442    /// Returns an error if:
9443    /// - Authentication fails
9444    /// - The HTTP request fails
9445    /// - The response cannot be parsed
9446    ///
9447    /// # Examples
9448    /// ```no_run
9449    /// # use amp_rs::ApiClient;
9450    /// # #[tokio::main]
9451    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9452    /// let client = ApiClient::new().await?;
9453    ///
9454    /// let categories = client.get_categories().await?;
9455    /// for category in categories {
9456    ///     println!("Category: {} (ID: {})", category.name, category.id);
9457    ///     if let Some(desc) = category.description {
9458    ///         println!("  Description: {}", desc);
9459    ///     }
9460    /// }
9461    /// # Ok(())
9462    /// # }
9463    /// ```
9464    pub async fn get_categories(&self) -> Result<Vec<CategoryResponse>, Error> {
9465        self.request_json(Method::GET, &["categories"], None::<&()>)
9466            .await
9467    }
9468
9469    /// Creates a new category for organizing users and assets.
9470    ///
9471    /// This method creates a new category that can be used to group registered users and assets
9472    /// for organizational purposes. Categories help manage permissions and provide logical
9473    /// groupings for assets and users.
9474    ///
9475    /// # Arguments
9476    /// * `new_category` - A `CategoryAdd` struct containing the category information to create
9477    ///
9478    /// # Returns
9479    /// Returns a `CategoryResponse` containing the created category information including
9480    /// the assigned category ID.
9481    ///
9482    /// # Errors
9483    /// Returns an error if:
9484    /// - Authentication fails or insufficient permissions
9485    /// - The category data is invalid (e.g., missing name, invalid characters)
9486    /// - A category with the same name already exists
9487    /// - The HTTP request fails
9488    /// - The server returns an error status
9489    /// - The response cannot be parsed
9490    ///
9491    /// # Examples
9492    /// ```no_run
9493    /// # use amp_rs::{ApiClient, model::CategoryAdd};
9494    /// # #[tokio::main]
9495    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9496    /// let client = ApiClient::new().await?;
9497    ///
9498    /// let new_category = CategoryAdd {
9499    ///     name: "Premium Users".to_string(),
9500    ///     description: Some("High-value users with special privileges".to_string()),
9501    /// };
9502    ///
9503    /// let created_category = client.add_category(&new_category).await?;
9504    /// println!("Created category: {} with ID {}", created_category.name, created_category.id);
9505    /// # Ok(())
9506    /// # }
9507    /// ```
9508    ///
9509    /// # Related Methods
9510    /// - [`get_categories`](Self::get_categories) - List all categories
9511    /// - [`edit_category`](Self::edit_category) - Update category information
9512    /// - [`delete_category`](Self::delete_category) - Remove a category
9513    /// - [`add_registered_user_to_category`](Self::add_registered_user_to_category) - Add users to category
9514    pub async fn add_category(
9515        &self,
9516        new_category: &CategoryAdd,
9517    ) -> Result<CategoryResponse, Error> {
9518        self.request_json(Method::POST, &["categories", "add"], Some(new_category))
9519            .await
9520    }
9521
9522    /// Gets a specific category by ID.
9523    ///
9524    /// This method retrieves detailed information about a specific category, including
9525    /// its name, description, and associated users and assets.
9526    ///
9527    /// # Arguments
9528    /// * `category_id` - The ID of the category to retrieve
9529    ///
9530    /// # Returns
9531    /// Returns a `CategoryResponse` containing the category information including:
9532    /// - Category ID, name, and description
9533    /// - List of associated registered users
9534    /// - List of associated assets
9535    /// - Creation and modification timestamps
9536    ///
9537    /// # Errors
9538    /// Returns an error if:
9539    /// - Authentication fails or insufficient permissions
9540    /// - The category ID is invalid or does not exist
9541    /// - The HTTP request fails
9542    /// - The server returns an error status
9543    /// - The response cannot be parsed
9544    ///
9545    /// # Examples
9546    /// ```no_run
9547    /// # use amp_rs::ApiClient;
9548    /// # #[tokio::main]
9549    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9550    /// let client = ApiClient::new().await?;
9551    ///
9552    /// let category_id = 1;
9553    /// let category = client.get_category(category_id).await?;
9554    ///
9555    /// println!("Category: {} (ID: {})", category.name, category.id);
9556    /// if let Some(desc) = category.description {
9557    ///     println!("Description: {}", desc);
9558    /// }
9559    /// println!("Users: {}, Assets: {}", category.registered_users.len(), category.assets.len());
9560    /// # Ok(())
9561    /// # }
9562    /// ```
9563    ///
9564    /// # Related Methods
9565    /// - [`get_categories`](Self::get_categories) - List all categories
9566    /// - [`add_category`](Self::add_category) - Create a new category
9567    /// - [`edit_category`](Self::edit_category) - Update category information
9568    /// - [`delete_category`](Self::delete_category) - Remove a category
9569    pub async fn get_category(&self, category_id: i64) -> Result<CategoryResponse, Error> {
9570        self.request_json(
9571            Method::GET,
9572            &["categories", &category_id.to_string()],
9573            None::<&()>,
9574        )
9575        .await
9576    }
9577
9578    /// Updates category information.
9579    ///
9580    /// This method allows you to modify the information of an existing category.
9581    /// Only the fields provided in the edit data will be updated; other fields remain unchanged.
9582    ///
9583    /// # Arguments
9584    /// * `category_id` - The ID of the category to update
9585    /// * `edit_category` - A `CategoryEdit` struct containing the fields to update
9586    ///
9587    /// # Returns
9588    /// Returns a `CategoryResponse` containing the updated category information.
9589    ///
9590    /// # Errors
9591    /// Returns an error if:
9592    /// - Authentication fails or insufficient permissions
9593    /// - The category ID is invalid or does not exist
9594    /// - The edit data contains invalid values (e.g., empty name, invalid characters)
9595    /// - A category with the new name already exists (if name is being changed)
9596    /// - The HTTP request fails
9597    /// - The server returns an error status
9598    /// - The response cannot be parsed
9599    ///
9600    /// # Examples
9601    /// ```no_run
9602    /// # use amp_rs::{ApiClient, model::CategoryEdit};
9603    /// # #[tokio::main]
9604    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9605    /// let client = ApiClient::new().await?;
9606    ///
9607    /// let category_id = 1;
9608    /// let edit_data = CategoryEdit {
9609    ///     name: Some("VIP Users".to_string()),
9610    ///     description: Some("Very important users with premium access".to_string()),
9611    /// };
9612    ///
9613    /// let updated_category = client.edit_category(category_id, &edit_data).await?;
9614    /// println!("Updated category: {}", updated_category.name);
9615    /// # Ok(())
9616    /// # }
9617    /// ```
9618    ///
9619    /// # Related Methods
9620    /// - [`get_category`](Self::get_category) - Get current category information
9621    /// - [`add_category`](Self::add_category) - Create a new category
9622    /// - [`delete_category`](Self::delete_category) - Remove a category
9623    pub async fn edit_category(
9624        &self,
9625        category_id: i64,
9626        edit_category: &CategoryEdit,
9627    ) -> Result<CategoryResponse, Error> {
9628        self.request_json(
9629            Method::PUT,
9630            &["categories", &category_id.to_string(), "edit"],
9631            Some(edit_category),
9632        )
9633        .await
9634    }
9635
9636    /// Removes a category from the system.
9637    ///
9638    /// This method permanently deletes a category. All users and assets associated with the
9639    /// category will be disassociated, but the users and assets themselves are not deleted.
9640    /// This operation cannot be undone.
9641    ///
9642    /// # Arguments
9643    /// * `category_id` - The ID of the category to delete
9644    ///
9645    /// # Returns
9646    /// Returns `Ok(())` on successful deletion.
9647    ///
9648    /// # Errors
9649    /// Returns an error if:
9650    /// - Authentication fails or insufficient permissions
9651    /// - The category ID is invalid or does not exist
9652    /// - The category is still in use and cannot be deleted (depending on system configuration)
9653    /// - The HTTP request fails
9654    /// - The server returns an error status
9655    ///
9656    /// # Examples
9657    /// ```no_run
9658    /// # use amp_rs::ApiClient;
9659    /// # #[tokio::main]
9660    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9661    /// let client = ApiClient::new().await?;
9662    ///
9663    /// let category_id = 1;
9664    /// client.delete_category(category_id).await?;
9665    /// println!("Successfully deleted category with ID {}", category_id);
9666    /// # Ok(())
9667    /// # }
9668    /// ```
9669    ///
9670    /// # Related Methods
9671    /// - [`get_category`](Self::get_category) - Get category information before deletion
9672    /// - [`add_category`](Self::add_category) - Create a new category
9673    /// - [`remove_registered_user_from_category`](Self::remove_registered_user_from_category) - Remove users first
9674    /// - [`remove_asset_from_category`](Self::remove_asset_from_category) - Remove assets first
9675    pub async fn delete_category(&self, category_id: i64) -> Result<(), Error> {
9676        self.request_empty(
9677            Method::DELETE,
9678            &["categories", &category_id.to_string(), "delete"],
9679            None::<&()>,
9680        )
9681        .await
9682    }
9683
9684    /// Associates a registered user with a category.
9685    ///
9686    /// This method adds a registered user to a category, allowing for organized grouping
9687    /// of users. Users can belong to multiple categories, and categories can contain
9688    /// multiple users.
9689    ///
9690    /// # Arguments
9691    /// * `category_id` - The ID of the category to add the user to
9692    /// * `user_id` - The ID of the registered user to add to the category
9693    ///
9694    /// # Returns
9695    /// Returns a `CategoryResponse` containing the updated category information including
9696    /// the newly added user.
9697    ///
9698    /// # Errors
9699    /// Returns an error if:
9700    /// - Authentication fails or insufficient permissions
9701    /// - The category ID is invalid or does not exist
9702    /// - The user ID is invalid or does not exist
9703    /// - The user is already associated with the category
9704    /// - The HTTP request fails
9705    /// - The server returns an error status
9706    /// - The response cannot be parsed
9707    ///
9708    /// # Examples
9709    /// ```no_run
9710    /// # use amp_rs::ApiClient;
9711    /// # #[tokio::main]
9712    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9713    /// let client = ApiClient::new().await?;
9714    ///
9715    /// let category_id = 1;
9716    /// let user_id = 123;
9717    ///
9718    /// let updated_category = client.add_registered_user_to_category(category_id, user_id).await?;
9719    /// println!("Added user {} to category '{}'", user_id, updated_category.name);
9720    /// println!("Category now has {} users", updated_category.registered_users.len());
9721    /// # Ok(())
9722    /// # }
9723    /// ```
9724    ///
9725    /// # Related Methods
9726    /// - [`remove_registered_user_from_category`](Self::remove_registered_user_from_category) - Remove user from category
9727    /// - [`get_category`](Self::get_category) - Get category information including users
9728    /// - [`get_registered_user`](Self::get_registered_user) - Get user information
9729    pub async fn add_registered_user_to_category(
9730        &self,
9731        category_id: i64,
9732        user_id: i64,
9733    ) -> Result<CategoryResponse, Error> {
9734        self.request_json(
9735            Method::PUT,
9736            &[
9737                "categories",
9738                &category_id.to_string(),
9739                "registered_users",
9740                &user_id.to_string(),
9741                "add",
9742            ],
9743            None::<&()>,
9744        )
9745        .await
9746    }
9747
9748    /// Removes a registered user from a category.
9749    ///
9750    /// This method disassociates a registered user from a category. The user remains in the
9751    /// system but is no longer part of the specified category. This does not affect the user's
9752    /// association with other categories.
9753    ///
9754    /// # Arguments
9755    /// * `category_id` - The ID of the category to remove the user from
9756    /// * `user_id` - The ID of the registered user to remove from the category
9757    ///
9758    /// # Returns
9759    /// Returns a `CategoryResponse` containing the updated category information without
9760    /// the removed user.
9761    ///
9762    /// # Errors
9763    /// Returns an error if:
9764    /// - Authentication fails or insufficient permissions
9765    /// - The category ID is invalid or does not exist
9766    /// - The user ID is invalid or does not exist
9767    /// - The user is not currently associated with the category
9768    /// - The HTTP request fails
9769    /// - The server returns an error status
9770    /// - The response cannot be parsed
9771    ///
9772    /// # Examples
9773    /// ```no_run
9774    /// # use amp_rs::ApiClient;
9775    /// # #[tokio::main]
9776    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9777    /// let client = ApiClient::new().await?;
9778    ///
9779    /// let category_id = 1;
9780    /// let user_id = 123;
9781    ///
9782    /// let updated_category = client.remove_registered_user_from_category(category_id, user_id).await?;
9783    /// println!("Removed user {} from category '{}'", user_id, updated_category.name);
9784    /// println!("Category now has {} users", updated_category.registered_users.len());
9785    /// # Ok(())
9786    /// # }
9787    /// ```
9788    ///
9789    /// # Related Methods
9790    /// - [`add_registered_user_to_category`](Self::add_registered_user_to_category) - Add user to category
9791    /// - [`get_category`](Self::get_category) - Get category information including users
9792    /// - [`get_registered_user`](Self::get_registered_user) - Get user information
9793    pub async fn remove_registered_user_from_category(
9794        &self,
9795        category_id: i64,
9796        user_id: i64,
9797    ) -> Result<CategoryResponse, Error> {
9798        self.request_json(
9799            Method::PUT,
9800            &[
9801                "categories",
9802                &category_id.to_string(),
9803                "registered_users",
9804                &user_id.to_string(),
9805                "remove",
9806            ],
9807            None::<&()>,
9808        )
9809        .await
9810    }
9811
9812    /// Associates an asset with a category.
9813    ///
9814    /// This method adds an asset to a category, allowing for organized grouping of assets.
9815    /// Assets can belong to multiple categories, and categories can contain multiple assets.
9816    /// This helps with asset management and permission organization.
9817    ///
9818    /// # Arguments
9819    /// * `category_id` - The ID of the category to add the asset to
9820    /// * `asset_uuid` - The UUID of the asset to add to the category
9821    ///
9822    /// # Returns
9823    /// Returns a `CategoryResponse` containing the updated category information including
9824    /// the newly added asset.
9825    ///
9826    /// # Errors
9827    /// Returns an error if:
9828    /// - Authentication fails or insufficient permissions
9829    /// - The category ID is invalid or does not exist
9830    /// - The asset UUID is invalid or does not exist
9831    /// - The asset is already associated with the category
9832    /// - The HTTP request fails
9833    /// - The server returns an error status
9834    /// - The response cannot be parsed
9835    ///
9836    /// # Examples
9837    /// ```no_run
9838    /// # use amp_rs::ApiClient;
9839    /// # #[tokio::main]
9840    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9841    /// let client = ApiClient::new().await?;
9842    ///
9843    /// let category_id = 1;
9844    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
9845    ///
9846    /// let updated_category = client.add_asset_to_category(category_id, asset_uuid).await?;
9847    /// println!("Added asset {} to category '{}'", asset_uuid, updated_category.name);
9848    /// println!("Category now has {} assets", updated_category.assets.len());
9849    /// # Ok(())
9850    /// # }
9851    /// ```
9852    ///
9853    /// # Related Methods
9854    /// - [`remove_asset_from_category`](Self::remove_asset_from_category) - Remove asset from category
9855    /// - [`get_category`](Self::get_category) - Get category information including assets
9856    /// - [`get_asset`](Self::get_asset) - Get asset information
9857    pub async fn add_asset_to_category(
9858        &self,
9859        category_id: i64,
9860        asset_uuid: &str,
9861    ) -> Result<CategoryResponse, Error> {
9862        self.request_json(
9863            Method::PUT,
9864            &[
9865                "categories",
9866                &category_id.to_string(),
9867                "assets",
9868                asset_uuid,
9869                "add",
9870            ],
9871            None::<&()>,
9872        )
9873        .await
9874    }
9875
9876    /// Removes an asset from a category.
9877    ///
9878    /// This method disassociates an asset from a category. The asset remains in the system
9879    /// but is no longer part of the specified category. This does not affect the asset's
9880    /// association with other categories.
9881    ///
9882    /// # Arguments
9883    /// * `category_id` - The ID of the category to remove the asset from
9884    /// * `asset_uuid` - The UUID of the asset to remove from the category
9885    ///
9886    /// # Returns
9887    /// Returns a `CategoryResponse` containing the updated category information without
9888    /// the removed asset.
9889    ///
9890    /// # Errors
9891    /// Returns an error if:
9892    /// - Authentication fails or insufficient permissions
9893    /// - The category ID is invalid or does not exist
9894    /// - The asset UUID is invalid or does not exist
9895    /// - The asset is not currently associated with the category
9896    /// - The HTTP request fails
9897    /// - The server returns an error status
9898    /// - The response cannot be parsed
9899    ///
9900    /// # Examples
9901    /// ```no_run
9902    /// # use amp_rs::ApiClient;
9903    /// # #[tokio::main]
9904    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9905    /// let client = ApiClient::new().await?;
9906    ///
9907    /// let category_id = 1;
9908    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
9909    ///
9910    /// let updated_category = client.remove_asset_from_category(category_id, asset_uuid).await?;
9911    /// println!("Removed asset {} from category '{}'", asset_uuid, updated_category.name);
9912    /// println!("Category now has {} assets", updated_category.assets.len());
9913    /// # Ok(())
9914    /// # }
9915    /// ```
9916    ///
9917    /// # Related Methods
9918    /// - [`add_asset_to_category`](Self::add_asset_to_category) - Add asset to category
9919    /// - [`get_category`](Self::get_category) - Get category information including assets
9920    /// - [`get_asset`](Self::get_asset) - Get asset information
9921    pub async fn remove_asset_from_category(
9922        &self,
9923        category_id: i64,
9924        asset_uuid: &str,
9925    ) -> Result<CategoryResponse, Error> {
9926        self.request_json(
9927            Method::PUT,
9928            &[
9929                "categories",
9930                &category_id.to_string(),
9931                "assets",
9932                asset_uuid,
9933                "remove",
9934            ],
9935            None::<&()>,
9936        )
9937        .await
9938    }
9939
9940    /// Validates a GAID (Green Address ID).
9941    ///
9942    /// # Arguments
9943    /// * `gaid` - The GAID string to validate
9944    ///
9945    /// # Returns
9946    /// Returns a `ValidateGaidResponse` indicating whether the GAID is valid
9947    ///
9948    /// # Errors
9949    /// Returns an error if:
9950    /// - Authentication fails
9951    /// - The HTTP request fails
9952    /// - The response cannot be parsed
9953    ///
9954    /// # Examples
9955    /// ```no_run
9956    /// # use amp_rs::ApiClient;
9957    /// # #[tokio::main]
9958    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
9959    /// let client = ApiClient::new().await?;
9960    ///
9961    /// let gaid = "GAbYScu6jkWUND2jo3L4KJxyvo55d";
9962    /// let validation = client.validate_gaid(gaid).await?;
9963    ///
9964    /// if validation.is_valid {
9965    ///     println!("GAID {} is valid", gaid);
9966    /// } else {
9967    ///     println!("GAID {} is invalid: {:?}", gaid, validation.error);
9968    /// }
9969    /// # Ok(())
9970    /// # }
9971    /// ```
9972    pub async fn validate_gaid(
9973        &self,
9974        gaid: &str,
9975    ) -> Result<crate::model::ValidateGaidResponse, Error> {
9976        self.request_json(Method::GET, &["gaids", gaid, "validate"], None::<&()>)
9977            .await
9978    }
9979
9980    /// Gets the address associated with a GAID.
9981    ///
9982    /// # Arguments
9983    /// * `gaid` - The GAID to get the address for
9984    ///
9985    /// # Returns
9986    /// Returns an `AddressGaidResponse` containing the address
9987    ///
9988    /// # Errors
9989    /// Returns an error if:
9990    /// - The GAID is invalid
9991    /// - Authentication fails
9992    /// - The HTTP request fails
9993    /// - The response cannot be parsed
9994    ///
9995    /// # Examples
9996    /// ```no_run
9997    /// # use amp_rs::ApiClient;
9998    /// # #[tokio::main]
9999    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
10000    /// let client = ApiClient::new().await?;
10001    ///
10002    /// let gaid = "GAbYScu6jkWUND2jo3L4KJxyvo55d";
10003    /// let address_response = client.get_gaid_address(gaid).await?;
10004    ///
10005    /// println!("Address for GAID {}: {}", gaid, address_response.address);
10006    /// # Ok(())
10007    /// # }
10008    /// ```
10009    pub async fn get_gaid_address(
10010        &self,
10011        gaid: &str,
10012    ) -> Result<crate::model::AddressGaidResponse, Error> {
10013        self.request_json(Method::GET, &["gaids", gaid, "address"], None::<&()>)
10014            .await
10015    }
10016
10017    /// Gets a list of all managers.
10018    ///
10019    /// # Returns
10020    /// Returns a vector of `Manager` objects
10021    ///
10022    /// # Errors
10023    /// Returns an error if:
10024    /// - Authentication fails
10025    /// - The HTTP request fails
10026    /// - The response cannot be parsed
10027    ///
10028    /// # Examples
10029    /// ```no_run
10030    /// # use amp_rs::ApiClient;
10031    /// # #[tokio::main]
10032    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
10033    /// let client = ApiClient::new().await?;
10034    ///
10035    /// let managers = client.get_managers().await?;
10036    /// for manager in managers {
10037    ///     println!("Manager: {} (ID: {})", manager.username, manager.id);
10038    /// }
10039    /// # Ok(())
10040    /// # }
10041    /// ```
10042    pub async fn get_managers(&self) -> Result<Vec<crate::model::Manager>, Error> {
10043        self.request_json(Method::GET, &["managers"], None::<&()>)
10044            .await
10045    }
10046
10047    /// Creates a new manager.
10048    ///
10049    /// # Arguments
10050    /// * `new_manager` - The manager creation request containing username and password
10051    ///
10052    /// # Returns
10053    /// Returns the created `Manager` object
10054    ///
10055    /// # Errors
10056    /// Returns an error if:
10057    /// - Authentication fails
10058    /// - The HTTP request fails
10059    /// - The manager creation request is invalid
10060    /// - The response cannot be parsed
10061    ///
10062    /// # Examples
10063    /// ```no_run
10064    /// # use amp_rs::{ApiClient, model::ManagerCreate};
10065    /// # #[tokio::main]
10066    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
10067    /// let client = ApiClient::new().await?;
10068    ///
10069    /// let new_manager = ManagerCreate {
10070    ///     username: "new_manager".to_string(),
10071    ///     password: "secure_password".to_string(),
10072    /// };
10073    ///
10074    /// let manager = client.create_manager(&new_manager).await?;
10075    /// println!("Created manager: {} (ID: {})", manager.username, manager.id);
10076    /// # Ok(())
10077    /// # }
10078    /// ```
10079    pub async fn create_manager(
10080        &self,
10081        new_manager: &crate::model::ManagerCreate,
10082    ) -> Result<crate::model::Manager, Error> {
10083        self.request_json(Method::POST, &["managers", "create"], Some(new_manager))
10084            .await
10085    }
10086
10087    /// Gets all assignments for a specific asset.
10088    ///
10089    /// # Arguments
10090    /// * `asset_uuid` - The UUID of the asset to get assignments for
10091    ///
10092    /// # Returns
10093    /// Returns a vector of `Assignment` objects
10094    ///
10095    /// # Errors
10096    /// Returns an error if:
10097    /// - Authentication fails
10098    /// - The HTTP request fails
10099    /// - The asset UUID is invalid
10100    /// - The response cannot be parsed
10101    ///
10102    /// # Examples
10103    /// ```no_run
10104    /// # use amp_rs::ApiClient;
10105    /// # #[tokio::main]
10106    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
10107    /// let client = ApiClient::new().await?;
10108    ///
10109    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
10110    /// let assignments = client.get_asset_assignments(asset_uuid).await?;
10111    ///
10112    /// for assignment in assignments {
10113    ///     println!("Assignment ID: {}, Amount: {}", assignment.id, assignment.amount);
10114    /// }
10115    /// # Ok(())
10116    /// # }
10117    /// ```
10118    pub async fn get_asset_assignments(&self, asset_uuid: &str) -> Result<Vec<Assignment>, Error> {
10119        self.request_json(
10120            Method::GET,
10121            &["assets", asset_uuid, "assignments"],
10122            None::<&()>,
10123        )
10124        .await
10125    }
10126
10127    /// Creates multiple asset assignments in batch.
10128    ///
10129    /// This method creates multiple asset assignments for the specified asset. Each assignment
10130    /// allocates a specific amount of the asset to a registered user. The assignments are
10131    /// created individually due to API limitations, but this method handles the batch processing
10132    /// automatically.
10133    ///
10134    /// # Arguments
10135    /// * `asset_uuid` - The UUID of the asset to create assignments for
10136    /// * `requests` - A slice of `CreateAssetAssignmentRequest` structs containing assignment details
10137    ///
10138    /// # Returns
10139    /// Returns a vector of `Assignment` structs representing the created assignments with their
10140    /// assigned IDs and status information.
10141    ///
10142    /// # Errors
10143    /// Returns an error if:
10144    /// - Authentication fails or insufficient permissions
10145    /// - The asset UUID is invalid or does not exist
10146    /// - Any assignment request contains invalid data (e.g., invalid user ID, negative amount)
10147    /// - Insufficient asset balance for the total requested assignments
10148    /// - Any individual assignment creation fails
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, model::CreateAssetAssignmentRequest};
10156    /// # #[tokio::main]
10157    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
10158    /// let client = ApiClient::new().await?;
10159    ///
10160    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
10161    /// let requests = vec![
10162    ///     CreateAssetAssignmentRequest {
10163    ///         registered_user: 123,
10164    ///         amount: 1000,
10165    ///         vesting_timestamp: None,
10166    ///         ready_for_distribution: false,
10167    ///     },
10168    ///     CreateAssetAssignmentRequest {
10169    ///         registered_user: 456,
10170    ///         amount: 500,
10171    ///         vesting_timestamp: None,
10172    ///         ready_for_distribution: true,
10173    ///     },
10174    /// ];
10175    ///
10176    /// let assignments = client.create_asset_assignments(asset_uuid, &requests).await?;
10177    /// println!("Created {} assignments", assignments.len());
10178    /// for assignment in assignments {
10179    ///     println!("Assignment {}: {} units to user {}",
10180    ///              assignment.id, assignment.amount, assignment.registered_user);
10181    /// }
10182    /// # Ok(())
10183    /// # }
10184    /// ```
10185    ///
10186    /// # Related Methods
10187    /// - [`get_asset_assignments`](Self::get_asset_assignments) - List all assignments for an asset
10188    /// - [`delete_asset_assignment`](Self::delete_asset_assignment) - Remove an assignment
10189    /// - [`edit_asset_assignment`](Self::edit_asset_assignment) - Update assignment details
10190    /// - [`set_assignment_ready_for_distribution`](Self::set_assignment_ready_for_distribution) - Mark for distribution
10191    pub async fn create_asset_assignments(
10192        &self,
10193        asset_uuid: &str,
10194        requests: &[CreateAssetAssignmentRequest],
10195    ) -> Result<Vec<Assignment>, Error> {
10196        use crate::model::CreateAssetAssignmentRequestWrapper;
10197
10198        // The API only supports maximum length 1 per request, so we need to break
10199        // multiple assignments into separate CreateAssetAssignmentRequestWrapper instances
10200        let mut all_assignments = Vec::new();
10201
10202        for request in requests {
10203            let wrapper = CreateAssetAssignmentRequestWrapper {
10204                assignments: vec![request.clone()],
10205            };
10206
10207            let assignments: Vec<Assignment> = self
10208                .request_json(
10209                    Method::POST,
10210                    &["assets", asset_uuid, "assignments", "create"],
10211                    Some(&wrapper),
10212                )
10213                .await?;
10214
10215            all_assignments.extend(assignments);
10216        }
10217
10218        Ok(all_assignments)
10219    }
10220
10221    /// Gets a specific asset assignment by asset UUID and assignment ID.
10222    ///
10223    /// This method sends a GET request to retrieve detailed information about a specific asset
10224    /// assignment. Asset assignments represent the allocation of assets to users or entities,
10225    /// including information such as the assigned amount, recipient details, and assignment status.
10226    ///
10227    /// # Arguments
10228    /// * `asset_uuid` - The UUID of the asset for which to retrieve the assignment
10229    /// * `assignment_id` - The ID of the specific assignment to retrieve
10230    ///
10231    /// # Returns
10232    /// Returns an `Assignment` struct containing the assignment details including:
10233    /// - Assignment ID and amount
10234    /// - Recipient information
10235    /// - Assignment status and metadata
10236    /// - Creation and modification timestamps
10237    ///
10238    /// # Errors
10239    /// Returns an error if:
10240    /// - Authentication fails
10241    /// - The HTTP request fails
10242    /// - The server returns an error status
10243    /// - The asset UUID is invalid or does not exist
10244    /// - The assignment ID is invalid or does not exist
10245    /// - The assignment is not accessible to the current user
10246    /// - The response cannot be parsed as a valid Assignment
10247    ///
10248    /// # Example
10249    /// ```no_run
10250    /// # use amp_rs::ApiClient;
10251    /// # #[tokio::main]
10252    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
10253    /// let client = ApiClient::new().await?;
10254    ///
10255    /// // Retrieve assignment with ID "123" for asset "550e8400-e29b-41d4-a716-446655440000"
10256    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
10257    /// let assignment_id = "123";
10258    ///
10259    /// let assignment = client.get_asset_assignment(asset_uuid, assignment_id).await?;
10260    ///
10261    /// println!("Assignment ID: {}", assignment.id);
10262    /// println!("Assigned amount: {}", assignment.amount);
10263    /// println!("Registered user: {}", assignment.registered_user);
10264    /// # Ok(())
10265    /// # }
10266    /// ```
10267    pub async fn get_asset_assignment(
10268        &self,
10269        asset_uuid: &str,
10270        assignment_id: &str,
10271    ) -> Result<Assignment, Error> {
10272        self.request_json(
10273            Method::GET,
10274            &["assets", asset_uuid, "assignments", assignment_id],
10275            None::<&()>,
10276        )
10277        .await
10278    }
10279
10280    /// Creates a distribution for an asset with the specified assignments.
10281    ///
10282    /// This method initiates the distribution creation process by sending assignment details
10283    /// to the AMP API. The API will return a distribution UUID and address mappings that
10284    /// can be used for subsequent transaction creation and confirmation steps.
10285    ///
10286    /// # Arguments
10287    /// * `asset_uuid` - The UUID of the asset to distribute
10288    /// * `assignments` - A vector of `AssetDistributionAssignment` structs containing user IDs, addresses, and amounts
10289    ///
10290    /// # Returns
10291    /// Returns a `DistributionResponse` containing:
10292    /// - `distribution_uuid` - Unique identifier for the created distribution
10293    /// - `map_address_amount` - Mapping of addresses to amounts to be distributed
10294    /// - `map_address_asset` - Mapping of addresses to asset IDs
10295    /// - `asset_id` - The asset ID for the distribution
10296    ///
10297    /// # Errors
10298    /// Returns an `AmpError` if:
10299    /// - Authentication fails or insufficient permissions
10300    /// - The asset UUID is invalid or does not exist
10301    /// - Assignment data is invalid (e.g., invalid user IDs, negative amounts, invalid addresses)
10302    /// - Insufficient asset balance for the requested distribution
10303    /// - The HTTP request fails
10304    /// - The server returns an error status
10305    /// - The response cannot be parsed
10306    ///
10307    /// # Examples
10308    /// ```no_run
10309    /// # use amp_rs::{ApiClient, model::AssetDistributionAssignment, AmpError};
10310    /// # #[tokio::main]
10311    /// # async fn main() -> Result<(), AmpError> {
10312    /// let client = ApiClient::new().await.map_err(AmpError::from)?;
10313    ///
10314    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
10315    /// let assignments = vec![
10316    ///     AssetDistributionAssignment {
10317    ///         user_id: "user123".to_string(),
10318    ///         address: "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
10319    ///         amount: 100.0,
10320    ///     },
10321    ///     AssetDistributionAssignment {
10322    ///         user_id: "user456".to_string(),
10323    ///         address: "lq1qq3xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
10324    ///         amount: 50.0,
10325    ///     },
10326    /// ];
10327    ///
10328    /// let distribution_response = client.create_distribution(asset_uuid, assignments).await?;
10329    /// println!("Created distribution: {}", distribution_response.distribution_uuid);
10330    /// println!("Asset ID: {}", distribution_response.asset_id);
10331    /// # Ok(())
10332    /// # }
10333    /// ```
10334    ///
10335    /// # Related Methods
10336    /// - [`get_asset_assignments`](Self::get_asset_assignments) - List assignments for an asset
10337    /// - [`create_asset_assignments`](Self::create_asset_assignments) - Create new assignments
10338    #[allow(clippy::cognitive_complexity, clippy::too_many_lines)]
10339    pub async fn create_distribution(
10340        &self,
10341        asset_uuid: &str,
10342        assignments: Vec<crate::model::AssetDistributionAssignment>,
10343    ) -> Result<crate::model::DistributionResponse, AmpError> {
10344        use crate::model::{CreateDistributionRequest, DistributionAssignmentRequest};
10345
10346        let create_span = tracing::debug_span!(
10347            "create_distribution",
10348            asset_uuid = %asset_uuid,
10349            assignment_count = assignments.len()
10350        );
10351        let _enter = create_span.enter();
10352
10353        tracing::debug!(
10354            "Creating distribution for asset {} with {} assignments",
10355            asset_uuid,
10356            assignments.len()
10357        );
10358
10359        // Validate inputs
10360        if asset_uuid.is_empty() {
10361            tracing::error!("Distribution creation failed: empty asset UUID");
10362            return Err(AmpError::validation("Asset UUID cannot be empty"));
10363        }
10364
10365        if assignments.is_empty() {
10366            tracing::error!("Distribution creation failed: empty assignments");
10367            return Err(AmpError::validation("Assignments cannot be empty"));
10368        }
10369
10370        // Convert AssetDistributionAssignment to DistributionAssignmentRequest
10371        // The API expects user_uuid field, but our input uses user_id
10372        tracing::trace!("Converting {} assignments to API format", assignments.len());
10373        let mut total_amount = 0.0;
10374        let api_assignments: Vec<DistributionAssignmentRequest> = assignments
10375            .into_iter()
10376            .enumerate()
10377            .map(
10378                #[allow(clippy::cognitive_complexity)]
10379                |(index, assignment)| {
10380                    tracing::trace!(
10381                        "Converting assignment {}: user_id={}, address={}, amount={}",
10382                        index,
10383                        assignment.user_id,
10384                        assignment.address,
10385                        assignment.amount
10386                    );
10387
10388                    // Validate assignment data
10389                    if assignment.user_id.is_empty() {
10390                        tracing::error!("Assignment {} has empty user_id", index);
10391                        return Err(AmpError::validation(format!(
10392                            "Assignment {index} has empty user_id"
10393                        )));
10394                    }
10395                    if assignment.address.is_empty() {
10396                        tracing::error!("Assignment {} has empty address", index);
10397                        return Err(AmpError::validation(format!(
10398                            "Assignment {index} has empty address"
10399                        )));
10400                    }
10401                    if assignment.amount <= 0.0 {
10402                        tracing::error!(
10403                            "Assignment {} has non-positive amount: {}",
10404                            index,
10405                            assignment.amount
10406                        );
10407                        return Err(AmpError::validation(format!(
10408                            "Assignment {} has non-positive amount: {}",
10409                            index, assignment.amount
10410                        )));
10411                    }
10412
10413                    total_amount += assignment.amount;
10414
10415                    Ok(DistributionAssignmentRequest {
10416                        user_uuid: assignment.user_id, // Map user_id to user_uuid for API
10417                        amount: assignment.amount,
10418                        address: assignment.address,
10419                    })
10420                },
10421            )
10422            .collect::<Result<Vec<_>, AmpError>>()?;
10423
10424        tracing::debug!(
10425            "Converted {} assignments successfully, total amount: {}",
10426            api_assignments.len(),
10427            total_amount
10428        );
10429
10430        let request = CreateDistributionRequest {
10431            assignments: api_assignments,
10432        };
10433
10434        tracing::debug!("Sending distribution creation request to AMP API");
10435        let api_call_start = std::time::Instant::now();
10436
10437        // Make the API call
10438        let response: crate::model::DistributionResponse = self
10439            .request_json(
10440                Method::GET,
10441                &["assets", asset_uuid, "distributions", "create"],
10442                Some(&request),
10443            )
10444            .await
10445            .map_err(
10446                #[allow(clippy::cognitive_complexity)]
10447                |e| {
10448                    let api_call_duration = api_call_start.elapsed();
10449                    let error_msg =
10450                        format!("Failed to create distribution after {api_call_duration:?}: {e}");
10451                    tracing::error!("{}", error_msg);
10452
10453                    // Check for specific API error patterns
10454                    let error_str = e.to_string();
10455                    if error_str.contains("404") || error_str.contains("not found") {
10456                        tracing::error!(
10457                            "Asset {} not found - verify asset UUID is correct",
10458                            asset_uuid
10459                        );
10460                    } else if error_str.contains("400") || error_str.contains("bad request") {
10461                        tracing::error!("Bad request - check assignment data format and values");
10462                    } else if error_str.contains("401") || error_str.contains("unauthorized") {
10463                        tracing::error!("Unauthorized - check API credentials and token validity");
10464                    } else if error_str.contains("403") || error_str.contains("forbidden") {
10465                        tracing::error!("Forbidden - check permissions for asset distribution");
10466                    } else if error_str.contains("429") || error_str.contains("rate limit") {
10467                        tracing::error!("Rate limited - wait before retrying");
10468                    } else if error_str.contains("500") || error_str.contains("internal server") {
10469                        tracing::error!(
10470                            "Server error - this may be a temporary issue, retry may help"
10471                        );
10472                    }
10473
10474                    AmpError::api(error_msg)
10475                },
10476            )?;
10477
10478        let api_call_duration = api_call_start.elapsed();
10479        tracing::info!(
10480            "Successfully created distribution: {} (took {:?})",
10481            response.distribution_uuid,
10482            api_call_duration
10483        );
10484
10485        // Validate response data
10486        if response.distribution_uuid.is_empty() {
10487            tracing::error!("API returned empty distribution UUID");
10488            return Err(AmpError::api("API returned empty distribution UUID"));
10489        }
10490
10491        if response.asset_id.is_empty() {
10492            tracing::error!("API returned empty asset ID");
10493            return Err(AmpError::api("API returned empty asset ID"));
10494        }
10495
10496        if response.map_address_amount.is_empty() {
10497            tracing::error!("API returned empty address mapping");
10498            return Err(AmpError::api("API returned empty address mapping"));
10499        }
10500
10501        tracing::debug!(
10502            "Distribution response validated - {} addresses mapped, asset_id: {}",
10503            response.map_address_amount.len(),
10504            response.asset_id
10505        );
10506
10507        Ok(response)
10508    }
10509
10510    /// Confirms a distribution with transaction and change data.
10511    ///
10512    /// This method submits the final confirmation for a distribution by providing
10513    /// the transaction details and any change UTXOs to the AMP API. This completes
10514    /// the distribution workflow after the transaction has been broadcast and confirmed
10515    /// on the blockchain.
10516    ///
10517    /// # Arguments
10518    /// * `asset_uuid` - The UUID of the asset being distributed
10519    /// * `distribution_uuid` - The UUID of the distribution to confirm (from `create_distribution` response)
10520    /// * `tx_data` - Transaction data containing details and txid from the blockchain
10521    /// * `change_data` - Vector of change UTXOs from the transaction
10522    ///
10523    /// # Errors
10524    /// Returns an error if:
10525    /// - Authentication fails
10526    /// - The asset UUID or distribution UUID is invalid
10527    /// - The transaction data is invalid or incomplete
10528    /// - The HTTP request fails
10529    /// - The server returns an error status
10530    /// - The response cannot be parsed
10531    ///
10532    /// # Examples
10533    /// ```no_run
10534    /// # use amp_rs::{ApiClient, model::{AmpTxData, Unspent}, AmpError};
10535    /// # #[tokio::main]
10536    /// # async fn main() -> Result<(), AmpError> {
10537    /// # let client = ApiClient::new().await?;
10538    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
10539    /// let distribution_uuid = "dist-550e8400-e29b-41d4-a716-446655440000";
10540    ///
10541    /// // Transaction data for AMP API confirmation
10542    /// let tx_data = AmpTxData {
10543    ///     details: serde_json::json!([{
10544    ///         "account": "",
10545    ///         "address": "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq",
10546    ///         "category": "send",
10547    ///         "amount": -100.0,
10548    ///         "vout": 0,
10549    ///         "fee": -0.001
10550    ///     }]),
10551    ///     txid: "abc123def456...".to_string(),
10552    /// };
10553    ///
10554    /// // Change UTXOs from Elements node listunspent call
10555    /// let change_data = vec![
10556    ///     Unspent {
10557    ///         txid: "abc123def456...".to_string(),
10558    ///         vout: 1,
10559    ///         amount: 25.0,
10560    ///         asset: "asset_id_hex".to_string(),
10561    ///         address: "change_address".to_string(),
10562    ///         spendable: true,
10563    ///         confirmations: Some(2),
10564    ///         scriptpubkey: Some("76a914...88ac".to_string()),
10565    ///         redeemscript: None,
10566    ///         witnessscript: None,
10567    ///         amountblinder: None,
10568    ///         assetblinder: None,
10569    ///     }
10570    /// ];
10571    ///
10572    /// client.confirm_distribution(asset_uuid, distribution_uuid, tx_data, change_data).await?;
10573    /// println!("Distribution confirmed successfully");
10574    /// # Ok(())
10575    /// # }
10576    /// ```
10577    ///
10578    /// # Related Methods
10579    /// - [`create_distribution`](Self::create_distribution) - Create a new distribution
10580    /// - [`get_asset_assignments`](Self::get_asset_assignments) - List assignments for an asset
10581    #[allow(clippy::cognitive_complexity, clippy::too_many_lines)]
10582    pub async fn confirm_distribution(
10583        &self,
10584        asset_uuid: &str,
10585        distribution_uuid: &str,
10586        tx_data: crate::model::AmpTxData,
10587        change_data: Vec<crate::model::Unspent>,
10588    ) -> Result<(), AmpError> {
10589        use crate::model::ConfirmDistributionRequest;
10590
10591        let confirm_span = tracing::debug_span!(
10592            "confirm_distribution",
10593            asset_uuid = %asset_uuid,
10594            distribution_uuid = %distribution_uuid,
10595            txid = %tx_data.txid,
10596            change_count = change_data.len()
10597        );
10598        let _enter = confirm_span.enter();
10599
10600        tracing::debug!(
10601            "Confirming distribution {} for asset {} with txid {} ({} change UTXOs)",
10602            distribution_uuid,
10603            asset_uuid,
10604            tx_data.txid,
10605            change_data.len()
10606        );
10607
10608        // Validate inputs
10609        if asset_uuid.is_empty() {
10610            tracing::error!("Distribution confirmation failed: empty asset UUID");
10611            return Err(AmpError::validation("Asset UUID cannot be empty"));
10612        }
10613
10614        if distribution_uuid.is_empty() {
10615            tracing::error!("Distribution confirmation failed: empty distribution UUID");
10616            return Err(AmpError::validation("Distribution UUID cannot be empty"));
10617        }
10618
10619        if tx_data.txid.is_empty() {
10620            tracing::error!("Distribution confirmation failed: empty transaction ID");
10621            return Err(AmpError::validation("Transaction ID cannot be empty"));
10622        }
10623
10624        // Log transaction details for debugging
10625        tracing::debug!("Transaction details array: {:?}", tx_data.details);
10626
10627        // Log change data details
10628        if change_data.is_empty() {
10629            tracing::debug!("No change UTXOs to include in confirmation");
10630        } else {
10631            let total_change: f64 = change_data.iter().map(|utxo| utxo.amount).sum();
10632            tracing::debug!(
10633                "Change data - {} UTXOs, total amount: {}",
10634                change_data.len(),
10635                total_change
10636            );
10637
10638            for (i, utxo) in change_data.iter().enumerate() {
10639                tracing::trace!(
10640                    "Change UTXO {}: txid={}, vout={}, amount={}, spendable={}",
10641                    i,
10642                    utxo.txid,
10643                    utxo.vout,
10644                    utxo.amount,
10645                    utxo.spendable
10646                );
10647            }
10648        }
10649
10650        let request = ConfirmDistributionRequest {
10651            tx_data: tx_data.clone(),
10652            change_data: change_data.clone(),
10653        };
10654
10655        tracing::debug!("Sending distribution confirmation request to AMP API");
10656        let api_call_start = std::time::Instant::now();
10657
10658        // Make the API call
10659        self.request_empty(
10660            Method::POST,
10661            &["assets", asset_uuid, "distributions", distribution_uuid, "confirm"],
10662            Some(&request),
10663        )
10664        .await
10665        .map_err(#[allow(clippy::cognitive_complexity)] |e| {
10666            let api_call_duration = api_call_start.elapsed();
10667            let error_msg = format!(
10668                "Failed to confirm distribution {} after {:?}: {}. IMPORTANT: Transaction {} was successful on blockchain. Use this txid to manually retry confirmation.",
10669                distribution_uuid, api_call_duration, e, tx_data.txid
10670            );
10671            tracing::error!("{}", error_msg);
10672
10673            // Check for specific API error patterns
10674            let error_str = e.to_string();
10675            if error_str.contains("404") || error_str.contains("not found") {
10676                tracing::error!("Distribution {} not found - verify distribution UUID is correct", distribution_uuid);
10677            } else if error_str.contains("400") || error_str.contains("bad request") {
10678                tracing::error!("Bad request - check transaction data format and change data");
10679            } else if error_str.contains("409") || error_str.contains("conflict") {
10680                tracing::error!("Conflict - distribution may already be confirmed");
10681            } else if error_str.contains("422") || error_str.contains("unprocessable") {
10682                tracing::error!("Unprocessable entity - check transaction confirmations and data validity");
10683            } else if error_str.contains("500") || error_str.contains("internal server") {
10684                tracing::error!("Server error - this may be a temporary issue, retry with txid: {}", tx_data.txid);
10685            }
10686
10687            AmpError::api(error_msg)
10688        })?;
10689
10690        let api_call_duration = api_call_start.elapsed();
10691        tracing::info!(
10692            "Successfully confirmed distribution: {} for asset: {} with txid: {} (took {:?})",
10693            distribution_uuid,
10694            asset_uuid,
10695            tx_data.txid,
10696            api_call_duration
10697        );
10698
10699        Ok(())
10700    }
10701
10702    /// Cancels an in-progress distribution for an asset.
10703    ///
10704    /// This method cancels a distribution that is currently in progress (unconfirmed status).
10705    /// Once a distribution is cancelled, it cannot be confirmed and the assigned amounts
10706    /// become available for new distributions.
10707    ///
10708    /// # Arguments
10709    /// * `asset_uuid` - The UUID of the asset
10710    /// * `distribution_uuid` - The UUID of the distribution to cancel
10711    ///
10712    /// # Returns
10713    /// Returns `Ok(())` if the distribution was successfully cancelled.
10714    ///
10715    /// # Errors
10716    /// Returns an error if:
10717    /// - Authentication fails
10718    /// - The HTTP request fails
10719    /// - The server returns an error status
10720    /// - The distribution is not found
10721    /// - The distribution is already confirmed and cannot be cancelled
10722    ///
10723    /// # Examples
10724    /// ```no_run
10725    /// use amp_rs::ApiClient;
10726    ///
10727    /// #[tokio::main]
10728    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
10729    ///     let client = ApiClient::new().await?;
10730    ///     
10731    ///     client.cancel_distribution(
10732    ///         "asset-uuid-123",
10733    ///         "distribution-uuid-456"
10734    ///     ).await?;
10735    ///     
10736    ///     println!("Distribution cancelled successfully");
10737    ///     Ok(())
10738    /// # }
10739    /// ```
10740    #[allow(clippy::cognitive_complexity)]
10741    pub async fn cancel_distribution(
10742        &self,
10743        asset_uuid: &str,
10744        distribution_uuid: &str,
10745    ) -> Result<(), AmpError> {
10746        let cancel_span = tracing::debug_span!(
10747            "cancel_distribution",
10748            asset_uuid = %asset_uuid,
10749            distribution_uuid = %distribution_uuid
10750        );
10751        let _enter = cancel_span.enter();
10752
10753        tracing::debug!(
10754            "Cancelling distribution {} for asset {}",
10755            distribution_uuid,
10756            asset_uuid
10757        );
10758
10759        // Validate inputs
10760        if asset_uuid.is_empty() {
10761            tracing::error!("Distribution cancellation failed: empty asset UUID");
10762            return Err(AmpError::validation("Asset UUID cannot be empty"));
10763        }
10764
10765        if distribution_uuid.is_empty() {
10766            tracing::error!("Distribution cancellation failed: empty distribution UUID");
10767            return Err(AmpError::validation("Distribution UUID cannot be empty"));
10768        }
10769
10770        let api_call_start = std::time::Instant::now();
10771
10772        self.request_empty(
10773            Method::DELETE,
10774            &[
10775                "assets",
10776                asset_uuid,
10777                "distributions",
10778                distribution_uuid,
10779                "cancel",
10780            ],
10781            None::<&()>,
10782        )
10783        .await
10784        .map_err(|e| {
10785            let api_call_duration = api_call_start.elapsed();
10786            let error_msg = format!(
10787                "Failed to cancel distribution {distribution_uuid} for asset {asset_uuid} after {api_call_duration:?}: {e}"
10788            );
10789            tracing::error!("{}", error_msg);
10790
10791            // Check for specific API error patterns
10792            let error_str = e.to_string();
10793            if error_str.contains("404") || error_str.contains("not found") {
10794                tracing::error!(
10795                    "Distribution {} not found - verify distribution UUID is correct",
10796                    distribution_uuid
10797                );
10798            } else if error_str.contains("400") || error_str.contains("bad request") {
10799                tracing::error!("Bad request - distribution may already be confirmed or invalid");
10800            } else if error_str.contains("409") || error_str.contains("conflict") {
10801                tracing::error!(
10802                    "Conflict - distribution may already be confirmed and cannot be cancelled"
10803                );
10804            } else if error_str.contains("422") || error_str.contains("unprocessable") {
10805                tracing::error!(
10806                    "Unprocessable entity - distribution is in a state that cannot be cancelled"
10807                );
10808            }
10809
10810            AmpError::api(error_msg)
10811        })?;
10812
10813        let api_call_duration = api_call_start.elapsed();
10814        tracing::info!(
10815            "Successfully cancelled distribution: {} for asset: {} (took {:?})",
10816            distribution_uuid,
10817            asset_uuid,
10818            api_call_duration
10819        );
10820
10821        Ok(())
10822    }
10823
10824    /// Gets all distributions for a specific asset.
10825    ///
10826    /// This method retrieves all distributions (both confirmed and unconfirmed) for the specified asset.
10827    /// This is useful for checking if there are any in-progress distributions before deleting an asset.
10828    ///
10829    /// # Arguments
10830    /// * `asset_uuid` - The UUID of the asset to get distributions for
10831    ///
10832    /// # Returns
10833    /// Returns a vector of `Distribution` objects for the asset.
10834    ///
10835    /// # Errors
10836    /// Returns an error if:
10837    /// - Authentication fails
10838    /// - The HTTP request fails
10839    /// - The server returns an error status
10840    /// - The response cannot be parsed
10841    ///
10842    /// # Examples
10843    /// ```no_run
10844    /// use amp_rs::ApiClient;
10845    ///
10846    /// #[tokio::main]
10847    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
10848    ///     let client = ApiClient::new().await?;
10849    ///     
10850    ///     let distributions = client.get_asset_distributions("asset-uuid-123").await?;
10851    ///     
10852    ///     for distribution in distributions {
10853    ///         println!("Distribution: {} - Status: {:?}",
10854    ///                  distribution.distribution_uuid,
10855    ///                  distribution.distribution_status);
10856    ///     }
10857    ///     Ok(())
10858    /// }
10859    /// ```
10860    pub async fn get_asset_distributions(
10861        &self,
10862        asset_uuid: &str,
10863    ) -> Result<Vec<crate::model::Distribution>, Error> {
10864        let distributions_span = tracing::debug_span!(
10865            "get_asset_distributions",
10866            asset_uuid = %asset_uuid
10867        );
10868        let _enter = distributions_span.enter();
10869
10870        tracing::debug!("Getting distributions for asset {}", asset_uuid);
10871
10872        // Validate input
10873        if asset_uuid.is_empty() {
10874            tracing::error!("Get distributions failed: empty asset UUID");
10875            return Err(Error::RequestFailed(
10876                "Asset UUID cannot be empty".to_string(),
10877            ));
10878        }
10879
10880        self.request_json(
10881            Method::GET,
10882            &["assets", asset_uuid, "distributions"],
10883            None::<&()>,
10884        )
10885        .await
10886    }
10887
10888    /// Gets a specific distribution by UUID for an asset.
10889    ///
10890    /// This method retrieves detailed information about a specific distribution,
10891    /// including its status, UUID, and associated transactions.
10892    ///
10893    /// # Arguments
10894    /// * `asset_uuid` - The UUID of the asset
10895    /// * `distribution_uuid` - The UUID of the distribution to retrieve
10896    ///
10897    /// # Returns
10898    /// Returns a `Distribution` struct containing:
10899    /// - `distribution_uuid` - The unique identifier for the distribution
10900    /// - `distribution_status` - Current status of the distribution
10901    /// - `transactions` - List of transactions associated with the distribution
10902    ///
10903    /// # Errors
10904    /// Returns an error if:
10905    /// - Authentication fails
10906    /// - The HTTP request fails
10907    /// - The server returns an error status
10908    /// - The response cannot be parsed as JSON
10909    /// - The asset UUID or distribution UUID is empty
10910    ///
10911    /// # Examples
10912    /// ```no_run
10913    /// # use amp_rs::ApiClient;
10914    /// # #[tokio::main]
10915    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
10916    /// let client = ApiClient::new().await?;
10917    ///
10918    /// let distribution = client.get_asset_distribution(
10919    ///     "asset-uuid-123",
10920    ///     "distribution-uuid-456"
10921    /// ).await?;
10922    ///
10923    /// println!("Distribution: {} - Status: {:?}",
10924    ///          distribution.distribution_uuid,
10925    ///          distribution.distribution_status);
10926    /// # Ok(())
10927    /// # }
10928    /// ```
10929    ///
10930    /// # Related Methods
10931    /// - [`get_asset_distributions`](Self::get_asset_distributions) - List all distributions for an asset
10932    /// - [`create_distribution`](Self::create_distribution) - Create a new distribution
10933    /// - [`confirm_distribution`](Self::confirm_distribution) - Confirm a distribution
10934    /// - [`cancel_distribution`](Self::cancel_distribution) - Cancel a distribution
10935    #[allow(clippy::cognitive_complexity)]
10936    pub async fn get_asset_distribution(
10937        &self,
10938        asset_uuid: &str,
10939        distribution_uuid: &str,
10940    ) -> Result<crate::model::Distribution, Error> {
10941        let distribution_span = tracing::debug_span!(
10942            "get_asset_distribution",
10943            asset_uuid = %asset_uuid,
10944            distribution_uuid = %distribution_uuid
10945        );
10946        let _enter = distribution_span.enter();
10947
10948        tracing::debug!(
10949            "Getting distribution {} for asset {}",
10950            distribution_uuid,
10951            asset_uuid
10952        );
10953
10954        // Validate inputs
10955        if asset_uuid.is_empty() {
10956            tracing::error!("Get distribution failed: empty asset UUID");
10957            return Err(Error::RequestFailed(
10958                "Asset UUID cannot be empty".to_string(),
10959            ));
10960        }
10961
10962        if distribution_uuid.is_empty() {
10963            tracing::error!("Get distribution failed: empty distribution UUID");
10964            return Err(Error::RequestFailed(
10965                "Distribution UUID cannot be empty".to_string(),
10966            ));
10967        }
10968
10969        self.request_json(
10970            Method::GET,
10971            &["assets", asset_uuid, "distributions", distribution_uuid],
10972            None::<&()>,
10973        )
10974        .await
10975    }
10976
10977    /// Gets a specific manager by ID.
10978    ///
10979    /// # Arguments
10980    /// * `manager_id` - The ID of the manager to retrieve
10981    ///
10982    /// # Errors
10983    /// Returns an error if:
10984    /// - Authentication fails
10985    /// - The HTTP request fails
10986    /// - The server returns an error status
10987    /// - The response cannot be parsed as JSON
10988    pub async fn get_manager(&self, manager_id: i64) -> Result<crate::model::Manager, Error> {
10989        self.request_json(
10990            Method::GET,
10991            &["managers", &manager_id.to_string()],
10992            None::<&()>,
10993        )
10994        .await
10995    }
10996
10997    /// Removes a manager's permissions to modify a specific asset.
10998    ///
10999    /// This method revokes a manager's access to a specific asset, preventing them from
11000    /// performing asset management operations such as creating assignments, managing ownership,
11001    /// or modifying asset properties. The manager will no longer be able to access this asset
11002    /// through their management interface.
11003    ///
11004    /// # Arguments
11005    /// * `manager_id` - The ID of the manager to remove permissions from
11006    /// * `asset_uuid` - The UUID of the asset to remove permissions for
11007    ///
11008    /// # Returns
11009    /// Returns `Ok(())` on successful permission removal.
11010    ///
11011    /// # Errors
11012    /// Returns an error if:
11013    /// - Authentication fails or insufficient permissions
11014    /// - The manager ID is invalid or does not exist
11015    /// - The asset UUID is invalid or does not exist
11016    /// - The manager does not currently have permissions for this asset
11017    /// - The HTTP request fails
11018    /// - The server returns an error status
11019    ///
11020    /// # Examples
11021    /// ```no_run
11022    /// # use amp_rs::ApiClient;
11023    /// # #[tokio::main]
11024    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
11025    /// let client = ApiClient::new().await?;
11026    ///
11027    /// let manager_id = 123;
11028    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
11029    ///
11030    /// client.manager_remove_asset(manager_id, asset_uuid).await?;
11031    /// println!("Removed asset {} from manager {}", asset_uuid, manager_id);
11032    /// # Ok(())
11033    /// # }
11034    /// ```
11035    ///
11036    /// # Related Methods
11037    /// - [`add_asset_to_manager`](Self::add_asset_to_manager) - Grant manager permissions for an asset
11038    /// - [`get_manager`](Self::get_manager) - Get manager information including current assets
11039    /// - [`revoke_manager`](Self::revoke_manager) - Remove all asset permissions from manager
11040    /// - [`lock_manager`](Self::lock_manager) - Lock manager account
11041    pub async fn manager_remove_asset(
11042        &self,
11043        manager_id: i64,
11044        asset_uuid: &str,
11045    ) -> Result<(), Error> {
11046        self.request_empty(
11047            Method::POST,
11048            &[
11049                "managers",
11050                &manager_id.to_string(),
11051                "assets",
11052                asset_uuid,
11053                "remove",
11054            ],
11055            None::<&()>,
11056        )
11057        .await
11058    }
11059
11060    /// Revokes all asset permissions for a manager.
11061    ///
11062    /// This method first retrieves the manager's current asset permissions,
11063    /// then removes the manager's access to each asset they currently have access to.
11064    ///
11065    /// # Arguments
11066    /// * `manager_id` - The ID of the manager to revoke permissions for
11067    ///
11068    /// # Errors
11069    /// Returns an error if:
11070    /// - Authentication fails
11071    /// - The HTTP request fails
11072    /// - The server returns an error status
11073    /// - Any individual asset removal fails
11074    pub async fn revoke_manager(&self, manager_id: i64) -> Result<(), Error> {
11075        // First, get the manager to see which assets they have access to
11076        let manager = self.get_manager(manager_id).await?;
11077
11078        // Remove the manager's access to each asset
11079        for asset_uuid in &manager.assets {
11080            self.manager_remove_asset(manager_id, asset_uuid).await?;
11081        }
11082
11083        Ok(())
11084    }
11085
11086    /// Gets the current manager information as raw JSON.
11087    ///
11088    /// This method calls the `/managers/me` endpoint to retrieve information
11089    /// about the currently authenticated manager.
11090    ///
11091    /// # Errors
11092    /// Returns an error if:
11093    /// - Authentication fails
11094    /// - The HTTP request fails
11095    /// - The server returns an error status
11096    /// - The response cannot be parsed as JSON
11097    pub async fn get_current_manager_raw(&self) -> Result<serde_json::Value, Error> {
11098        self.request_json(Method::GET, &["managers", "me"], None::<&()>)
11099            .await
11100    }
11101
11102    /// Locks a manager account to prevent further operations.
11103    ///
11104    /// This method sends a PUT request to lock the specified manager, preventing any further
11105    /// operations on that manager account. This is typically used for security purposes or
11106    /// when a manager needs to be temporarily disabled.
11107    ///
11108    /// # Arguments
11109    /// * `manager_id` - The ID of the manager to lock
11110    ///
11111    /// # Returns
11112    /// Returns `Ok(())` if the manager was successfully locked.
11113    ///
11114    /// # Errors
11115    /// Returns an error if:
11116    /// - Authentication fails
11117    /// - The HTTP request fails
11118    /// - The server returns an error status
11119    /// - The manager ID is invalid or does not exist
11120    /// - The manager is already locked
11121    ///
11122    /// # Example
11123    /// ```no_run
11124    /// # use amp_rs::ApiClient;
11125    /// # #[tokio::main]
11126    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
11127    /// let client = ApiClient::new().await?;
11128    ///
11129    /// // Lock manager with ID 123
11130    /// client.lock_manager(123).await?;
11131    /// println!("Manager 123 has been locked successfully");
11132    /// # Ok(())
11133    /// # }
11134    /// ```
11135    pub async fn lock_manager(&self, manager_id: i64) -> Result<(), Error> {
11136        self.request_empty(
11137            Method::PUT,
11138            &["managers", &manager_id.to_string(), "lock"],
11139            None::<&()>,
11140        )
11141        .await
11142    }
11143
11144    /// Unlocks a manager account.
11145    ///
11146    /// # Arguments
11147    /// * `manager_id` - The ID of the manager to unlock
11148    ///
11149    /// # Errors
11150    /// Returns an error if:
11151    /// - Authentication fails
11152    /// - The HTTP request fails
11153    /// - The server returns an error status
11154    pub async fn unlock_manager(&self, manager_id: i64) -> Result<(), Error> {
11155        self.request_empty(
11156            Method::PUT,
11157            &["managers", &manager_id.to_string(), "unlock"],
11158            None::<&()>,
11159        )
11160        .await
11161    }
11162
11163    /// Authorizes a manager to manage a specific asset.
11164    ///
11165    /// This method sends a PUT request to authorize the specified manager to manage the given asset.
11166    /// Once authorized, the manager will have permissions to perform operations on the asset such as
11167    /// creating assignments, managing ownership, and other asset-related operations.
11168    ///
11169    /// # Arguments
11170    /// * `manager_id` - The ID of the manager to authorize
11171    /// * `asset_uuid` - The UUID of the asset to add to the manager's authorized assets
11172    ///
11173    /// # Returns
11174    /// Returns `Ok(())` if the manager was successfully authorized for the asset.
11175    ///
11176    /// # Errors
11177    /// Returns an error if:
11178    /// - Authentication fails or insufficient permissions
11179    /// - The HTTP request fails
11180    /// - The server returns an error status
11181    /// - The manager ID is invalid or does not exist
11182    /// - The asset UUID is invalid or does not exist
11183    /// - The manager is already authorized for this asset
11184    /// - The manager is locked and cannot be modified
11185    ///
11186    /// # Examples
11187    /// ```no_run
11188    /// # use amp_rs::ApiClient;
11189    /// # #[tokio::main]
11190    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
11191    /// let client = ApiClient::new().await?;
11192    ///
11193    /// // Authorize manager 123 to manage asset with UUID "550e8400-e29b-41d4-a716-446655440000"
11194    /// let manager_id = 123;
11195    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
11196    ///
11197    /// client.add_asset_to_manager(manager_id, asset_uuid).await?;
11198    /// println!("Manager {} is now authorized to manage asset {}", manager_id, asset_uuid);
11199    /// # Ok(())
11200    /// # }
11201    /// ```
11202    ///
11203    /// # Related Methods
11204    /// - [`manager_remove_asset`](Self::manager_remove_asset) - Remove manager permissions for an asset
11205    /// - [`get_manager`](Self::get_manager) - Get manager information including current assets
11206    /// - [`get_manager_permissions`](Self::get_manager_permissions) - Get manager's current permissions
11207    /// - [`lock_manager`](Self::lock_manager) - Lock manager account
11208    pub async fn add_asset_to_manager(
11209        &self,
11210        manager_id: i64,
11211        asset_uuid: &str,
11212    ) -> Result<(), Error> {
11213        self.request_empty(
11214            Method::PUT,
11215            &[
11216                "managers",
11217                &manager_id.to_string(),
11218                "assets",
11219                asset_uuid,
11220                "add",
11221            ],
11222            None::<&()>,
11223        )
11224        .await
11225    }
11226
11227    /// Deletes a specific asset assignment.
11228    ///
11229    /// # Arguments
11230    /// * `asset_uuid` - The UUID of the asset
11231    /// * `assignment_id` - The ID of the assignment to delete
11232    ///
11233    /// # Errors
11234    /// Returns an error if:
11235    /// - Authentication fails
11236    /// - The HTTP request fails
11237    /// - The server returns an error status
11238    ///   Removes an asset assignment.
11239    ///
11240    /// This method permanently deletes an asset assignment, returning the allocated assets
11241    /// back to the available pool. This operation cannot be undone. If the assignment has
11242    /// already been distributed, this operation may fail.
11243    ///
11244    /// # Arguments
11245    /// * `asset_uuid` - The UUID of the asset containing the assignment
11246    /// * `assignment_id` - The ID of the assignment to delete
11247    ///
11248    /// # Returns
11249    /// Returns `Ok(())` on successful deletion.
11250    ///
11251    /// # Errors
11252    /// Returns an error if:
11253    /// - Authentication fails or insufficient permissions
11254    /// - The asset UUID is invalid or does not exist
11255    /// - The assignment ID is invalid or does not exist
11256    /// - The assignment has already been distributed and cannot be deleted
11257    /// - The assignment is locked and cannot be modified
11258    /// - The HTTP request fails
11259    /// - The server returns an error status
11260    ///
11261    /// # Examples
11262    /// ```no_run
11263    /// # use amp_rs::ApiClient;
11264    /// # #[tokio::main]
11265    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
11266    /// let client = ApiClient::new().await?;
11267    ///
11268    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
11269    /// let assignment_id = "123";
11270    ///
11271    /// client.delete_asset_assignment(asset_uuid, assignment_id).await?;
11272    /// println!("Successfully deleted assignment {}", assignment_id);
11273    /// # Ok(())
11274    /// # }
11275    /// ```
11276    ///
11277    /// # Related Methods
11278    /// - [`get_asset_assignment`](Self::get_asset_assignment) - Get assignment details before deletion
11279    /// - [`create_asset_assignments`](Self::create_asset_assignments) - Create new assignments
11280    /// - [`edit_asset_assignment`](Self::edit_asset_assignment) - Update assignment instead of deleting
11281    /// - [`lock_asset_assignment`](Self::lock_asset_assignment) - Lock assignment to prevent changes
11282    pub async fn delete_asset_assignment(
11283        &self,
11284        asset_uuid: &str,
11285        assignment_id: &str,
11286    ) -> Result<(), Error> {
11287        self.request_empty(
11288            Method::DELETE,
11289            &["assets", asset_uuid, "assignments", assignment_id, "delete"],
11290            None::<&()>,
11291        )
11292        .await
11293    }
11294
11295    /// Locks a specific asset assignment.
11296    ///
11297    /// # Arguments
11298    /// * `asset_uuid` - The UUID of the asset
11299    /// * `assignment_id` - The ID of the assignment to lock
11300    ///
11301    /// # Errors
11302    /// Returns an error if:
11303    /// - Authentication fails
11304    /// - The HTTP request fails
11305    /// - The server returns an error status
11306    pub async fn lock_asset_assignment(
11307        &self,
11308        asset_uuid: &str,
11309        assignment_id: &str,
11310    ) -> Result<Assignment, Error> {
11311        self.request_json(
11312            Method::PUT,
11313            &["assets", asset_uuid, "assignments", assignment_id, "lock"],
11314            None::<&()>,
11315        )
11316        .await
11317    }
11318
11319    /// Unlocks a specific asset assignment.
11320    ///
11321    /// # Arguments
11322    /// * `asset_uuid` - The UUID of the asset
11323    /// * `assignment_id` - The ID of the assignment to unlock
11324    ///
11325    /// # Errors
11326    /// Returns an error if:
11327    /// - Authentication fails
11328    /// - The HTTP request fails
11329    /// - The server returns an error status
11330    pub async fn unlock_asset_assignment(
11331        &self,
11332        asset_uuid: &str,
11333        assignment_id: &str,
11334    ) -> Result<Assignment, Error> {
11335        self.request_json(
11336            Method::PUT,
11337            &["assets", asset_uuid, "assignments", assignment_id, "unlock"],
11338            None::<&()>,
11339        )
11340        .await
11341    }
11342
11343    /// Adds categories to a registered user.
11344    ///
11345    /// # Arguments
11346    /// * `registered_user_id` - The ID of the registered user
11347    /// * `categories` - A slice of category IDs to add to the user
11348    ///
11349    /// # Errors
11350    /// Returns an error if:
11351    /// - Authentication fails
11352    /// - The HTTP request fails
11353    /// - The server returns an error status
11354    /// - The registered user ID is invalid
11355    /// - Any category ID is invalid
11356    pub async fn add_categories_to_registered_user(
11357        &self,
11358        registered_user_id: i64,
11359        categories: &[i64],
11360    ) -> Result<(), Error> {
11361        let request_body = CategoriesRequest {
11362            categories: categories.to_vec(),
11363        };
11364
11365        self.request_empty(
11366            Method::PUT,
11367            &[
11368                "registered_users",
11369                &registered_user_id.to_string(),
11370                "categories",
11371                "add",
11372            ],
11373            Some(request_body),
11374        )
11375        .await
11376    }
11377
11378    /// Removes categories from a registered user
11379    ///
11380    /// # Arguments
11381    /// * `registered_user_id` - The ID of the registered user
11382    /// * `categories` - A slice of category IDs to remove from the user
11383    ///
11384    /// # Returns
11385    /// Returns `Ok(())` if the categories are successfully removed, or an error if:
11386    /// - Authentication fails
11387    /// - The HTTP request fails
11388    /// - The server returns an error status
11389    /// - The registered user ID is invalid
11390    /// - Any category ID is not associated with the user
11391    pub async fn remove_categories_from_registered_user(
11392        &self,
11393        registered_user_id: i64,
11394        categories: &[i64],
11395    ) -> Result<(), Error> {
11396        let request_body = CategoriesRequest {
11397            categories: categories.to_vec(),
11398        };
11399
11400        self.request_empty(
11401            Method::PUT,
11402            &[
11403                "registered_users",
11404                &registered_user_id.to_string(),
11405                "categories",
11406                "delete",
11407            ],
11408            Some(request_body),
11409        )
11410        .await
11411    }
11412
11413    /// Distributes assets to multiple users through a comprehensive workflow
11414    ///
11415    /// This method orchestrates the complete asset distribution process:
11416    /// 1. Validates input parameters (asset UUID format, assignments structure)
11417    /// 2. Verifies `ElementsRpc` connection and signer interface availability
11418    /// 3. Authenticates with the AMP API using the client's token
11419    /// 4. Creates a distribution request via the AMP API
11420    /// 5. Constructs and signs the blockchain transaction using the provided signer
11421    /// 6. Broadcasts the transaction to the Elements network
11422    /// 7. Waits for blockchain confirmations (2 confirmations minimum)
11423    /// 8. Confirms the distribution with the AMP API
11424    ///
11425    /// # Arguments
11426    /// * `asset_uuid` - The UUID of the asset to distribute (must be valid UUID format)
11427    /// * `assignments` - Vector of assignments specifying `user_id`, address, and amount
11428    /// * `node_rpc` - `ElementsRpc` client for blockchain operations
11429    /// * `signer` - Signer implementation for transaction signing
11430    ///
11431    /// # Returns
11432    /// Returns `Ok(())` if the distribution completes successfully, or an `AmpError` if:
11433    /// - Input validation fails (invalid UUID format, empty assignments, etc.)
11434    /// - `ElementsRpc` connection cannot be established
11435    /// - Signer interface is not available
11436    /// - Authentication with AMP API fails
11437    /// - Distribution creation fails
11438    /// - Transaction construction or signing fails
11439    /// - Blockchain broadcasting fails
11440    /// - Confirmation timeout occurs
11441    /// - Distribution confirmation with AMP API fails
11442    ///
11443    /// # Examples
11444    /// ```no_run
11445    /// # use amp_rs::{ApiClient, ElementsRpc, AmpError};
11446    /// # use amp_rs::model::AssetDistributionAssignment;
11447    /// # use amp_rs::signer::{Signer, LwkSoftwareSigner};
11448    /// # #[tokio::main]
11449    /// # async fn main() -> Result<(), AmpError> {
11450    /// let client = ApiClient::new().await?;
11451    /// let elements_rpc = ElementsRpc::from_env()?;
11452    /// let (_, signer) = LwkSoftwareSigner::generate_new()?;
11453    ///
11454    /// let assignments = vec![
11455    ///     AssetDistributionAssignment {
11456    ///         user_id: "user123".to_string(),
11457    ///         address: "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
11458    ///         amount: 100.0,
11459    ///     },
11460    /// ];
11461    ///
11462    /// client.distribute_asset(
11463    ///     "550e8400-e29b-41d4-a716-446655440000",
11464    ///     assignments,
11465    ///     &elements_rpc,
11466    ///     "wallet_name",
11467    ///     &signer
11468    /// ).await?;
11469    /// # Ok(())
11470    /// # }
11471    /// ```
11472    ///
11473    /// # Requirements
11474    /// This method implements requirements:
11475    /// - 1.1: Single method for complete distribution workflow
11476    /// - 2.2: Assignment details validation
11477    /// - 2.4: Input validation for all parameters
11478    /// - 5.1: Comprehensive error handling with context
11479    #[allow(clippy::cognitive_complexity, clippy::too_many_lines)]
11480    pub async fn distribute_asset(
11481        &self,
11482        asset_uuid: &str,
11483        assignments: Vec<AssetDistributionAssignment>,
11484        node_rpc: &ElementsRpc,
11485        wallet_name: &str,
11486        signer: &dyn Signer,
11487    ) -> Result<(), AmpError> {
11488        let distribution_span = tracing::info_span!(
11489            "distribute_asset",
11490            asset_uuid = %asset_uuid,
11491            assignment_count = assignments.len()
11492        );
11493        let _enter = distribution_span.enter();
11494
11495        tracing::info!(
11496            "Starting asset distribution workflow for asset: {} with {} assignments",
11497            asset_uuid,
11498            assignments.len()
11499        );
11500
11501        // Step 1: Input validation - asset_uuid format
11502        tracing::debug!("Step 1: Validating asset UUID format");
11503        Self::validate_asset_uuid(asset_uuid).map_err(|e| {
11504            let error = AmpError::validation(format!("Invalid asset UUID: {e}"));
11505            tracing::error!("Asset UUID validation failed: {}", e);
11506            error.with_context("Step 1: Asset UUID validation")
11507        })?;
11508        tracing::debug!("Asset UUID validation passed");
11509
11510        // Step 2: Input validation - assignments data structure
11511        tracing::debug!("Step 2: Validating {} assignments", assignments.len());
11512        Self::validate_assignments(&assignments).map_err(|e| {
11513            let error = AmpError::validation(format!("Invalid assignments: {e}"));
11514            tracing::error!("Assignments validation failed: {}", e);
11515            error.with_context("Step 2: Assignments validation")
11516        })?;
11517        tracing::debug!("Assignments validation passed");
11518
11519        // Step 3: Check ElementsRpc connection availability
11520        tracing::debug!("Step 3: Validating Elements RPC connection");
11521        self.validate_elements_rpc_connection(node_rpc)
11522            .await
11523            .map_err(|e| {
11524                let error = AmpError::rpc(format!("ElementsRpc connection validation failed: {e}"));
11525                tracing::error!("Elements RPC connection validation failed: {}", e);
11526                error.with_context("Step 3: Elements RPC connection validation")
11527            })?;
11528        tracing::debug!("Elements RPC connection validation passed");
11529
11530        // Step 4: Check signer interface availability
11531        tracing::debug!("Step 4: Validating signer interface");
11532        self.validate_signer_interface(signer).await.map_err(|e| {
11533            let error = AmpError::validation(format!("Signer interface validation failed: {e}"));
11534            tracing::error!("Signer interface validation failed: {}", e);
11535            error.with_context("Step 4: Signer interface validation")
11536        })?;
11537        tracing::debug!("Signer interface validation passed");
11538
11539        tracing::info!("✓ All input validations completed successfully");
11540
11541        // Step 5: Authenticate with AMP API using existing TokenManager
11542        tracing::debug!("Step 5: Authenticating with AMP API");
11543        let _token = self.token_strategy.get_token().await.map_err(|e| {
11544            tracing::error!("AMP API authentication failed: {}", e);
11545            let amp_error = AmpError::Existing(e);
11546            if amp_error.is_retryable() {
11547                if let Some(instructions) = amp_error.retry_instructions() {
11548                    tracing::warn!("Retry instructions: {}", instructions);
11549                }
11550            }
11551            amp_error.with_context("Step 5: AMP API authentication")
11552        })?;
11553        tracing::info!("✓ Successfully authenticated with AMP API");
11554
11555        // Step 6: Create distribution request and parse response data
11556        tracing::debug!(
11557            "Step 6: Creating distribution request with {} assignments",
11558            assignments.len()
11559        );
11560        let distribution_response = self
11561            .create_distribution(asset_uuid, assignments)
11562            .await
11563            .map_err(|e| {
11564                tracing::error!("Distribution creation failed: {}", e);
11565                if e.is_retryable() {
11566                    if let Some(instructions) = e.retry_instructions() {
11567                        tracing::warn!("Retry instructions: {}", instructions);
11568                    }
11569                }
11570                e.with_context("Step 6: Distribution creation")
11571            })?;
11572
11573        tracing::info!(
11574            "✓ Distribution created successfully: {} with asset_id: {}",
11575            distribution_response.distribution_uuid,
11576            distribution_response.asset_id
11577        );
11578
11579        // Step 7: Verify Elements node status and execute transaction workflow
11580        tracing::debug!("Step 7: Verifying Elements node status");
11581        let (network_info, blockchain_info) = node_rpc.get_node_status().await.map_err(|e| {
11582            tracing::error!("Elements node status verification failed: {}", e);
11583            if e.is_retryable() {
11584                if let Some(instructions) = e.retry_instructions() {
11585                    tracing::warn!("Retry instructions: {}", instructions);
11586                }
11587            }
11588            e.with_context("Step 7: Elements node status verification")
11589        })?;
11590
11591        tracing::info!(
11592            "✓ Elements node verified - chain: {}, blocks: {}, connections: {}",
11593            blockchain_info.chain,
11594            blockchain_info.blocks,
11595            network_info.connections
11596        );
11597
11598        // Step 8: Send distribution transaction using Elements' sendmany
11599        tracing::debug!("Step 8: Sending distribution transaction using Elements sendmany");
11600
11601        // Create asset amounts map for sendmany (all outputs use the same asset)
11602        let mut asset_amounts = std::collections::HashMap::new();
11603        for address in distribution_response.map_address_amount.keys() {
11604            asset_amounts.insert(address.clone(), distribution_response.asset_id.clone());
11605        }
11606
11607        tracing::info!(
11608            "Using sendmany for {} outputs with asset {}",
11609            distribution_response.map_address_amount.len(),
11610            distribution_response.asset_id
11611        );
11612
11613        // Use Elements' sendmany which properly handles confidential transactions
11614        let txid = node_rpc
11615            .sendmany(
11616                wallet_name,
11617                distribution_response.map_address_amount.clone(),
11618                asset_amounts,
11619                Some(0), // min_conf: 0 to include unconfirmed UTXOs (matches Python implementation)
11620                Some("AMP asset distribution"), // comment
11621                None,    // subtract_fee_from: let Elements handle fees automatically
11622                Some(false), // replaceable: false for final transactions
11623                Some(1), // conf_target: 1 block for faster confirmation
11624                Some("UNSET"), // estimate_mode: let Elements choose
11625            )
11626            .await
11627            .map_err(|e| {
11628                tracing::error!("Sendmany transaction failed: {}", e);
11629                if e.is_retryable() {
11630                    if let Some(instructions) = e.retry_instructions() {
11631                        tracing::warn!("Retry instructions: {}", instructions);
11632                    }
11633                }
11634                e.with_context("Step 8: Sendmany transaction")
11635            })?;
11636
11637        tracing::info!("✓ Transaction sent successfully with ID: {}", txid);
11638
11639        // Step 9: Wait for confirmations
11640        tracing::debug!("Step 9: Waiting for blockchain confirmations (minimum 2 confirmations, 10-minute timeout)");
11641        let confirmation_start = std::time::Instant::now();
11642        let tx_detail = node_rpc.wait_for_confirmations(&txid, Some(2), Some(10)).await
11643            .map_err(|e| {
11644                let elapsed = confirmation_start.elapsed();
11645                tracing::error!(
11646                    "Confirmation waiting failed after {:?}: {}",
11647                    elapsed,
11648                    e
11649                );
11650
11651                if let AmpError::Timeout(_) = &e {
11652                    tracing::warn!(
11653                        "Confirmation timeout - transaction {} may still be pending. \
11654                        Use this txid to manually confirm the distribution if it gets confirmed later.",
11655                        txid
11656                    );
11657                    let timeout_error = AmpError::timeout(format!(
11658                        "Confirmation timeout for txid: {txid}. Use this txid to manually confirm the distribution."
11659                    ));
11660                    timeout_error.with_context("Step 9: Confirmation waiting")
11661                } else {
11662                    if e.is_retryable() {
11663                        if let Some(instructions) = e.retry_instructions() {
11664                            tracing::warn!("Retry instructions: {}", instructions);
11665                        }
11666                    }
11667                    e.with_context(format!("Step 9: Confirmation waiting for txid: {txid}"))
11668                }
11669            })?;
11670
11671        let confirmation_duration = confirmation_start.elapsed();
11672        tracing::info!(
11673            "✓ Transaction confirmed with {} confirmations at block height: {:?} (took {:?})",
11674            tx_detail.confirmations,
11675            tx_detail.blockheight,
11676            confirmation_duration
11677        );
11678
11679        // Step 10: Collect change data for confirmation
11680        tracing::debug!("Step 10: Collecting change data for distribution confirmation");
11681        let change_data = node_rpc
11682            .collect_change_data(
11683                &distribution_response.asset_id,
11684                &txid,
11685                node_rpc,
11686                wallet_name,
11687            )
11688            .await
11689            .map_err(|e| {
11690                tracing::error!("Change data collection failed: {}", e);
11691                if e.is_retryable() {
11692                    if let Some(instructions) = e.retry_instructions() {
11693                        tracing::warn!("Retry instructions: {}", instructions);
11694                    }
11695                }
11696                e.with_context("Step 10: Change data collection")
11697            })?;
11698
11699        tracing::info!("✓ Collected {} change UTXOs", change_data.len());
11700        if !change_data.is_empty() {
11701            tracing::debug!("Change UTXOs: {:?}", change_data);
11702        }
11703
11704        // Step 11: Submit final confirmation to AMP API
11705        tracing::debug!("Step 11: Submitting final confirmation to AMP API");
11706
11707        // Extract the details field from the transaction (matching Python implementation)
11708        // Python: details = rpc.call('gettransaction', txid).get('details')
11709        let transaction_details = tx_detail.details.unwrap_or_else(Vec::new);
11710        tracing::debug!(
11711            "Transaction details for confirmation: {:?}",
11712            transaction_details
11713        );
11714
11715        let amp_tx_data = crate::model::AmpTxData {
11716            details: serde_json::Value::Array(transaction_details),
11717            txid: txid.clone(),
11718        };
11719
11720        // Log the exact payload being sent to AMP for debugging
11721        tracing::info!("Sending confirmation payload to AMP:");
11722        tracing::info!("  tx_data.txid: {}", amp_tx_data.txid);
11723        tracing::info!("  tx_data.details: {:?}", amp_tx_data.details);
11724        tracing::info!("  change_data: {} UTXOs", change_data.len());
11725
11726        let confirmation_request = crate::model::ConfirmDistributionRequest {
11727            tx_data: amp_tx_data.clone(),
11728            change_data: change_data.clone(),
11729        };
11730
11731        if let Ok(payload_json) = serde_json::to_string_pretty(&confirmation_request) {
11732            tracing::debug!("Full confirmation payload: {}", payload_json);
11733        }
11734
11735        self.confirm_distribution(
11736            asset_uuid,
11737            &distribution_response.distribution_uuid,
11738            amp_tx_data,
11739            change_data,
11740        )
11741        .await
11742        .map_err(|e| {
11743            tracing::error!("Distribution confirmation failed: {}", e);
11744
11745            // For confirmation failures, always provide retry instructions with txid
11746            let confirmation_error = AmpError::api(format!(
11747                "Failed to confirm distribution {}: {}. \
11748                IMPORTANT: Transaction {} was successful on blockchain. \
11749                Use this txid to manually retry confirmation.",
11750                distribution_response.distribution_uuid, e, txid
11751            ));
11752
11753            if e.is_retryable() {
11754                if let Some(instructions) = e.retry_instructions() {
11755                    tracing::warn!("Retry instructions: {}", instructions);
11756                }
11757            }
11758
11759            confirmation_error.with_context("Step 11: Distribution confirmation")
11760        })?;
11761
11762        tracing::info!(
11763            "🎉 Asset distribution completed successfully for asset: {} with transaction: {}",
11764            asset_uuid,
11765            txid
11766        );
11767
11768        Ok(())
11769    }
11770
11771    /// Validates the asset UUID format
11772    ///
11773    /// Ensures the asset UUID follows the standard UUID format (8-4-4-4-12 hexadecimal digits)
11774    ///
11775    /// # Arguments
11776    /// * `asset_uuid` - The asset UUID string to validate
11777    ///
11778    /// # Returns
11779    /// Returns `Ok(())` if valid, or an error describing the validation failure
11780    ///
11781    /// # Errors
11782    /// - Empty or whitespace-only UUID
11783    /// - Invalid UUID format (not matching standard UUID pattern)
11784    /// - UUID contains invalid characters
11785    fn validate_asset_uuid(asset_uuid: &str) -> Result<(), String> {
11786        if asset_uuid.trim().is_empty() {
11787            return Err("Asset UUID cannot be empty".to_string());
11788        }
11789
11790        // Basic UUID format validation (8-4-4-4-12 pattern)
11791        // Expected format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
11792        let parts: Vec<&str> = asset_uuid.split('-').collect();
11793        if parts.len() != 5 {
11794            return Err(format!(
11795                "Asset UUID '{asset_uuid}' does not have 5 parts separated by hyphens"
11796            ));
11797        }
11798
11799        // Check each part has the correct length and contains only hex characters
11800        let expected_lengths = [8, 4, 4, 4, 12];
11801        for (i, (part, &expected_len)) in parts.iter().zip(expected_lengths.iter()).enumerate() {
11802            if part.len() != expected_len {
11803                return Err(format!(
11804                    "Asset UUID part {} has length {} but expected {}",
11805                    i + 1,
11806                    part.len(),
11807                    expected_len
11808                ));
11809            }
11810
11811            // Check if all characters are valid hexadecimal
11812            if !part.chars().all(|c| c.is_ascii_hexdigit()) {
11813                return Err(format!(
11814                    "Asset UUID part {} contains non-hexadecimal characters: '{}'",
11815                    i + 1,
11816                    part
11817                ));
11818            }
11819        }
11820
11821        tracing::debug!("Asset UUID validation passed: {}", asset_uuid);
11822        Ok(())
11823    }
11824
11825    /// Validates the assignments data structure
11826    ///
11827    /// Ensures assignments vector is not empty and each assignment has valid data
11828    ///
11829    /// # Arguments
11830    /// * `assignments` - Vector of assignments to validate
11831    ///
11832    /// # Returns
11833    /// Returns `Ok(())` if valid, or an error describing the validation failure
11834    ///
11835    /// # Errors
11836    /// - Empty assignments vector
11837    /// - Assignment with empty `user_id`
11838    /// - Assignment with empty address
11839    /// - Assignment with non-positive amount
11840    /// - Assignment with invalid address format
11841    #[allow(clippy::cognitive_complexity)]
11842    fn validate_assignments(assignments: &[AssetDistributionAssignment]) -> Result<(), String> {
11843        tracing::debug!("Validating {} assignments", assignments.len());
11844
11845        if assignments.is_empty() {
11846            tracing::error!("Assignments validation failed: empty assignments vector");
11847            return Err("Assignments vector cannot be empty".to_string());
11848        }
11849
11850        let mut total_amount = 0.0;
11851        let mut unique_addresses = std::collections::HashSet::new();
11852        let mut unique_users = std::collections::HashSet::new();
11853
11854        for (index, assignment) in assignments.iter().enumerate() {
11855            tracing::trace!(
11856                "Validating assignment {}: user_id={}, address={}, amount={}",
11857                index,
11858                assignment.user_id,
11859                assignment.address,
11860                assignment.amount
11861            );
11862
11863            // Validate user_id
11864            if assignment.user_id.trim().is_empty() {
11865                tracing::error!("Assignment {} validation failed: empty user_id", index);
11866                return Err(format!("Assignment {index} has empty user_id"));
11867            }
11868
11869            // Validate address
11870            if assignment.address.trim().is_empty() {
11871                tracing::error!("Assignment {} validation failed: empty address", index);
11872                return Err(format!("Assignment {index} has empty address"));
11873            }
11874
11875            // Basic address format validation (should start with appropriate prefix for Liquid)
11876            if !assignment.address.starts_with("lq")
11877                && !assignment.address.starts_with("vj")
11878                && !assignment.address.starts_with("VJ")
11879                && !assignment.address.starts_with("VT")
11880            {
11881                tracing::error!(
11882                    "Assignment {} validation failed: invalid address format '{}' (should start with 'lq', 'vj', 'VJ', or 'VT')",
11883                    index, assignment.address
11884                );
11885                return Err(format!(
11886                    "Assignment {} has invalid address format: '{}' (should start with 'lq', 'vj', 'VJ', or 'VT')",
11887                    index, assignment.address
11888                ));
11889            }
11890
11891            // Validate amount
11892            if assignment.amount <= 0.0 {
11893                tracing::error!(
11894                    "Assignment {} validation failed: non-positive amount {}",
11895                    index,
11896                    assignment.amount
11897                );
11898                return Err(format!(
11899                    "Assignment {} has non-positive amount: {}",
11900                    index, assignment.amount
11901                ));
11902            }
11903
11904            // Check for reasonable amount limits (prevent overflow issues)
11905            if assignment.amount > 21_000_000.0 {
11906                tracing::error!(
11907                    "Assignment {} validation failed: unreasonably large amount {} (max: 21,000,000)",
11908                    index, assignment.amount
11909                );
11910                return Err(format!(
11911                    "Assignment {} has unreasonably large amount: {} (max: 21,000,000)",
11912                    index, assignment.amount
11913                ));
11914            }
11915
11916            // Check for precision issues (more than 8 decimal places)
11917            let amount_str = format!("{:.8}", assignment.amount);
11918            if amount_str.len() > 20 {
11919                // Reasonable length check
11920                tracing::warn!(
11921                    "Assignment {} has high precision amount: {} - may cause precision issues",
11922                    index,
11923                    assignment.amount
11924                );
11925            }
11926
11927            // Track duplicates for warnings
11928            if !unique_addresses.insert(&assignment.address) {
11929                tracing::warn!(
11930                    "Assignment {} uses duplicate address: {} (this may be intentional)",
11931                    index,
11932                    assignment.address
11933                );
11934            }
11935
11936            if !unique_users.insert(&assignment.user_id) {
11937                tracing::warn!(
11938                    "Assignment {} uses duplicate user_id: {} (this may be intentional)",
11939                    index,
11940                    assignment.user_id
11941                );
11942            }
11943
11944            total_amount += assignment.amount;
11945        }
11946
11947        tracing::debug!(
11948            "Assignments validation passed - {} assignments, total amount: {}, unique addresses: {}, unique users: {}",
11949            assignments.len(),
11950            total_amount,
11951            unique_addresses.len(),
11952            unique_users.len()
11953        );
11954
11955        if total_amount > 100_000_000.0 {
11956            tracing::warn!(
11957                "Total distribution amount is very large: {} - ensure this is intentional",
11958                total_amount
11959            );
11960        }
11961
11962        Ok(())
11963    }
11964
11965    /// Validates `ElementsRpc` connection availability
11966    ///
11967    /// Attempts to connect to the Elements node and verify basic functionality
11968    ///
11969    /// # Arguments
11970    /// * `node_rpc` - `ElementsRpc` client to validate
11971    ///
11972    /// # Returns
11973    /// Returns `Ok(())` if connection is valid, or an error describing the failure
11974    ///
11975    /// # Errors
11976    /// - Cannot connect to Elements node
11977    /// - Node is not synchronized
11978    /// - Node version is incompatible
11979    /// - RPC authentication fails
11980    #[allow(clippy::cognitive_complexity)]
11981    async fn validate_elements_rpc_connection(&self, node_rpc: &ElementsRpc) -> Result<(), String> {
11982        tracing::debug!("Validating Elements RPC connection");
11983
11984        // Test basic connectivity by getting network info
11985        tracing::trace!("Testing Elements RPC connectivity with getnetworkinfo");
11986        let network_info = node_rpc.get_network_info().await.map_err(|e| {
11987            tracing::error!("Failed to get network info from Elements node: {}", e);
11988            format!("Failed to get network info: {e}")
11989        })?;
11990
11991        tracing::debug!(
11992            "Network info retrieved - version: {}, connections: {}, network_active: {}",
11993            network_info.version,
11994            network_info.connections,
11995            network_info.networkactive
11996        );
11997
11998        // Check if network is active
11999        if !network_info.networkactive {
12000            tracing::error!("Elements node network is not active");
12001            return Err("Elements node network is not active".to_string());
12002        }
12003
12004        // Verify we have active connections (for non-regtest environments)
12005        if network_info.connections == 0 {
12006            tracing::warn!("Elements node has no peer connections (may be regtest environment)");
12007        } else {
12008            tracing::debug!(
12009                "Elements node has {} peer connections",
12010                network_info.connections
12011            );
12012        }
12013
12014        // Test blockchain info to ensure node is operational
12015        tracing::trace!("Testing Elements RPC with getblockchaininfo");
12016        let blockchain_info = node_rpc.get_blockchain_info().await.map_err(|e| {
12017            tracing::error!("Failed to get blockchain info from Elements node: {}", e);
12018            format!("Failed to get blockchain info: {e}")
12019        })?;
12020
12021        let sync_progress = blockchain_info.verificationprogress.unwrap_or(1.0) * 100.0;
12022        tracing::debug!(
12023            "Blockchain info retrieved - chain: {}, blocks: {}, sync_progress: {:.2}%",
12024            blockchain_info.chain,
12025            blockchain_info.blocks,
12026            sync_progress
12027        );
12028
12029        // Check if node is still in initial block download
12030        if blockchain_info.initialblockdownload.unwrap_or(false) {
12031            tracing::error!(
12032                "Elements node is still in initial block download (sync progress: {:.2}%)",
12033                sync_progress
12034            );
12035            return Err(format!(
12036                "Elements node is still in initial block download (sync progress: {sync_progress:.2}%)"
12037            ));
12038        }
12039
12040        // Check sync progress
12041        if blockchain_info.verificationprogress.unwrap_or(1.0) < 0.99 {
12042            tracing::warn!(
12043                "Elements node may not be fully synced (sync progress: {:.2}%)",
12044                sync_progress
12045            );
12046        }
12047
12048        // Check for warnings
12049        if !network_info.warnings.is_empty() {
12050            tracing::warn!("Elements node network warnings: {}", network_info.warnings);
12051        }
12052
12053        if let Some(warnings) = &blockchain_info.warnings {
12054            if !warnings.is_empty() {
12055                tracing::warn!("Elements node blockchain warnings: {}", warnings);
12056            }
12057        }
12058
12059        tracing::debug!(
12060            "ElementsRpc connection validation passed - chain: {}, blocks: {}, connections: {}, sync: {:.2}%",
12061            blockchain_info.chain,
12062            blockchain_info.blocks,
12063            network_info.connections,
12064            sync_progress
12065        );
12066
12067        Ok(())
12068    }
12069
12070    /// Validates signer interface availability
12071    ///
12072    /// Tests the signer interface with a dummy transaction to ensure it's functional
12073    ///
12074    /// # Arguments
12075    /// * `signer` - Signer implementation to validate
12076    ///
12077    /// # Returns
12078    /// Returns `Ok(())` if signer is functional, or an error describing the failure
12079    ///
12080    /// # Errors
12081    /// - Signer interface is not responsive
12082    /// - Signer fails basic functionality test
12083    #[allow(clippy::cognitive_complexity)]
12084    async fn validate_signer_interface(&self, signer: &dyn Signer) -> Result<(), String> {
12085        tracing::debug!("Validating signer interface");
12086
12087        // Test signer with a minimal dummy transaction hex
12088        // This is a minimal Elements transaction structure that should parse but not be valid for signing
12089        let dummy_tx = "0200000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000";
12090
12091        tracing::trace!("Testing signer interface with dummy transaction");
12092
12093        // Attempt to sign the dummy transaction - we expect this to fail with a specific error
12094        // but the signer should be responsive and not panic
12095        let validation_start = std::time::Instant::now();
12096        match signer.sign_transaction(dummy_tx).await {
12097            Ok(signed_tx) => {
12098                // Unexpected success with dummy transaction - this might indicate an issue
12099                tracing::warn!(
12100                    "Signer unexpectedly succeeded with dummy transaction (returned: {} chars)",
12101                    signed_tx.len()
12102                );
12103                tracing::debug!("Signer validation passed despite unexpected success");
12104            }
12105            Err(SignerError::InvalidTransaction(msg)) => {
12106                // Expected error - signer is working and correctly identified invalid transaction
12107                tracing::debug!(
12108                    "Signer interface validation passed - correctly rejected dummy transaction: {}",
12109                    msg
12110                );
12111            }
12112            Err(SignerError::HexParse(msg)) => {
12113                // Also acceptable - signer is working and correctly identified parsing issue
12114                tracing::debug!(
12115                    "Signer interface validation passed - correctly identified hex parsing issue: {}",
12116                    msg
12117                );
12118            }
12119            Err(SignerError::Lwk(msg)) => {
12120                // LWK-specific errors might be acceptable depending on the message
12121                if msg.contains("invalid") || msg.contains("parse") || msg.contains("decode") {
12122                    tracing::debug!(
12123                        "Signer interface validation passed - LWK correctly identified invalid transaction: {}",
12124                        msg
12125                    );
12126                } else {
12127                    tracing::error!("Signer interface test failed with LWK error: {}", msg);
12128                    return Err(format!(
12129                        "Signer interface test failed with LWK error: {msg}"
12130                    ));
12131                }
12132            }
12133            Err(e) => {
12134                // Other errors might indicate signer interface issues
12135                tracing::error!("Signer interface test failed: {}", e);
12136                return Err(format!("Signer interface test failed: {e}"));
12137            }
12138        }
12139
12140        let validation_duration = validation_start.elapsed();
12141        tracing::debug!(
12142            "Signer interface validation completed in {:?}",
12143            validation_duration
12144        );
12145
12146        // Warn if signer is very slow (might indicate performance issues)
12147        if validation_duration > std::time::Duration::from_secs(5) {
12148            tracing::warn!(
12149                "Signer interface validation took {:?} - this may indicate performance issues",
12150                validation_duration
12151            );
12152        }
12153
12154        Ok(())
12155    }
12156}
12157
12158fn get_amp_api_base_url() -> Result<Url, Error> {
12159    let url_str = env::var("AMP_API_BASE_URL")
12160        .unwrap_or_else(|_| "https://amp-test.blockstream.com/api".to_string());
12161    Url::parse(&url_str).map_err(Error::from)
12162}
12163
12164/// Creates a token strategy based on automatic environment detection
12165///
12166/// This function detects the current environment and creates the appropriate strategy:
12167/// - Mock strategy for mock environments (isolated, no persistence)
12168/// - Live strategy for live environments (full token management)
12169///
12170/// # Arguments
12171/// * `mock_token` - Optional token to use for mock environments
12172///
12173/// # Errors
12174/// Returns an error if strategy creation fails
12175pub async fn create_auto_token_strategy(
12176    mock_token: Option<String>,
12177) -> Result<Box<dyn TokenStrategy>, Error> {
12178    TokenEnvironment::create_auto_strategy(mock_token).await
12179}
12180
12181/// Creates a mock token strategy with the specified token
12182///
12183/// # Arguments
12184/// * `token` - The mock token to use
12185#[must_use]
12186pub fn create_mock_token_strategy(token: String) -> Box<dyn TokenStrategy> {
12187    Box::new(MockTokenStrategy::new(token))
12188}
12189
12190/// Creates a live token strategy with default configuration
12191///
12192/// # Errors
12193/// Returns an error if the `TokenManager` cannot be initialized
12194pub async fn create_live_token_strategy() -> Result<Box<dyn TokenStrategy>, Error> {
12195    let strategy = LiveTokenStrategy::new().await?;
12196    Ok(Box::new(strategy))
12197}
12198
12199/// Creates a token strategy for the specified environment
12200///
12201/// # Arguments
12202/// * `environment` - The target environment
12203/// * `mock_token` - Optional token to use for mock environments
12204///
12205/// # Errors
12206/// Returns an error if strategy creation fails
12207pub async fn create_token_strategy_for_environment(
12208    environment: TokenEnvironment,
12209    mock_token: Option<String>,
12210) -> Result<Box<dyn TokenStrategy>, Error> {
12211    environment.create_strategy(mock_token).await
12212}
12213
12214#[cfg(test)]
12215mod tests {
12216    use super::*;
12217    use crate::signer::LwkSoftwareSigner;
12218    use tokio;
12219
12220    #[tokio::test]
12221    async fn test_mock_token_strategy_basic_functionality() {
12222        let mock_token = "mock_token_12_345".to_string();
12223        let strategy = MockTokenStrategy::new(mock_token.clone());
12224
12225        // Test get_token returns the mock token
12226        let result = strategy.get_token().await;
12227        assert!(result.is_ok());
12228        assert_eq!(result.unwrap(), mock_token);
12229
12230        // Test strategy type identification
12231        assert_eq!(strategy.strategy_type(), "mock");
12232
12233        // Test persistence is disabled
12234        assert!(!strategy.should_persist());
12235
12236        // Test clear_token is a no-op (should not fail)
12237        let clear_result = strategy.clear_token().await;
12238        assert!(clear_result.is_ok());
12239
12240        // Verify token is still available after clear (since it's a no-op for mock)
12241        let token_after_clear = strategy.get_token().await;
12242        assert!(token_after_clear.is_ok());
12243        assert_eq!(token_after_clear.unwrap(), mock_token);
12244    }
12245
12246    #[tokio::test]
12247    async fn test_mock_token_strategy_isolation() {
12248        let token1 = "token_instance_1".to_string();
12249        let token2 = "token_instance_2".to_string();
12250
12251        let strategy1 = MockTokenStrategy::new(token1.clone());
12252        let strategy2 = MockTokenStrategy::new(token2.clone());
12253
12254        // Test that different instances are isolated
12255        let result1 = strategy1.get_token().await.unwrap();
12256        let result2 = strategy2.get_token().await.unwrap();
12257
12258        assert_eq!(result1, token1);
12259        assert_eq!(result2, token2);
12260        assert_ne!(result1, result2);
12261
12262        // Test that operations on one don't affect the other
12263        let _ = strategy1.clear_token().await;
12264        let result2_after_clear = strategy2.get_token().await.unwrap();
12265        assert_eq!(result2_after_clear, token2);
12266    }
12267
12268    #[tokio::test]
12269    async fn test_live_token_strategy_creation() {
12270        // Test creating a live strategy with global instance
12271        let strategy_result = LiveTokenStrategy::new().await;
12272        assert!(strategy_result.is_ok());
12273
12274        let strategy = strategy_result.unwrap();
12275        assert_eq!(strategy.strategy_type(), "live");
12276        assert!(strategy.should_persist());
12277    }
12278
12279    #[tokio::test]
12280    async fn test_live_token_strategy_with_custom_manager() {
12281        // Create a custom token manager for testing
12282        let config = RetryConfig::for_tests();
12283        let base_url = Url::parse("http://localhost:8080").unwrap();
12284        let mock_token = "test_live_token".to_string();
12285
12286        let token_manager =
12287            Arc::new(TokenManager::with_mock_token(config, base_url, mock_token.clone()).unwrap());
12288
12289        let strategy = LiveTokenStrategy::with_token_manager(token_manager);
12290
12291        // Test strategy properties
12292        assert_eq!(strategy.strategy_type(), "live");
12293        assert!(strategy.should_persist());
12294
12295        // Test token retrieval
12296        let token_result = strategy.get_token().await;
12297        assert!(token_result.is_ok());
12298        assert_eq!(token_result.unwrap(), mock_token);
12299    }
12300
12301    #[tokio::test]
12302    async fn test_live_token_strategy_clear_token() {
12303        // Create a live strategy with a mock token manager
12304        let config = RetryConfig::for_tests();
12305        let base_url = Url::parse("http://localhost:8080").unwrap();
12306        let mock_token = "test_clear_token".to_string();
12307
12308        let token_manager =
12309            Arc::new(TokenManager::with_mock_token(config, base_url, mock_token.clone()).unwrap());
12310
12311        let strategy = LiveTokenStrategy::with_token_manager(token_manager);
12312
12313        // Verify token is available initially
12314        let initial_token = strategy.get_token().await;
12315        assert!(initial_token.is_ok());
12316        assert_eq!(initial_token.unwrap(), mock_token);
12317
12318        // Clear the token
12319        let clear_result = strategy.clear_token().await;
12320        assert!(clear_result.is_ok());
12321
12322        // Note: After clearing, the TokenManager would try to obtain a new token
12323        // In a real scenario, this would fail without proper credentials
12324        // But our mock token manager will still return the same token
12325    }
12326
12327    #[tokio::test]
12328    async fn test_strategy_type_identification() {
12329        let mock_strategy = MockTokenStrategy::new("test_token".to_string());
12330        let live_strategy = LiveTokenStrategy::new().await.unwrap();
12331
12332        // Test that we can identify strategy types for debugging
12333        assert_eq!(mock_strategy.strategy_type(), "mock");
12334        assert_eq!(live_strategy.strategy_type(), "live");
12335
12336        // Test persistence settings
12337        assert!(!mock_strategy.should_persist());
12338        assert!(live_strategy.should_persist());
12339    }
12340
12341    #[tokio::test]
12342    async fn test_strategy_debug_formatting() {
12343        let mock_strategy = MockTokenStrategy::new("debug_test_token".to_string());
12344        let debug_output = format!("{mock_strategy:?}");
12345
12346        // Verify debug output contains expected information
12347        assert!(debug_output.contains("MockTokenStrategy"));
12348        assert!(debug_output.contains("debug_test_token"));
12349    }
12350
12351    // Environment Detection Tests
12352
12353    #[test]
12354    fn test_token_environment_detect_live_via_amp_tests() {
12355        // Set up environment for live test detection
12356        env::set_var("AMP_TESTS", "live");
12357        env::set_var("AMP_USERNAME", "real_user");
12358        env::set_var("AMP_PASSWORD", "real_pass");
12359        env::remove_var("AMP_API_BASE_URL");
12360
12361        let environment = TokenEnvironment::detect();
12362        assert_eq!(environment, TokenEnvironment::Live);
12363
12364        // Clean up
12365        env::remove_var("AMP_TESTS");
12366        env::remove_var("AMP_USERNAME");
12367        env::remove_var("AMP_PASSWORD");
12368    }
12369
12370    #[test]
12371    fn test_token_environment_detect_mock_via_credentials() {
12372        // Set up environment for mock detection via username
12373        env::remove_var("AMP_TESTS");
12374        env::set_var("AMP_USERNAME", "mock_user");
12375        env::set_var("AMP_PASSWORD", "real_pass");
12376        env::remove_var("AMP_API_BASE_URL");
12377
12378        let environment = TokenEnvironment::detect();
12379        assert_eq!(environment, TokenEnvironment::Mock);
12380
12381        // Test mock detection via password
12382        env::set_var("AMP_USERNAME", "real_user");
12383        env::set_var("AMP_PASSWORD", "mock_pass");
12384
12385        let environment = TokenEnvironment::detect();
12386        assert_eq!(environment, TokenEnvironment::Mock);
12387
12388        // Clean up
12389        env::remove_var("AMP_USERNAME");
12390        env::remove_var("AMP_PASSWORD");
12391    }
12392
12393    #[test]
12394    fn test_token_environment_detect_mock_via_base_url() {
12395        // Set up environment for mock detection via localhost URL
12396        env::remove_var("AMP_TESTS");
12397        env::set_var("AMP_USERNAME", "real_user");
12398        env::set_var("AMP_PASSWORD", "real_pass");
12399        env::set_var("AMP_API_BASE_URL", "http://localhost:8080/api");
12400
12401        let environment = TokenEnvironment::detect();
12402        assert_eq!(environment, TokenEnvironment::Mock);
12403
12404        // Test with 127.0.0.1
12405        env::set_var("AMP_API_BASE_URL", "http://127.0.0.1:3000/api");
12406        let environment = TokenEnvironment::detect();
12407        assert_eq!(environment, TokenEnvironment::Mock);
12408
12409        // Test with mock in URL
12410        env::set_var("AMP_API_BASE_URL", "http://mock-server.example.com/api");
12411        let environment = TokenEnvironment::detect();
12412        assert_eq!(environment, TokenEnvironment::Mock);
12413
12414        // Clean up
12415        env::remove_var("AMP_USERNAME");
12416        env::remove_var("AMP_PASSWORD");
12417        env::remove_var("AMP_API_BASE_URL");
12418    }
12419
12420    #[test]
12421    fn test_token_environment_detect_live_via_real_credentials() {
12422        // Set up environment for live detection via real credentials
12423        env::remove_var("AMP_TESTS");
12424        env::set_var("AMP_USERNAME", "real_user");
12425        env::set_var("AMP_PASSWORD", "real_pass");
12426        env::set_var("AMP_API_BASE_URL", "https://amp-test.blockstream.com/api");
12427
12428        let environment = TokenEnvironment::detect();
12429        assert_eq!(environment, TokenEnvironment::Live);
12430
12431        // Clean up
12432        env::remove_var("AMP_USERNAME");
12433        env::remove_var("AMP_PASSWORD");
12434        env::remove_var("AMP_API_BASE_URL");
12435    }
12436
12437    #[test]
12438    fn test_token_environment_detect_mock_fallback() {
12439        // Set up environment with no credentials (fallback to mock)
12440        env::remove_var("AMP_TESTS");
12441        env::remove_var("AMP_USERNAME");
12442        env::remove_var("AMP_PASSWORD");
12443        env::remove_var("AMP_API_BASE_URL");
12444
12445        let environment = TokenEnvironment::detect();
12446        assert_eq!(environment, TokenEnvironment::Mock);
12447    }
12448
12449    #[test]
12450    fn test_has_mock_credentials() {
12451        // Test mock username detection
12452        assert!(TokenEnvironment::has_mock_credentials(
12453            "mock_user",
12454            "real_pass",
12455            ""
12456        ));
12457        assert!(TokenEnvironment::has_mock_credentials(
12458            "Mock_User",
12459            "real_pass",
12460            ""
12461        ));
12462        assert!(TokenEnvironment::has_mock_credentials(
12463            "user_mock",
12464            "real_pass",
12465            ""
12466        ));
12467
12468        // Test mock password detection
12469        assert!(TokenEnvironment::has_mock_credentials(
12470            "real_user",
12471            "mock_pass",
12472            ""
12473        ));
12474        assert!(TokenEnvironment::has_mock_credentials(
12475            "real_user",
12476            "Mock_Pass",
12477            ""
12478        ));
12479        assert!(TokenEnvironment::has_mock_credentials(
12480            "real_user",
12481            "pass_mock",
12482            ""
12483        ));
12484
12485        // Test mock URL detection
12486        assert!(TokenEnvironment::has_mock_credentials(
12487            "real_user",
12488            "real_pass",
12489            "http://localhost:8080"
12490        ));
12491        assert!(TokenEnvironment::has_mock_credentials(
12492            "real_user",
12493            "real_pass",
12494            "http://127.0.0.1:3000"
12495        ));
12496        assert!(TokenEnvironment::has_mock_credentials(
12497            "real_user",
12498            "real_pass",
12499            "http://mock-server.com"
12500        ));
12501        assert!(TokenEnvironment::has_mock_credentials(
12502            "real_user",
12503            "real_pass",
12504            "http://Mock-Server.com"
12505        ));
12506
12507        // Test non-mock credentials
12508        assert!(!TokenEnvironment::has_mock_credentials(
12509            "real_user",
12510            "real_pass",
12511            "https://amp-test.blockstream.com"
12512        ));
12513        assert!(!TokenEnvironment::has_mock_credentials("", "", ""));
12514    }
12515
12516    #[test]
12517    fn test_token_environment_should_persist_tokens() {
12518        assert!(!TokenEnvironment::Mock.should_persist_tokens());
12519        assert!(TokenEnvironment::Live.should_persist_tokens());
12520
12521        // Auto should delegate to detect()
12522        env::set_var("AMP_TESTS", "live");
12523        assert!(TokenEnvironment::Auto.should_persist_tokens());
12524
12525        env::set_var("AMP_USERNAME", "mock_user");
12526        env::set_var("AMP_PASSWORD", "some_password");
12527        env::remove_var("AMP_TESTS");
12528        env::remove_var("AMP_API_BASE_URL");
12529        assert!(!TokenEnvironment::Auto.should_persist_tokens());
12530
12531        // Clean up
12532        env::remove_var("AMP_USERNAME");
12533        env::remove_var("AMP_PASSWORD");
12534    }
12535
12536    #[test]
12537    fn test_token_environment_is_mock_and_is_live() {
12538        assert!(TokenEnvironment::Mock.is_mock());
12539        assert!(!TokenEnvironment::Mock.is_live());
12540
12541        assert!(!TokenEnvironment::Live.is_mock());
12542        assert!(TokenEnvironment::Live.is_live());
12543
12544        // Auto should delegate to detect()
12545        env::set_var("AMP_USERNAME", "mock_user");
12546        env::set_var("AMP_PASSWORD", "some_password");
12547        env::remove_var("AMP_TESTS");
12548        env::remove_var("AMP_API_BASE_URL");
12549        assert!(TokenEnvironment::Auto.is_mock());
12550        assert!(!TokenEnvironment::Auto.is_live());
12551
12552        env::set_var("AMP_TESTS", "live");
12553        assert!(!TokenEnvironment::Auto.is_mock());
12554        assert!(TokenEnvironment::Auto.is_live());
12555
12556        // Clean up
12557        env::remove_var("AMP_USERNAME");
12558        env::remove_var("AMP_PASSWORD");
12559        env::remove_var("AMP_TESTS");
12560    }
12561
12562    #[tokio::test]
12563    async fn test_token_environment_create_strategy_mock() {
12564        let mock_token = "test_mock_token".to_string();
12565        let strategy = TokenEnvironment::Mock
12566            .create_strategy(Some(mock_token.clone()))
12567            .await
12568            .unwrap();
12569
12570        assert_eq!(strategy.strategy_type(), "mock");
12571        assert!(!strategy.should_persist());
12572
12573        let token = strategy.get_token().await.unwrap();
12574        assert_eq!(token, mock_token);
12575    }
12576
12577    #[tokio::test]
12578    async fn test_token_environment_create_strategy_live() {
12579        let strategy = TokenEnvironment::Live.create_strategy(None).await.unwrap();
12580
12581        assert_eq!(strategy.strategy_type(), "live");
12582        assert!(strategy.should_persist());
12583    }
12584
12585    #[tokio::test]
12586    async fn test_token_environment_create_auto_strategy() {
12587        // Test with mock environment - need both username and password for proper detection
12588        env::set_var("AMP_USERNAME", "mock_user");
12589        env::set_var("AMP_PASSWORD", "some_password");
12590        env::remove_var("AMP_TESTS");
12591        env::remove_var("AMP_API_BASE_URL");
12592
12593        let mock_token = "auto_mock_token".to_string();
12594        let strategy = TokenEnvironment::create_auto_strategy(Some(mock_token.clone()))
12595            .await
12596            .unwrap();
12597
12598        assert_eq!(strategy.strategy_type(), "mock");
12599        let token = strategy.get_token().await.unwrap();
12600        assert_eq!(token, mock_token);
12601
12602        // Clean up
12603        env::remove_var("AMP_USERNAME");
12604        env::remove_var("AMP_PASSWORD");
12605    }
12606
12607    #[tokio::test]
12608    async fn test_mock_token_strategy_factory_methods() {
12609        // Test with_default_token
12610        let strategy = MockTokenStrategy::with_default_token();
12611        assert_eq!(strategy.strategy_type(), "mock");
12612        let token = strategy.get_token().await.unwrap();
12613        assert_eq!(token, "mock_token_default");
12614
12615        // Test for_test
12616        let strategy = MockTokenStrategy::for_test("my_test");
12617        let token = strategy.get_token().await.unwrap();
12618        assert_eq!(token, "mock_token_my_test");
12619    }
12620
12621    #[tokio::test]
12622    async fn test_live_token_strategy_factory_methods() {
12623        // Test for_testing
12624        let strategy = LiveTokenStrategy::for_testing().await.unwrap();
12625        assert_eq!(strategy.strategy_type(), "live");
12626        assert!(strategy.should_persist());
12627    }
12628
12629    #[tokio::test]
12630    async fn test_standalone_factory_functions() {
12631        // Test create_mock_token_strategy
12632        let mock_token = "standalone_mock".to_string();
12633        let strategy = create_mock_token_strategy(mock_token.clone());
12634        assert_eq!(strategy.strategy_type(), "mock");
12635        let token = strategy.get_token().await.unwrap();
12636        assert_eq!(token, mock_token);
12637
12638        // Test create_live_token_strategy
12639        let strategy = create_live_token_strategy().await.unwrap();
12640        assert_eq!(strategy.strategy_type(), "live");
12641
12642        // Test create_auto_token_strategy with mock environment
12643        env::set_var("AMP_USERNAME", "mock_user");
12644        env::set_var("AMP_PASSWORD", "some_password");
12645        env::remove_var("AMP_TESTS");
12646        env::remove_var("AMP_API_BASE_URL");
12647
12648        let auto_mock_token = "auto_standalone_mock".to_string();
12649        let strategy = create_auto_token_strategy(Some(auto_mock_token.clone()))
12650            .await
12651            .unwrap();
12652        assert_eq!(strategy.strategy_type(), "mock");
12653        let token = strategy.get_token().await.unwrap();
12654        assert_eq!(token, auto_mock_token);
12655
12656        // Test create_token_strategy_for_environment
12657        let env_mock_token = "env_mock".to_string();
12658        let strategy = create_token_strategy_for_environment(
12659            TokenEnvironment::Mock,
12660            Some(env_mock_token.clone()),
12661        )
12662        .await
12663        .unwrap();
12664        assert_eq!(strategy.strategy_type(), "mock");
12665        let token = strategy.get_token().await.unwrap();
12666        assert_eq!(token, env_mock_token);
12667
12668        // Clean up
12669        env::remove_var("AMP_USERNAME");
12670        env::remove_var("AMP_PASSWORD");
12671    }
12672
12673    #[test]
12674    fn test_environment_detection_with_various_credential_combinations() {
12675        // Test case 1: AMP_TESTS=live overrides everything
12676        env::set_var("AMP_TESTS", "live");
12677        env::set_var("AMP_USERNAME", "mock_user");
12678        env::set_var("AMP_PASSWORD", "mock_pass");
12679        env::set_var("AMP_API_BASE_URL", "http://localhost:8080");
12680        assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Live);
12681
12682        // Test case 2: Mock username with real password and URL
12683        env::remove_var("AMP_TESTS");
12684        env::set_var("AMP_USERNAME", "mock_user");
12685        env::set_var("AMP_PASSWORD", "real_password");
12686        env::set_var("AMP_API_BASE_URL", "https://amp-test.blockstream.com/api");
12687        assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Mock);
12688
12689        // Test case 3: Real username with mock password
12690        env::set_var("AMP_USERNAME", "real_user");
12691        env::set_var("AMP_PASSWORD", "mock_password");
12692        env::set_var("AMP_API_BASE_URL", "https://amp-test.blockstream.com/api");
12693        assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Mock);
12694
12695        // Test case 4: Real credentials with localhost URL
12696        env::set_var("AMP_USERNAME", "real_user");
12697        env::set_var("AMP_PASSWORD", "real_password");
12698        env::set_var("AMP_API_BASE_URL", "http://localhost:3000/api");
12699        assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Mock);
12700
12701        // Test case 5: All real credentials
12702        env::set_var("AMP_USERNAME", "real_user");
12703        env::set_var("AMP_PASSWORD", "real_password");
12704        env::set_var("AMP_API_BASE_URL", "https://amp-test.blockstream.com/api");
12705        assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Live);
12706
12707        // Test case 6: Empty credentials
12708        env::remove_var("AMP_USERNAME");
12709        env::remove_var("AMP_PASSWORD");
12710        env::remove_var("AMP_API_BASE_URL");
12711        assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Mock);
12712
12713        // Test case 7: Only username set
12714        env::set_var("AMP_USERNAME", "real_user");
12715        env::remove_var("AMP_PASSWORD");
12716        assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Mock);
12717
12718        // Test case 8: Only password set
12719        env::remove_var("AMP_USERNAME");
12720        env::set_var("AMP_PASSWORD", "real_password");
12721        assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Mock);
12722
12723        // Clean up all environment variables
12724        env::remove_var("AMP_TESTS");
12725        env::remove_var("AMP_USERNAME");
12726        env::remove_var("AMP_PASSWORD");
12727        env::remove_var("AMP_API_BASE_URL");
12728    }
12729
12730    #[tokio::test]
12731    async fn test_distribute_asset_input_validation() {
12732        // Create a mock client for testing
12733        let client = ApiClient::with_mock_token(
12734            reqwest::Url::parse("http://localhost:8080/api").unwrap(),
12735            "test_token".to_string(),
12736        )
12737        .unwrap();
12738
12739        // Test invalid asset UUID
12740        let assignments = vec![AssetDistributionAssignment {
12741            user_id: "user123".to_string(),
12742            address: "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
12743            amount: 100.0,
12744        }];
12745
12746        // Create a mock ElementsRpc (this will fail connection validation, but that's expected)
12747        let elements_rpc = ElementsRpc::new(
12748            "http://localhost:18884".to_string(),
12749            "user".to_string(),
12750            "pass".to_string(),
12751        );
12752
12753        // Create a mock signer
12754        let (_, signer) = LwkSoftwareSigner::generate_new().unwrap();
12755
12756        // Test with invalid UUID format
12757        let result = client
12758            .distribute_asset(
12759                "invalid-uuid",
12760                assignments.clone(),
12761                &elements_rpc,
12762                "test_wallet",
12763                &signer,
12764            )
12765            .await;
12766
12767        assert!(result.is_err());
12768        if let Err(AmpError::Validation(msg)) = result {
12769            assert!(msg.contains("Invalid asset UUID"));
12770        } else {
12771            panic!("Expected validation error for invalid UUID");
12772        }
12773
12774        // Test with empty assignments
12775        let result = client
12776            .distribute_asset(
12777                "550e8400-e29b-41d4-a716-446655440000",
12778                vec![],
12779                &elements_rpc,
12780                "test_wallet",
12781                &signer,
12782            )
12783            .await;
12784
12785        assert!(result.is_err());
12786        if let Err(AmpError::Validation(msg)) = result {
12787            assert!(msg.contains("Invalid assignments"));
12788        } else {
12789            panic!("Expected validation error for empty assignments");
12790        }
12791    }
12792
12793    #[test]
12794    fn test_validate_asset_uuid() {
12795        let _client = ApiClient::with_mock_token(
12796            reqwest::Url::parse("http://localhost:8080/api").unwrap(),
12797            "test_token".to_string(),
12798        )
12799        .unwrap();
12800
12801        // Valid UUID
12802        assert!(ApiClient::validate_asset_uuid("550e8400-e29b-41d4-a716-446655440000").is_ok());
12803
12804        // Invalid UUIDs
12805        assert!(ApiClient::validate_asset_uuid("").is_err());
12806        assert!(ApiClient::validate_asset_uuid("invalid").is_err());
12807        assert!(ApiClient::validate_asset_uuid("550e8400-e29b-41d4-a716").is_err()); // Too short
12808        assert!(
12809            ApiClient::validate_asset_uuid("550e8400-e29b-41d4-a716-446655440000-extra").is_err()
12810        ); // Too long
12811        assert!(ApiClient::validate_asset_uuid("550e8400xe29bx41d4xa716x446655440000").is_err()); // Wrong separators
12812        assert!(ApiClient::validate_asset_uuid("550e8400-e29g-41d4-a716-446655440000").is_err());
12813        // Invalid hex char
12814    }
12815
12816    #[test]
12817    fn test_validate_assignments() {
12818        let _client = ApiClient::with_mock_token(
12819            reqwest::Url::parse("http://localhost:8080/api").unwrap(),
12820            "test_token".to_string(),
12821        )
12822        .unwrap();
12823
12824        // Valid assignments
12825        let valid_assignments = vec![AssetDistributionAssignment {
12826            user_id: "user123".to_string(),
12827            address: "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
12828            amount: 100.0,
12829        }];
12830        assert!(ApiClient::validate_assignments(&valid_assignments).is_ok());
12831
12832        // Empty assignments
12833        assert!(ApiClient::validate_assignments(&[]).is_err());
12834
12835        // Assignment with empty user_id
12836        let invalid_assignments = vec![AssetDistributionAssignment {
12837            user_id: "".to_string(),
12838            address: "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
12839            amount: 100.0,
12840        }];
12841        assert!(ApiClient::validate_assignments(&invalid_assignments).is_err());
12842
12843        // Assignment with empty address
12844        let invalid_assignments = vec![AssetDistributionAssignment {
12845            user_id: "user123".to_string(),
12846            address: "".to_string(),
12847            amount: 100.0,
12848        }];
12849        assert!(ApiClient::validate_assignments(&invalid_assignments).is_err());
12850
12851        // Assignment with invalid address format
12852        let invalid_assignments = vec![AssetDistributionAssignment {
12853            user_id: "user123".to_string(),
12854            address: "invalid_address".to_string(),
12855            amount: 100.0,
12856        }];
12857        assert!(ApiClient::validate_assignments(&invalid_assignments).is_err());
12858
12859        // Assignment with non-positive amount
12860        let invalid_assignments = vec![AssetDistributionAssignment {
12861            user_id: "user123".to_string(),
12862            address: "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
12863            amount: 0.0,
12864        }];
12865        assert!(ApiClient::validate_assignments(&invalid_assignments).is_err());
12866
12867        // Assignment with unreasonably large amount
12868        let invalid_assignments = vec![AssetDistributionAssignment {
12869            user_id: "user123".to_string(),
12870            address: "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
12871            amount: 25_000_000.0,
12872        }];
12873        assert!(ApiClient::validate_assignments(&invalid_assignments).is_err());
12874    }
12875
12876    #[test]
12877    fn test_enhanced_error_handling_and_logging() {
12878        // Test AmpError creation and context enhancement
12879        let api_error = AmpError::api("Distribution creation failed");
12880        let contextual_error = api_error.with_context("Step 6: Distribution creation");
12881
12882        match contextual_error {
12883            AmpError::Api(msg) => {
12884                assert!(msg.contains("Step 6: Distribution creation"));
12885                assert!(msg.contains("Distribution creation failed"));
12886            }
12887            _ => panic!("Expected Api error variant"),
12888        }
12889
12890        // Test retry instructions for different error types
12891        let rpc_error = AmpError::rpc("Connection failed");
12892        assert!(rpc_error.is_retryable());
12893        assert!(rpc_error.retry_instructions().is_some());
12894        assert!(rpc_error
12895            .retry_instructions()
12896            .unwrap()
12897            .contains("Elements node"));
12898
12899        let validation_error = AmpError::validation("Invalid UUID");
12900        assert!(!validation_error.is_retryable());
12901        assert!(validation_error.retry_instructions().is_none());
12902
12903        let timeout_error = AmpError::timeout("Confirmation timeout for txid abc123");
12904        assert!(!timeout_error.is_retryable());
12905        let instructions = timeout_error.retry_instructions();
12906        assert!(instructions.is_some());
12907        assert!(instructions.unwrap().contains("transaction ID"));
12908
12909        // Test error helper methods
12910        let signer_error =
12911            AmpError::Signer(crate::signer::SignerError::Lwk("Test error".to_string()));
12912        assert!(!signer_error.is_retryable());
12913        assert!(signer_error.retry_instructions().is_none());
12914
12915        // Test serialization error
12916        let json_error = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
12917        let serialization_error = AmpError::from(json_error);
12918        assert!(matches!(serialization_error, AmpError::Serialization(_)));
12919        assert!(!serialization_error.is_retryable());
12920    }
12921}