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 secrecy::ExposeSecret;
16use secrecy::Secret;
17
18use crate::model::{
19 Activity, Asset, AssetActivityParams, AssetSummary, Assignment, Balance, BroadcastResponse,
20 CategoriesRequest, CategoryAdd, CategoryEdit, CategoryResponse, ChangePasswordRequest,
21 ChangePasswordResponse, CreateAssetAssignmentRequest, EditAssetRequest, GaidBalanceEntry,
22 GaidRequest, IssuanceRequest, IssuanceResponse, Outpoint, Ownership, Password, TokenData,
23 TokenInfo, TokenRequest, TokenResponse, Utxo,
24};
25
26/// Environment variables used for token environment detection
27#[derive(Debug)]
28struct EnvironmentVariables {
29 username: String,
30 password: String,
31 amp_tests: String,
32 base_url: String,
33}
34
35/// Token environment detection for automatic strategy selection
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum TokenEnvironment {
38 /// Mock environment - use isolated token management without persistence
39 Mock,
40 /// Live environment - use full token management with persistence
41 Live,
42 /// Auto-detect environment based on credentials and settings
43 Auto,
44}
45
46impl TokenEnvironment {
47 /// Detects the current token environment based on environment variables and credential patterns
48 ///
49 /// Detection logic:
50 /// 1. If `AMP_TESTS=live` is set, returns `Live`
51 /// 2. If credentials contain "mock" string, returns `Mock`
52 /// 3. If real credentials are present without live test flag, returns `Live`
53 /// 4. Fallback to `Mock` for safety
54 #[must_use]
55 pub fn detect() -> Self {
56 let env_vars = Self::read_environment_variables();
57 Self::log_detection_start(&env_vars);
58
59 if Self::is_explicit_live_environment(&env_vars.amp_tests) {
60 return Self::Live;
61 }
62
63 if Self::has_mock_credentials(&env_vars.username, &env_vars.password, &env_vars.base_url) {
64 Self::log_detection_result("mock environment via mock credentials");
65 return Self::Mock;
66 }
67
68 if Self::has_real_credentials(&env_vars.username, &env_vars.password) {
69 Self::log_detection_result("live environment via real credentials");
70 return Self::Live;
71 }
72
73 Self::log_detection_result("mock environment via fallback (no credentials)");
74 Self::Mock
75 }
76
77 /// Reads environment variables needed for token environment detection
78 fn read_environment_variables() -> EnvironmentVariables {
79 EnvironmentVariables {
80 username: env::var("AMP_USERNAME").unwrap_or_default(),
81 password: env::var("AMP_PASSWORD").unwrap_or_default(),
82 amp_tests: env::var("AMP_TESTS").unwrap_or_default(),
83 base_url: env::var("AMP_API_BASE_URL").unwrap_or_default(),
84 }
85 }
86
87 /// Logs the start of environment detection with current variable values
88 fn log_detection_start(env_vars: &EnvironmentVariables) {
89 tracing::debug!(
90 "Detecting token environment - AMP_TESTS: '{}', username: '{}', base_url: '{}'",
91 env_vars.amp_tests,
92 env_vars.username,
93 env_vars.base_url
94 );
95 }
96
97 /// Checks if the environment is explicitly set to live testing
98 fn is_explicit_live_environment(amp_tests: &str) -> bool {
99 if amp_tests == "live" {
100 Self::log_detection_result("live environment via AMP_TESTS=live");
101 true
102 } else {
103 false
104 }
105 }
106
107 /// Checks if real (non-empty) credentials are present
108 const fn has_real_credentials(username: &str, password: &str) -> bool {
109 !username.is_empty() && !password.is_empty()
110 }
111
112 /// Logs the final detection result
113 fn log_detection_result(reason: &str) {
114 tracing::info!("Detected {}", reason);
115 }
116
117 /// Checks if the provided credentials indicate a mock environment
118 ///
119 /// Mock credentials are detected by:
120 /// - Username containing "mock" (case-insensitive)
121 /// - Password containing "mock" (case-insensitive)
122 /// - Base URL containing localhost, 127.0.0.1, or "mock"
123 #[must_use]
124 pub fn has_mock_credentials(username: &str, password: &str, base_url: &str) -> bool {
125 let username_lower = username.to_lowercase();
126 let password_lower = password.to_lowercase();
127 let base_url_lower = base_url.to_lowercase();
128
129 let has_mock_username = username_lower.contains("mock");
130 let has_mock_password = password_lower.contains("mock");
131 let has_mock_url = base_url_lower.contains("localhost")
132 || base_url_lower.contains("127.0.0.1")
133 || base_url_lower.contains("mock");
134
135 let is_mock = has_mock_username || has_mock_password || has_mock_url;
136
137 tracing::debug!(
138 "Mock credential check - username: {}, password: {}, url: {}, result: {}",
139 has_mock_username,
140 has_mock_password,
141 has_mock_url,
142 is_mock
143 );
144
145 is_mock
146 }
147
148 /// Creates a token strategy based on the environment type
149 ///
150 /// # Arguments
151 /// * `mock_token` - Optional mock token to use for mock environments
152 ///
153 /// # Errors
154 /// Returns an error if strategy creation fails
155 pub async fn create_strategy(
156 &self,
157 mock_token: Option<String>,
158 ) -> Result<Box<dyn TokenStrategy>, Error> {
159 match self {
160 Self::Mock => Ok(Self::create_mock_strategy(mock_token)),
161 Self::Live => Self::create_live_strategy().await,
162 Self::Auto => Self::create_auto_detected_strategy(mock_token).await,
163 }
164 }
165
166 /// Creates a mock token strategy with the provided or default token
167 fn create_mock_strategy(mock_token: Option<String>) -> Box<dyn TokenStrategy> {
168 let token = mock_token.unwrap_or_else(|| "default_mock_token".to_string());
169 tracing::debug!("Creating mock token strategy with token");
170 Box::new(MockTokenStrategy::new(token))
171 }
172
173 /// Creates a live token strategy
174 async fn create_live_strategy() -> Result<Box<dyn TokenStrategy>, Error> {
175 tracing::debug!("Creating live token strategy");
176 let strategy = LiveTokenStrategy::new().await?;
177 Ok(Box::new(strategy))
178 }
179
180 /// Creates a strategy based on auto-detected environment
181 async fn create_auto_detected_strategy(
182 mock_token: Option<String>,
183 ) -> Result<Box<dyn TokenStrategy>, Error> {
184 tracing::debug!("Auto-detecting environment for strategy creation");
185 let detected = Self::detect();
186
187 match detected {
188 Self::Mock => Ok(Self::create_auto_detected_mock_strategy(mock_token)),
189 Self::Live => Self::create_auto_detected_live_strategy().await,
190 Self::Auto => Self::handle_unexpected_auto_detection(),
191 }
192 }
193
194 /// Creates a mock strategy for auto-detected mock environment
195 fn create_auto_detected_mock_strategy(mock_token: Option<String>) -> Box<dyn TokenStrategy> {
196 let token = mock_token.unwrap_or_else(|| "default_mock_token".to_string());
197 tracing::debug!("Auto-detected mock environment, creating mock strategy");
198 Box::new(MockTokenStrategy::new(token))
199 }
200
201 /// Creates a live strategy for auto-detected live environment
202 async fn create_auto_detected_live_strategy() -> Result<Box<dyn TokenStrategy>, Error> {
203 tracing::debug!("Auto-detected live environment, creating live strategy");
204 let strategy = LiveTokenStrategy::new().await?;
205 Ok(Box::new(strategy))
206 }
207
208 /// Handles the unexpected case where `detect()` returns Auto
209 fn handle_unexpected_auto_detection() -> Result<Box<dyn TokenStrategy>, Error> {
210 tracing::error!("Unexpected Auto environment from detect()");
211 Err(Error::Token(TokenError::validation(
212 "Environment detection returned Auto, which should not happen".to_string(),
213 )))
214 }
215
216 /// Creates a token strategy with automatic environment detection
217 ///
218 /// This is a convenience method that combines environment detection with strategy creation.
219 ///
220 /// # Arguments
221 /// * `mock_token` - Optional mock token to use if mock environment is detected
222 ///
223 /// # Errors
224 /// Returns an error if strategy creation fails
225 pub async fn create_auto_strategy(
226 mock_token: Option<String>,
227 ) -> Result<Box<dyn TokenStrategy>, Error> {
228 let environment = Self::detect();
229 environment.create_strategy(mock_token).await
230 }
231
232 /// Determines if token persistence should be enabled for this environment
233 #[must_use]
234 pub fn should_persist_tokens(&self) -> bool {
235 match self {
236 Self::Mock => false,
237 Self::Live => true,
238 Self::Auto => Self::detect().should_persist_tokens(),
239 }
240 }
241
242 /// Returns true if this is a mock environment
243 #[must_use]
244 pub fn is_mock(&self) -> bool {
245 matches!(self, Self::Mock) || (matches!(self, Self::Auto) && Self::detect().is_mock())
246 }
247
248 /// Returns true if this is a live environment
249 #[must_use]
250 pub fn is_live(&self) -> bool {
251 matches!(self, Self::Live) || (matches!(self, Self::Auto) && Self::detect().is_live())
252 }
253}
254
255/// Token management strategy trait for different token handling approaches
256#[async_trait]
257pub trait TokenStrategy: Send + Sync + std::fmt::Debug {
258 /// Gets a valid authentication token
259 async fn get_token(&self) -> Result<String, Error>;
260
261 /// Clears stored token (for testing)
262 async fn clear_token(&self) -> Result<(), Error>;
263
264 /// Returns whether this strategy should persist tokens
265 fn should_persist(&self) -> bool;
266
267 /// Returns the strategy type for debugging
268 fn strategy_type(&self) -> &'static str;
269
270 /// Returns self as Any for downcasting (used internally)
271 fn as_any(&self) -> &dyn std::any::Any;
272}
273
274/// Mock token strategy for isolated testing without persistence
275#[derive(Debug, Clone)]
276pub struct MockTokenStrategy {
277 token: String,
278}
279
280impl MockTokenStrategy {
281 /// Creates a new mock token strategy with the provided token
282 #[must_use]
283 pub const fn new(token: String) -> Self {
284 Self { token }
285 }
286
287 /// Creates a mock token strategy with a default test token
288 #[must_use]
289 pub fn with_default_token() -> Self {
290 Self::new("mock_token_default".to_string())
291 }
292
293 /// Creates a mock token strategy for a specific test case
294 #[must_use]
295 pub fn for_test(test_name: &str) -> Self {
296 Self::new(format!("mock_token_{test_name}"))
297 }
298}
299
300#[async_trait]
301impl TokenStrategy for MockTokenStrategy {
302 async fn get_token(&self) -> Result<String, Error> {
303 tracing::debug!("Using mock token strategy - returning pre-set token");
304 Ok(self.token.clone())
305 }
306
307 async fn clear_token(&self) -> Result<(), Error> {
308 tracing::debug!("Mock token strategy - clear_token is a no-op");
309 Ok(())
310 }
311
312 fn should_persist(&self) -> bool {
313 false
314 }
315
316 fn strategy_type(&self) -> &'static str {
317 "mock"
318 }
319
320 fn as_any(&self) -> &dyn std::any::Any {
321 self
322 }
323}
324
325/// Live token strategy that wraps the existing `TokenManager` for full token management
326#[derive(Debug)]
327pub struct LiveTokenStrategy {
328 token_manager: Arc<TokenManager>,
329}
330
331impl LiveTokenStrategy {
332 /// Creates a new live token strategy using the global `TokenManager` instance
333 ///
334 /// # Errors
335 /// Returns an error if the `TokenManager` cannot be initialized
336 pub async fn new() -> Result<Self, Error> {
337 let token_manager = TokenManager::get_global_instance().await?;
338 Ok(Self { token_manager })
339 }
340
341 /// Creates a new live token strategy with a custom `TokenManager`
342 #[must_use]
343 pub const fn with_token_manager(token_manager: Arc<TokenManager>) -> Self {
344 Self { token_manager }
345 }
346
347 /// Creates a live token strategy with custom retry configuration
348 ///
349 /// # Errors
350 /// Returns an error if the `TokenManager` cannot be initialized
351 pub async fn with_config(config: RetryConfig) -> Result<Self, Error> {
352 let base_url = get_amp_api_base_url()?;
353 let token_manager =
354 Arc::new(TokenManager::with_config_and_base_url(config, base_url).await?);
355 Ok(Self { token_manager })
356 }
357
358 /// Creates a live token strategy optimized for testing
359 ///
360 /// # Errors
361 /// Returns an error if the `TokenManager` cannot be initialized
362 pub async fn for_testing() -> Result<Self, Error> {
363 let config = RetryConfig::for_tests();
364 Self::with_config(config).await
365 }
366
367 /// Gets current token information for debugging and monitoring
368 ///
369 /// # Errors
370 /// Returns an error if token information retrieval fails
371 pub async fn get_token_info(&self) -> Result<Option<TokenInfo>, Error> {
372 self.token_manager.get_token_info().await
373 }
374}
375
376#[async_trait]
377impl TokenStrategy for LiveTokenStrategy {
378 async fn get_token(&self) -> Result<String, Error> {
379 tracing::debug!("Using live token strategy - full token management");
380 self.token_manager.get_token().await
381 }
382
383 async fn clear_token(&self) -> Result<(), Error> {
384 self.token_manager.clear_token().await
385 }
386
387 fn should_persist(&self) -> bool {
388 true
389 }
390
391 fn strategy_type(&self) -> &'static str {
392 "live"
393 }
394
395 fn as_any(&self) -> &dyn std::any::Any {
396 self
397 }
398}
399
400#[derive(Error, Debug)]
401pub enum Error {
402 #[error("Missing {0} environment variable")]
403 MissingEnvVar(String),
404 #[error("AMP request failed: {0}")]
405 RequestFailed(String),
406 #[error("Failed to parse AMP response: {0}")]
407 ResponseParsingFailed(String),
408 #[error("AMP token request failed with status {status}: {error_text}")]
409 TokenRequestFailed {
410 status: reqwest::StatusCode,
411 error_text: String,
412 },
413 #[error("Failed to parse url: {0}")]
414 UrlParse(#[from] url::ParseError),
415 #[error("Reqwest error: {0}")]
416 Reqwest(#[from] reqwest::Error),
417 #[error("Invalid retry configuration: {0}")]
418 InvalidRetryConfig(String),
419 #[error("Token management error: {0}")]
420 Token(#[from] TokenError),
421}
422
423/// Detailed error types for token management operations
424#[derive(Error, Debug, Clone, PartialEq, Eq)]
425pub enum TokenError {
426 #[error("Token refresh failed: {0}")]
427 RefreshFailed(String),
428 #[error("Token obtain failed after {attempts} attempts: {last_error}")]
429 ObtainFailed { attempts: u32, last_error: String },
430 #[error("Rate limited: retry after {retry_after_seconds} seconds")]
431 RateLimited { retry_after_seconds: u64 },
432 #[error("Request timeout after {timeout_seconds} seconds")]
433 Timeout { timeout_seconds: u64 },
434 #[error("Serialization error: {0}")]
435 Serialization(String),
436 #[error("Token storage error: {0}")]
437 Storage(String),
438 #[error("Token validation error: {0}")]
439 Validation(String),
440}
441
442impl TokenError {
443 /// Creates a new `RefreshFailed` error
444 #[must_use]
445 pub fn refresh_failed<S: Into<String>>(message: S) -> Self {
446 Self::RefreshFailed(message.into())
447 }
448
449 /// Creates a new `ObtainFailed` error
450 #[must_use]
451 pub const fn obtain_failed(attempts: u32, last_error: String) -> Self {
452 Self::ObtainFailed {
453 attempts,
454 last_error,
455 }
456 }
457
458 /// Creates a new `RateLimited` error
459 #[must_use]
460 pub const fn rate_limited(retry_after_seconds: u64) -> Self {
461 Self::RateLimited {
462 retry_after_seconds,
463 }
464 }
465
466 /// Creates a new Timeout error
467 #[must_use]
468 pub const fn timeout(timeout_seconds: u64) -> Self {
469 Self::Timeout { timeout_seconds }
470 }
471
472 /// Creates a new Serialization error
473 #[must_use]
474 pub fn serialization<S: Into<String>>(message: S) -> Self {
475 Self::Serialization(message.into())
476 }
477
478 /// Creates a new Storage error
479 #[must_use]
480 pub fn storage<S: Into<String>>(message: S) -> Self {
481 Self::Storage(message.into())
482 }
483
484 /// Creates a new Validation error
485 #[must_use]
486 pub fn validation<S: Into<String>>(message: S) -> Self {
487 Self::Validation(message.into())
488 }
489
490 /// Returns true if this error indicates a retryable condition
491 #[must_use]
492 pub const fn is_retryable(&self) -> bool {
493 matches!(
494 self,
495 Self::RefreshFailed(_) | Self::RateLimited { .. } | Self::Timeout { .. }
496 )
497 }
498
499 /// Returns true if this error indicates a rate limiting condition
500 #[must_use]
501 pub const fn is_rate_limited(&self) -> bool {
502 matches!(self, Self::RateLimited { .. })
503 }
504
505 /// Returns the retry delay in seconds if this is a rate limited error
506 #[must_use]
507 pub const fn retry_after_seconds(&self) -> Option<u64> {
508 match self {
509 Self::RateLimited {
510 retry_after_seconds,
511 } => Some(*retry_after_seconds),
512 _ => None,
513 }
514 }
515}
516
517// Conversion from serde_json::Error for serialization errors
518impl From<serde_json::Error> for TokenError {
519 fn from(err: serde_json::Error) -> Self {
520 Self::Serialization(err.to_string())
521 }
522}
523
524/// Configuration for retry behavior in API requests
525#[derive(Debug, Clone)]
526pub struct RetryConfig {
527 /// Maximum number of retry attempts
528 pub max_attempts: u32,
529 /// Base delay in milliseconds for exponential backoff
530 pub base_delay_ms: u64,
531 /// Maximum delay in milliseconds to cap exponential backoff
532 pub max_delay_ms: u64,
533 /// Request timeout in seconds
534 pub timeout_seconds: u64,
535}
536
537impl Default for RetryConfig {
538 fn default() -> Self {
539 Self {
540 max_attempts: 3,
541 base_delay_ms: 1000,
542 max_delay_ms: 30_000,
543 timeout_seconds: 10,
544 }
545 }
546}
547
548impl RetryConfig {
549 /// Creates a `RetryConfig` from environment variables with default fallbacks
550 ///
551 /// Environment variables:
552 /// - `API_RETRY_MAX_ATTEMPTS`: Maximum retry attempts (default: 3)
553 /// - `API_RETRY_BASE_DELAY_MS`: Base delay in milliseconds (default: 1000)
554 /// - `API_RETRY_MAX_DELAY_MS`: Maximum delay in milliseconds (default: 30000)
555 /// - `API_REQUEST_TIMEOUT_SECONDS`: Request timeout in seconds (default: 10)
556 ///
557 /// # Errors
558 ///
559 /// Returns an error if any environment variable contains an invalid value
560 pub fn from_env() -> Result<Self, Error> {
561 let max_attempts = match env::var("API_RETRY_MAX_ATTEMPTS") {
562 Ok(val) => val.parse::<u32>().map_err(|e| {
563 Error::InvalidRetryConfig(format!("Invalid API_RETRY_MAX_ATTEMPTS: {e}"))
564 })?,
565 Err(_) => 3,
566 };
567
568 let base_delay_ms = match env::var("API_RETRY_BASE_DELAY_MS") {
569 Ok(val) => val.parse::<u64>().map_err(|e| {
570 Error::InvalidRetryConfig(format!("Invalid API_RETRY_BASE_DELAY_MS: {e}"))
571 })?,
572 Err(_) => 1000,
573 };
574
575 let max_delay_ms = match env::var("API_RETRY_MAX_DELAY_MS") {
576 Ok(val) => val.parse::<u64>().map_err(|e| {
577 Error::InvalidRetryConfig(format!("Invalid API_RETRY_MAX_DELAY_MS: {e}"))
578 })?,
579 Err(_) => 30_000,
580 };
581
582 let timeout_seconds = match env::var("API_REQUEST_TIMEOUT_SECONDS") {
583 Ok(val) => val.parse::<u64>().map_err(|e| {
584 Error::InvalidRetryConfig(format!("Invalid API_REQUEST_TIMEOUT_SECONDS: {e}"))
585 })?,
586 Err(_) => 10,
587 };
588
589 // Validate configuration
590 if max_attempts == 0 {
591 return Err(Error::InvalidRetryConfig(
592 "max_attempts must be greater than 0".to_string(),
593 ));
594 }
595 if base_delay_ms == 0 {
596 return Err(Error::InvalidRetryConfig(
597 "base_delay_ms must be greater than 0".to_string(),
598 ));
599 }
600 if max_delay_ms < base_delay_ms {
601 return Err(Error::InvalidRetryConfig(
602 "max_delay_ms must be greater than or equal to base_delay_ms".to_string(),
603 ));
604 }
605 if timeout_seconds == 0 {
606 return Err(Error::InvalidRetryConfig(
607 "timeout_seconds must be greater than 0".to_string(),
608 ));
609 }
610
611 Ok(Self {
612 max_attempts,
613 base_delay_ms,
614 max_delay_ms,
615 timeout_seconds,
616 })
617 }
618
619 /// Creates a `RetryConfig` optimized for test environments
620 ///
621 /// Uses reduced values for faster test execution:
622 /// - 2 retry attempts
623 /// - 500ms base delay
624 /// - 5000ms max delay
625 /// - 5 second timeout
626 #[must_use]
627 pub const fn for_tests() -> Self {
628 Self {
629 max_attempts: 2,
630 base_delay_ms: 500,
631 max_delay_ms: 5000,
632 timeout_seconds: 5,
633 }
634 }
635
636 /// Sets a custom timeout value
637 #[must_use]
638 pub const fn with_timeout(mut self, timeout_seconds: u64) -> Self {
639 self.timeout_seconds = timeout_seconds;
640 self
641 }
642
643 /// Sets custom max attempts
644 #[must_use]
645 pub const fn with_max_attempts(mut self, max_attempts: u32) -> Self {
646 self.max_attempts = max_attempts;
647 self
648 }
649
650 /// Sets custom base delay
651 #[must_use]
652 pub const fn with_base_delay_ms(mut self, base_delay_ms: u64) -> Self {
653 self.base_delay_ms = base_delay_ms;
654 self
655 }
656
657 /// Sets custom max delay
658 #[must_use]
659 pub const fn with_max_delay_ms(mut self, max_delay_ms: u64) -> Self {
660 self.max_delay_ms = max_delay_ms;
661 self
662 }
663}
664
665/// HTTP client with sophisticated retry logic and exponential backoff
666#[derive(Debug, Clone)]
667pub struct RetryClient {
668 client: Client,
669 config: RetryConfig,
670}
671
672impl RetryClient {
673 /// Creates a new `RetryClient` with the given configuration
674 #[must_use]
675 pub fn new(config: RetryConfig) -> Self {
676 Self {
677 client: Client::new(),
678 config,
679 }
680 }
681
682 /// Creates a new `RetryClient` with default configuration
683 #[must_use]
684 pub fn with_default_config() -> Self {
685 Self::new(RetryConfig::default())
686 }
687
688 /// Creates a new `RetryClient` with test-optimized configuration
689 #[must_use]
690 pub fn for_tests() -> Self {
691 Self::new(RetryConfig::for_tests())
692 }
693
694 /// Executes an HTTP request with retry logic and exponential backoff
695 ///
696 /// # Arguments
697 /// * `request_builder` - A function that creates the request builder
698 ///
699 /// # Returns
700 /// The response if successful, or an error after all retries are exhausted
701 ///
702 /// # Errors
703 /// Returns `TokenError::Timeout` if the request times out
704 /// Returns `TokenError::RateLimited` if rate limited and retries are exhausted
705 /// Returns `TokenError::ObtainFailed` if all retry attempts fail
706 #[allow(clippy::cognitive_complexity)]
707 pub async fn execute_with_retry<F>(
708 &self,
709 request_builder: F,
710 ) -> Result<reqwest::Response, TokenError>
711 where
712 F: Fn() -> reqwest::RequestBuilder + Send + Sync,
713 {
714 let mut last_error = String::new();
715 let mut attempt = 0;
716
717 while attempt < self.config.max_attempts {
718 attempt += 1;
719
720 // Create the request with timeout
721 let request =
722 request_builder().timeout(StdDuration::from_secs(self.config.timeout_seconds));
723
724 // Execute the request
725 match request.send().await {
726 Ok(response) => {
727 let status = response.status();
728
729 // Handle rate limiting (429 Too Many Requests)
730 if status == reqwest::StatusCode::TOO_MANY_REQUESTS {
731 let retry_after = Self::extract_retry_after(&response).unwrap_or(60);
732
733 tracing::warn!(
734 "Rate limited (429) on attempt {}/{}. Retry after {} seconds",
735 attempt,
736 self.config.max_attempts,
737 retry_after
738 );
739
740 // If this is our last attempt, return the rate limit error
741 if attempt >= self.config.max_attempts {
742 return Err(TokenError::rate_limited(retry_after));
743 }
744
745 // Wait for the rate limit period (or our max delay, whichever is smaller)
746 let delay_ms = std::cmp::min(retry_after * 1000, self.config.max_delay_ms);
747 sleep(StdDuration::from_millis(delay_ms)).await;
748 continue;
749 }
750
751 // Handle other client errors (4xx) - these are generally not retryable
752 if status.is_client_error() && status != reqwest::StatusCode::TOO_MANY_REQUESTS
753 {
754 last_error = format!("Client error: {status}");
755 tracing::error!("Non-retryable client error: {}", status);
756 break;
757 }
758
759 // Handle server errors (5xx) - these are retryable
760 if status.is_server_error() {
761 last_error = format!("Server error: {status}");
762 tracing::warn!(
763 "Server error {} on attempt {}/{}",
764 status,
765 attempt,
766 self.config.max_attempts
767 );
768
769 if attempt < self.config.max_attempts {
770 let delay = self.calculate_backoff_delay(attempt);
771 sleep(delay).await;
772 continue;
773 }
774 break;
775 }
776
777 // Success case
778 return Ok(response);
779 }
780 Err(e) => {
781 last_error = e.to_string();
782
783 // Check if this is a timeout error
784 if e.is_timeout() {
785 tracing::warn!(
786 "Request timeout on attempt {}/{}",
787 attempt,
788 self.config.max_attempts
789 );
790
791 if attempt >= self.config.max_attempts {
792 return Err(TokenError::timeout(self.config.timeout_seconds));
793 }
794 } else {
795 tracing::warn!(
796 "Request failed on attempt {}/{}: {}",
797 attempt,
798 self.config.max_attempts,
799 e
800 );
801 }
802
803 // If we have more attempts, wait and retry
804 if attempt < self.config.max_attempts {
805 let delay = self.calculate_backoff_delay(attempt);
806 sleep(delay).await;
807 }
808 }
809 }
810 }
811
812 // All retries exhausted
813 Err(TokenError::obtain_failed(attempt, last_error))
814 }
815
816 /// Calculates the delay for exponential backoff with jitter
817 ///
818 /// Uses the formula: `min(base_delay * 2^(attempt-1) + jitter, max_delay)`
819 /// where jitter is a random value between 0 and `base_delay/2`
820 pub fn calculate_backoff_delay(&self, attempt: u32) -> StdDuration {
821 use rand::Rng;
822
823 let base_delay = self.config.base_delay_ms;
824 let max_delay = self.config.max_delay_ms;
825
826 // Calculate exponential backoff: base_delay * 2^(attempt-1)
827 let exponential_delay = base_delay * 2_u64.pow(attempt.saturating_sub(1));
828
829 // Add jitter (random value between 0 and base_delay/2)
830 let jitter = rand::thread_rng().gen_range(0..=base_delay / 2);
831 let total_delay = exponential_delay + jitter;
832
833 // Cap at max_delay
834 let final_delay = std::cmp::min(total_delay, max_delay);
835
836 tracing::debug!(
837 "Calculated backoff delay for attempt {}: {}ms (exponential: {}ms, jitter: {}ms, capped at: {}ms)",
838 attempt,
839 final_delay,
840 exponential_delay,
841 jitter,
842 max_delay
843 );
844
845 StdDuration::from_millis(final_delay)
846 }
847
848 /// Extracts the Retry-After header value from a 429 response
849 ///
850 /// Returns the number of seconds to wait, or None if the header is not present
851 /// or cannot be parsed
852 fn extract_retry_after(response: &reqwest::Response) -> Option<u64> {
853 response
854 .headers()
855 .get("retry-after")
856 .and_then(|value| value.to_str().ok())
857 .and_then(|s| s.parse::<u64>().ok())
858 }
859
860 /// Gets the underlying reqwest client
861 #[must_use]
862 pub const fn client(&self) -> &Client {
863 &self.client
864 }
865
866 /// Gets the retry configuration
867 #[must_use]
868 pub const fn config(&self) -> &RetryConfig {
869 &self.config
870 }
871}
872
873/// Singleton instance of the `TokenManager` for shared token storage across all `ApiClient` instances
874static GLOBAL_TOKEN_MANAGER: OnceCell<Arc<TokenManager>> = OnceCell::const_new();
875
876/// Core token manager with proactive refresh and secure storage
877#[derive(Debug)]
878pub struct TokenManager {
879 pub token_data: Arc<Mutex<Option<TokenData>>>,
880 pub retry_client: RetryClient,
881 base_url: Url,
882 /// Semaphore to ensure only one token operation (obtain/refresh) happens at a time
883 /// This prevents race conditions where multiple threads try to refresh/obtain simultaneously
884 token_operation_semaphore: Arc<Semaphore>,
885}
886
887impl TokenManager {
888 /// Gets the global singleton instance of `TokenManager`
889 ///
890 /// This ensures all `ApiClient` instances share the same token storage,
891 /// preventing multiple token acquisition attempts in concurrent tests.
892 ///
893 /// # Errors
894 /// Returns an error if the `TokenManager` cannot be initialized
895 pub async fn get_global_instance() -> Result<Arc<Self>, Error> {
896 let manager = GLOBAL_TOKEN_MANAGER
897 .get_or_try_init(|| async {
898 let config = RetryConfig::from_env()?;
899 let base_url = get_amp_api_base_url()?;
900 let manager = Self::with_config_and_base_url(config, base_url).await?;
901 Ok::<Arc<Self>, Error>(Arc::new(manager))
902 })
903 .await?;
904
905 Ok(manager.clone())
906 }
907
908 /// Creates a new `TokenManager` with default configuration
909 ///
910 /// # Errors
911 /// Returns an error if the base URL cannot be obtained from environment variables
912 pub async fn new() -> Result<Self, Error> {
913 let config = RetryConfig::from_env()?;
914 Self::with_config(config).await
915 }
916
917 /// Creates a new `TokenManager` with the specified retry configuration
918 ///
919 /// # Errors
920 /// Returns an error if the base URL cannot be obtained from environment variables
921 pub async fn with_config(config: RetryConfig) -> Result<Self, Error> {
922 let base_url = get_amp_api_base_url()?;
923 Self::with_config_and_base_url(config, base_url).await
924 }
925
926 /// Creates a new `TokenManager` with the specified configuration and base URL (for testing)
927 ///
928 /// # Errors
929 /// This method is infallible but returns Result for API consistency
930 pub async fn with_config_and_base_url(
931 config: RetryConfig,
932 base_url: Url,
933 ) -> Result<Self, Error> {
934 let manager = Self {
935 token_data: Arc::new(Mutex::new(None)),
936 retry_client: RetryClient::new(config),
937 base_url,
938 token_operation_semaphore: Arc::new(Semaphore::new(1)),
939 };
940
941 // Load token from disk if persistence is enabled
942 if Self::should_persist_tokens() {
943 if let Ok(Some(token_data)) = manager.load_token_from_disk().await {
944 *manager.token_data.lock().await = Some(token_data);
945 tracing::info!("Token loaded from disk during initialization");
946 }
947 }
948
949 Ok(manager)
950 }
951
952 /// Creates a new `TokenManager` with a pre-set mock token (for testing)
953 ///
954 /// # Errors
955 /// This method is infallible but returns Result for API consistency
956 pub fn with_mock_token(
957 config: RetryConfig,
958 base_url: Url,
959 mock_token: String,
960 ) -> Result<Self, Error> {
961 let expires_at = Utc::now() + Duration::hours(24); // Mock token valid for 24 hours
962 let token_data = TokenData::new(mock_token, expires_at);
963
964 let manager = Self {
965 token_data: Arc::new(Mutex::new(Some(token_data))),
966 retry_client: RetryClient::new(config),
967 base_url,
968 token_operation_semaphore: Arc::new(Semaphore::new(1)),
969 };
970
971 Ok(manager)
972 }
973
974 /// Gets a valid authentication token with proactive refresh logic
975 ///
976 /// This method implements thread-safe token management logic:
977 /// 1. Check if a valid token exists and is not expiring soon (within 5 minutes)
978 /// 2. If token needs refresh/obtain, acquire semaphore to prevent concurrent operations
979 /// 3. Double-check token state after acquiring semaphore (another thread may have updated it)
980 /// 4. Perform atomic token update operations
981 /// 5. Return the valid token
982 ///
983 /// # Thread Safety
984 /// This method is fully thread-safe and prevents race conditions by:
985 /// - Using a semaphore to ensure only one token operation at a time
986 /// - Double-checking token state after acquiring the semaphore
987 /// - Performing atomic token updates within the critical section
988 ///
989 /// # Errors
990 /// Returns a `TokenError` if token acquisition or refresh fails after all retries
991 pub async fn get_token(&self) -> Result<String, Error> {
992 // Fast path: check if we have a valid token without acquiring semaphore
993 if let Some(token) = self.check_existing_token().await? {
994 return Ok(token);
995 }
996
997 // Slow path: token needs refresh/obtain, acquire semaphore for thread safety
998 let _permit = self.acquire_token_semaphore().await?;
999
1000 // Double-check token state after acquiring semaphore - another thread may have updated it
1001 if let Some(token) = self.check_existing_token().await? {
1002 tracing::debug!("Token was updated by another thread, using existing valid token");
1003 return Ok(token);
1004 }
1005
1006 // At this point, we need to refresh or obtain a new token
1007 self.handle_token_refresh_or_obtain().await
1008 }
1009
1010 /// Checks if we have a valid existing token that doesn't expire soon
1011 async fn check_existing_token(&self) -> Result<Option<String>, Error> {
1012 let token_guard = self.token_data.lock().await;
1013 if let Some(ref token_data) = *token_guard {
1014 if !token_data.expires_soon(Duration::minutes(5)) {
1015 tracing::debug!("Using existing valid token");
1016 let token = token_data.token.expose_secret().clone();
1017 drop(token_guard);
1018 return Ok(Some(token));
1019 }
1020 }
1021 drop(token_guard);
1022 Ok(None)
1023 }
1024
1025 /// Acquires the token operation semaphore for thread-safe operations
1026 async fn acquire_token_semaphore(&self) -> Result<tokio::sync::SemaphorePermit<'_>, Error> {
1027 let permit = self
1028 .token_operation_semaphore
1029 .acquire()
1030 .await
1031 .map_err(|e| {
1032 Error::Token(TokenError::storage(format!(
1033 "Failed to acquire token operation semaphore: {e}"
1034 )))
1035 })?;
1036
1037 tracing::debug!("Acquired token operation semaphore for thread-safe token management");
1038 Ok(permit)
1039 }
1040
1041 /// Handles the token refresh or obtain logic
1042 async fn handle_token_refresh_or_obtain(&self) -> Result<String, Error> {
1043 let needs_refresh = self.determine_token_operation().await;
1044
1045 if needs_refresh {
1046 match self.refresh_token_internal().await {
1047 Ok(token) => {
1048 tracing::info!("Token refreshed successfully");
1049 return Ok(token);
1050 }
1051 Err(e) => {
1052 tracing::warn!("Token refresh failed, falling back to obtain: {e}");
1053 // Fall through to obtain new token
1054 }
1055 }
1056 }
1057
1058 // Either we needed to obtain from the start, or refresh failed
1059 self.obtain_token_internal().await
1060 }
1061
1062 /// Determines whether we need to refresh or obtain a new token
1063 async fn determine_token_operation(&self) -> bool {
1064 let token_guard = self.token_data.lock().await;
1065 token_guard.as_ref().map_or_else(
1066 || {
1067 tracing::info!("No token exists, will obtain new token");
1068 false
1069 },
1070 |token_data| {
1071 if token_data.is_expired() {
1072 tracing::info!("Token is expired, will obtain new token");
1073 false
1074 } else {
1075 tracing::info!("Token expires soon, will attempt refresh");
1076 true
1077 }
1078 },
1079 )
1080 }
1081
1082 /// Obtains a new authentication token using environment credentials with retry logic
1083 ///
1084 /// This method:
1085 /// 1. Reads credentials from environment variables
1086 /// 2. Makes a token request with retry logic
1087 /// 3. Stores the new token with 24-hour expiry
1088 /// 4. Returns the token string
1089 ///
1090 /// # Thread Safety
1091 /// This method acquires the token operation semaphore to ensure thread-safe operation.
1092 /// For internal use within already-synchronized contexts, use `obtain_token_internal()`.
1093 ///
1094 /// # Errors
1095 /// Returns an error if:
1096 /// - Environment variables are missing
1097 /// - All retry attempts fail
1098 /// - Response parsing fails
1099 pub async fn obtain_token(&self) -> Result<String, Error> {
1100 let _permit = self
1101 .token_operation_semaphore
1102 .acquire()
1103 .await
1104 .map_err(|e| {
1105 Error::Token(TokenError::storage(format!(
1106 "Failed to acquire token operation semaphore: {e}"
1107 )))
1108 })?;
1109
1110 self.obtain_token_internal().await
1111 }
1112
1113 /// Internal method to obtain a new authentication token without acquiring semaphore
1114 ///
1115 /// This method should only be called from contexts where the token operation semaphore
1116 /// has already been acquired (e.g., from within `get_token()`).
1117 ///
1118 /// # Errors
1119 /// Returns an error if:
1120 /// - Environment variables are missing
1121 /// - All retry attempts fail
1122 /// - Response parsing fails
1123 async fn obtain_token_internal(&self) -> Result<String, Error> {
1124 tracing::debug!("Obtaining new authentication token");
1125
1126 let request_payload = Self::get_credentials_from_env()?;
1127 let url = self.build_obtain_token_url();
1128 let response = self.execute_token_request(&url, &request_payload).await?;
1129 let token_response = self.parse_token_response(response).await?;
1130
1131 self.store_token_data(&token_response.token).await;
1132
1133 tracing::info!("New authentication token obtained successfully");
1134 Ok(token_response.token)
1135 }
1136
1137 /// Gets credentials from environment variables
1138 fn get_credentials_from_env() -> Result<TokenRequest, Error> {
1139 let username = env::var("AMP_USERNAME")
1140 .map_err(|_| Error::MissingEnvVar("AMP_USERNAME".to_string()))?;
1141 let password = env::var("AMP_PASSWORD")
1142 .map_err(|_| Error::MissingEnvVar("AMP_PASSWORD".to_string()))?;
1143
1144 Ok(TokenRequest { username, password })
1145 }
1146
1147 /// Builds the URL for token obtain endpoint
1148 fn build_obtain_token_url(&self) -> Url {
1149 let mut url = self.base_url.clone();
1150 url.path_segments_mut()
1151 .unwrap()
1152 .push("user")
1153 .push("obtain_token");
1154 url
1155 }
1156
1157 /// Executes the token request with retry logic
1158 async fn execute_token_request(
1159 &self,
1160 url: &Url,
1161 request_payload: &TokenRequest,
1162 ) -> Result<reqwest::Response, Error> {
1163 let response = self
1164 .retry_client
1165 .execute_with_retry(|| {
1166 self.retry_client
1167 .client()
1168 .post(url.clone())
1169 .json(request_payload)
1170 })
1171 .await
1172 .map_err(Error::Token)?;
1173
1174 if !response.status().is_success() {
1175 let status = response.status();
1176 let error_text = response
1177 .text()
1178 .await
1179 .unwrap_or_else(|_| "Unknown error".to_string());
1180 return Err(Error::TokenRequestFailed { status, error_text });
1181 }
1182
1183 Ok(response)
1184 }
1185
1186 /// Parses the token response from the API
1187 async fn parse_token_response(
1188 &self,
1189 response: reqwest::Response,
1190 ) -> Result<TokenResponse, Error> {
1191 response
1192 .json()
1193 .await
1194 .map_err(|e| Error::ResponseParsingFailed(e.to_string()))
1195 }
1196
1197 /// Stores the token data with 24-hour expiry and optional disk persistence
1198 async fn store_token_data(&self, token: &str) {
1199 let expires_at = Utc::now() + Duration::days(1);
1200 let token_data = TokenData::new(token.to_string(), expires_at);
1201
1202 // Atomic token update - hold the lock for the minimal time needed
1203 *self.token_data.lock().await = Some(token_data.clone());
1204 tracing::debug!("Token data updated atomically in storage");
1205
1206 // Save to disk if persistence is enabled
1207 if Self::should_persist_tokens() {
1208 if let Err(e) = self.save_token_to_disk(&token_data).await {
1209 tracing::warn!("Failed to save token to disk: {e}");
1210 }
1211 }
1212 }
1213
1214 /// Refreshes the current authentication token with fallback to obtain on failure
1215 ///
1216 /// This method:
1217 /// 1. Uses the existing token to request a refresh
1218 /// 2. Updates the stored token data on success
1219 /// 3. Falls back to obtaining a new token if refresh fails
1220 ///
1221 /// # Thread Safety
1222 /// This method acquires the token operation semaphore to ensure thread-safe operation.
1223 /// For internal use within already-synchronized contexts, use `refresh_token_internal()`.
1224 ///
1225 /// # Errors
1226 /// Returns an error if both refresh and obtain operations fail
1227 pub async fn refresh_token(&self) -> Result<String, Error> {
1228 let _permit = self
1229 .token_operation_semaphore
1230 .acquire()
1231 .await
1232 .map_err(|e| {
1233 Error::Token(TokenError::storage(format!(
1234 "Failed to acquire token operation semaphore: {e}"
1235 )))
1236 })?;
1237
1238 self.refresh_token_internal().await
1239 }
1240
1241 /// Internal method to refresh the current authentication token without acquiring semaphore
1242 ///
1243 /// This method should only be called from contexts where the token operation semaphore
1244 /// has already been acquired (e.g., from within `get_token()`).
1245 ///
1246 /// # Errors
1247 /// Returns an error if both refresh and obtain operations fail
1248 #[allow(clippy::cognitive_complexity)]
1249 async fn refresh_token_internal(&self) -> Result<String, Error> {
1250 tracing::debug!("Refreshing authentication token");
1251
1252 let Some(current_token) = self.get_current_token_for_refresh().await else {
1253 tracing::warn!("No token available for refresh, obtaining new token");
1254 return self.obtain_token_internal().await;
1255 };
1256
1257 let url = self.build_refresh_token_url();
1258 let response = self.execute_refresh_request(&url, ¤t_token).await;
1259
1260 match response {
1261 Ok(resp) => self.handle_refresh_response(resp).await,
1262 Err(e) => {
1263 tracing::warn!("Token refresh request failed: {e}, falling back to obtain");
1264 self.obtain_token_internal().await
1265 }
1266 }
1267 }
1268
1269 /// Gets the current token for refresh operations
1270 async fn get_current_token_for_refresh(&self) -> Option<String> {
1271 let token_guard = self.token_data.lock().await;
1272 token_guard
1273 .as_ref()
1274 .map(|token_data| token_data.token.expose_secret().clone())
1275 }
1276
1277 /// Builds the URL for token refresh endpoint
1278 fn build_refresh_token_url(&self) -> Url {
1279 let mut url = self.base_url.clone();
1280 url.path_segments_mut()
1281 .unwrap()
1282 .push("user")
1283 .push("refresh_token");
1284 url
1285 }
1286
1287 /// Executes the refresh request with retry logic
1288 async fn execute_refresh_request(
1289 &self,
1290 url: &Url,
1291 current_token: &str,
1292 ) -> Result<reqwest::Response, TokenError> {
1293 self.retry_client
1294 .execute_with_retry(|| {
1295 self.retry_client
1296 .client()
1297 .post(url.clone())
1298 .header(AUTHORIZATION, format!("token {current_token}"))
1299 })
1300 .await
1301 }
1302
1303 /// Handles the refresh response, either storing the new token or falling back to obtain
1304 async fn handle_refresh_response(&self, resp: reqwest::Response) -> Result<String, Error> {
1305 if !resp.status().is_success() {
1306 let status = resp.status();
1307 let error_text = resp
1308 .text()
1309 .await
1310 .unwrap_or_else(|_| "Unknown error".to_string());
1311
1312 tracing::warn!("Token refresh failed with status {status}: {error_text}");
1313 return self.obtain_token_internal().await;
1314 }
1315
1316 let token_response: TokenResponse = resp
1317 .json()
1318 .await
1319 .map_err(|e| Error::ResponseParsingFailed(e.to_string()))?;
1320
1321 self.store_token_data(&token_response.token).await;
1322 tracing::info!("Authentication token refreshed successfully");
1323 Ok(token_response.token)
1324 }
1325
1326 /// Gets current token information for debugging and monitoring
1327 ///
1328 /// Returns detailed information about the current token including:
1329 /// - Expiry time and remaining duration
1330 /// - Token age since acquisition
1331 /// - Expiry status flags
1332 ///
1333 /// # Returns
1334 /// `Some(TokenInfo)` if a token exists, `None` if no token is stored
1335 ///
1336 /// # Errors
1337 /// Returns an error if token information retrieval fails
1338 pub async fn get_token_info(&self) -> Result<Option<TokenInfo>, Error> {
1339 tracing::debug!("Retrieving token information for debugging");
1340
1341 let token_info = self.token_data.lock().await.as_ref().map(TokenInfo::from);
1342
1343 match &token_info {
1344 Some(info) => {
1345 tracing::debug!(
1346 "Token info retrieved - expires_at: {}, age: {:?}, expires_in: {:?}, is_expired: {}, expires_soon: {}",
1347 info.expires_at,
1348 info.age,
1349 info.expires_in,
1350 info.is_expired,
1351 info.expires_soon
1352 );
1353 }
1354 None => {
1355 tracing::debug!("No token information available - no token stored");
1356 }
1357 }
1358
1359 Ok(token_info)
1360 }
1361
1362 /// Clears the stored token (useful for testing scenarios)
1363 ///
1364 /// This method removes the current token from storage, forcing the next
1365 /// `get_token()` call to obtain a fresh token.
1366 ///
1367 /// # Errors
1368 /// Returns an error if token clearing fails
1369 pub async fn clear_token(&self) -> Result<(), Error> {
1370 tracing::debug!("Clearing stored token from memory and disk");
1371
1372 let had_token = self.clear_token_from_memory().await;
1373 self.clear_token_from_disk_if_enabled().await;
1374 Self::log_token_clear_result(had_token);
1375
1376 Ok(())
1377 }
1378
1379 /// Clears the token from memory and returns whether a token was present
1380 async fn clear_token_from_memory(&self) -> bool {
1381 let mut token_guard = self.token_data.lock().await;
1382 let had_token = token_guard.is_some();
1383 *token_guard = None;
1384 drop(token_guard);
1385 had_token
1386 }
1387
1388 /// Clears the token from disk if persistence is enabled
1389 async fn clear_token_from_disk_if_enabled(&self) {
1390 if Self::should_persist_tokens() {
1391 if let Err(e) = self.remove_token_from_disk().await {
1392 tracing::warn!("Failed to remove token from disk: {e}");
1393 }
1394 }
1395 }
1396
1397 /// Logs the result of the token clearing operation
1398 fn log_token_clear_result(had_token: bool) {
1399 if had_token {
1400 tracing::info!("Token successfully cleared from memory and disk - next get_token() will obtain fresh token");
1401 } else {
1402 tracing::debug!("No token was stored to clear");
1403 }
1404 }
1405
1406 /// Forces a token refresh regardless of current token status
1407 ///
1408 /// This method bypasses the normal proactive refresh logic and immediately
1409 /// attempts to refresh the current token. If no token exists or refresh fails,
1410 /// it falls back to obtaining a new token.
1411 ///
1412 /// # Thread Safety
1413 /// This method is fully thread-safe and uses the same semaphore-based synchronization
1414 /// as other token operations to prevent race conditions.
1415 ///
1416 /// # Errors
1417 /// Returns an error if both refresh and obtain operations fail
1418 pub async fn force_refresh(&self) -> Result<String, Error> {
1419 tracing::info!("Forcing token refresh - bypassing normal proactive refresh logic");
1420
1421 let _permit = self.acquire_token_semaphore().await?;
1422 self.log_token_status_for_refresh().await;
1423 self.execute_forced_refresh().await
1424 }
1425
1426 /// Logs the current token status for forced refresh operation
1427 async fn log_token_status_for_refresh(&self) {
1428 let has_token = {
1429 let token_guard = self.token_data.lock().await;
1430 token_guard.is_some()
1431 };
1432
1433 if has_token {
1434 tracing::debug!("Existing token found, attempting forced refresh");
1435 } else {
1436 tracing::debug!("No existing token found, will obtain new token");
1437 }
1438 }
1439
1440 /// Executes the forced refresh operation
1441 async fn execute_forced_refresh(&self) -> Result<String, Error> {
1442 match self.refresh_token_internal().await {
1443 Ok(token) => {
1444 tracing::info!("Forced token refresh completed successfully");
1445 Ok(token)
1446 }
1447 Err(e) => {
1448 tracing::error!("Forced token refresh failed: {e}");
1449 Err(e)
1450 }
1451 }
1452 }
1453
1454 /// Determines if token persistence is enabled based on environment variables
1455 ///
1456 /// Token persistence is enabled when:
1457 /// - `AMP_TESTS=live` (for live API testing)
1458 /// - `AMP_TOKEN_PERSISTENCE=true` is set
1459 /// - NOT in mock test environments (to prevent test pollution)
1460 fn should_persist_tokens() -> bool {
1461 // Use the new environment detection logic
1462 let environment = TokenEnvironment::detect();
1463
1464 // Never persist tokens in mock environments to prevent test pollution
1465 if environment.is_mock() {
1466 tracing::debug!("Token persistence disabled - mock environment detected");
1467 return false;
1468 }
1469
1470 // Check if explicitly enabled
1471 if env::var("AMP_TOKEN_PERSISTENCE").unwrap_or_default() == "true" {
1472 tracing::debug!("Token persistence enabled - AMP_TOKEN_PERSISTENCE=true");
1473 return true;
1474 }
1475
1476 // Use environment-based persistence setting
1477 let should_persist = environment.should_persist_tokens();
1478 tracing::debug!(
1479 "Token persistence setting from environment: {}",
1480 should_persist
1481 );
1482 should_persist
1483 }
1484
1485 /// Loads token data from disk if it exists and is valid
1486 async fn load_token_from_disk(&self) -> Result<Option<TokenData>, Error> {
1487 let token_file = "token.json";
1488
1489 if !self.token_file_exists(token_file).await {
1490 return Ok(None);
1491 }
1492
1493 let content = self.read_token_file(token_file).await?;
1494 self.parse_and_validate_token(token_file, &content).await
1495 }
1496
1497 /// Checks if the token file exists on disk
1498 async fn token_file_exists(&self, token_file: &str) -> bool {
1499 tokio::fs::try_exists(token_file).await.map_or_else(
1500 |_| {
1501 tracing::debug!("Error checking token file existence: {}", token_file);
1502 false
1503 },
1504 |exists| {
1505 if !exists {
1506 tracing::debug!("Token file does not exist: {}", token_file);
1507 }
1508 exists
1509 },
1510 )
1511 }
1512
1513 /// Reads the token file content from disk
1514 async fn read_token_file(&self, token_file: &str) -> Result<String, Error> {
1515 use tokio::fs;
1516
1517 match fs::read_to_string(token_file).await {
1518 Ok(content) => Ok(content),
1519 Err(e) => {
1520 tracing::warn!("Failed to read token file: {e}");
1521 Err(Error::Token(TokenError::storage(format!(
1522 "Failed to read token file: {e}"
1523 ))))
1524 }
1525 }
1526 }
1527
1528 /// Parses token content and validates expiration
1529 async fn parse_and_validate_token(
1530 &self,
1531 token_file: &str,
1532 content: &str,
1533 ) -> Result<Option<TokenData>, Error> {
1534 match serde_json::from_str::<TokenData>(content) {
1535 Ok(token_data) => self.handle_parsed_token(token_file, token_data).await,
1536 Err(e) => self.handle_parse_error(token_file, e).await,
1537 }
1538 }
1539
1540 /// Handles successfully parsed token data, checking expiration
1541 async fn handle_parsed_token(
1542 &self,
1543 token_file: &str,
1544 token_data: TokenData,
1545 ) -> Result<Option<TokenData>, Error> {
1546 if token_data.is_expired() {
1547 tracing::info!("Token loaded from disk is expired, removing file");
1548 let _ = tokio::fs::remove_file(token_file).await;
1549 Ok(None)
1550 } else {
1551 tracing::info!("Valid token loaded from disk");
1552 Ok(Some(token_data))
1553 }
1554 }
1555
1556 /// Handles token parsing errors by cleaning up the invalid file
1557 async fn handle_parse_error(
1558 &self,
1559 token_file: &str,
1560 e: serde_json::Error,
1561 ) -> Result<Option<TokenData>, Error> {
1562 tracing::warn!("Failed to parse token file, removing: {e}");
1563 let _ = tokio::fs::remove_file(token_file).await;
1564 Err(Error::Token(TokenError::serialization(format!(
1565 "Failed to parse token file: {e}"
1566 ))))
1567 }
1568
1569 /// Saves token data to disk
1570 async fn save_token_to_disk(&self, token_data: &TokenData) -> Result<(), Error> {
1571 use tokio::fs;
1572
1573 let token_file = "token.json";
1574
1575 match serde_json::to_string_pretty(token_data) {
1576 Ok(json) => match fs::write(token_file, json).await {
1577 Ok(()) => {
1578 tracing::debug!("Token saved to disk: {}", token_file);
1579 Ok(())
1580 }
1581 Err(e) => {
1582 tracing::error!("Failed to write token file: {e}");
1583 Err(Error::Token(TokenError::storage(format!(
1584 "Failed to write token file: {e}"
1585 ))))
1586 }
1587 },
1588 Err(e) => {
1589 tracing::error!("Failed to serialize token data: {e}");
1590 Err(Error::Token(TokenError::serialization(format!(
1591 "Failed to serialize token data: {e}"
1592 ))))
1593 }
1594 }
1595 }
1596
1597 /// Removes the token file from disk
1598 async fn remove_token_from_disk(&self) -> Result<(), Error> {
1599 use tokio::fs;
1600
1601 let token_file = "token.json";
1602
1603 match fs::remove_file(token_file).await {
1604 Ok(()) => {
1605 tracing::debug!("Token file removed from disk: {}", token_file);
1606 Ok(())
1607 }
1608 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
1609 tracing::debug!("Token file does not exist, nothing to remove");
1610 Ok(())
1611 }
1612 Err(e) => {
1613 tracing::warn!("Failed to remove token file: {e}");
1614 Err(Error::Token(TokenError::storage(format!(
1615 "Failed to remove token file: {e}"
1616 ))))
1617 }
1618 }
1619 }
1620
1621 /// Forces cleanup of token persistence files (useful for testing)
1622 /// This method removes token files regardless of persistence settings
1623 ///
1624 /// # Errors
1625 /// Returns an error if:
1626 /// - File system permissions prevent deletion of the token file
1627 /// - I/O errors occur during file deletion operations
1628 /// - The token file is locked by another process
1629 pub async fn force_cleanup_token_files() -> Result<(), Error> {
1630 use tokio::fs;
1631
1632 let token_file = "token.json";
1633
1634 match fs::remove_file(token_file).await {
1635 Ok(()) => {
1636 tracing::debug!("Token file forcefully removed: {}", token_file);
1637 Ok(())
1638 }
1639 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
1640 tracing::debug!("No token file to clean up");
1641 Ok(())
1642 }
1643 Err(e) => {
1644 tracing::warn!("Failed to force cleanup token file: {e}");
1645 Err(Error::Token(TokenError::storage(format!(
1646 "Failed to force cleanup token file: {e}"
1647 ))))
1648 }
1649 }
1650 }
1651
1652 /// Resets the global `TokenManager` singleton (useful for testing)
1653 ///
1654 /// This method clears the global singleton instance, forcing the next
1655 /// call to `get_global_instance()` to create a fresh `TokenManager`.
1656 /// Primarily intended for test scenarios where a clean state is needed.
1657 ///
1658 /// # Errors
1659 /// Returns an error if:
1660 /// - Token clearing operations fail during the reset process
1661 /// - File system errors occur when clearing persistent token data
1662 /// - The global instance is in an invalid state that prevents cleanup
1663 pub async fn reset_global_instance() -> Result<(), Error> {
1664 // Clear any existing token from the current global instance
1665 if let Some(manager) = GLOBAL_TOKEN_MANAGER.get() {
1666 let _ = manager.clear_token().await;
1667 }
1668
1669 // Reset the OnceCell to allow a new instance to be created
1670 // Note: OnceCell doesn't have a reset method, so we can't actually reset it
1671 // The best we can do is clear the token from the existing instance
1672 tracing::debug!("Global TokenManager instance token cleared for testing");
1673 Ok(())
1674 }
1675}
1676
1677#[derive(Debug)]
1678pub struct ApiClient {
1679 client: Client,
1680 base_url: Url,
1681 token_strategy: Box<dyn TokenStrategy>,
1682}
1683
1684#[allow(clippy::missing_errors_doc, clippy::missing_panics_doc)]
1685impl ApiClient {
1686 /// Creates a new API client with the base URL from environment variables.
1687 ///
1688 /// Automatically selects the appropriate token strategy based on environment detection:
1689 /// - Mock strategy for mock environments (no persistence, isolated tokens)
1690 /// - Live strategy for live environments (full token management with persistence)
1691 ///
1692 /// # Errors
1693 ///
1694 /// Returns an error if:
1695 /// - The `AMP_API_BASE_URL` environment variable contains an invalid URL
1696 /// - Token strategy initialization fails
1697 ///
1698 /// # Examples
1699 /// ```no_run
1700 /// # use amp_rs::ApiClient;
1701 /// # #[tokio::main]
1702 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1703 /// // Create a new client - automatically detects environment
1704 /// let client = ApiClient::new().await?;
1705 ///
1706 /// // Client is ready to use
1707 /// let assets = client.get_assets().await?;
1708 /// println!("Found {} assets", assets.len());
1709 /// # Ok(())
1710 /// # }
1711 /// ```
1712 pub async fn new() -> Result<Self, Error> {
1713 let base_url = get_amp_api_base_url()?;
1714 let client = Client::new();
1715
1716 // Automatic strategy selection based on environment
1717 let token_strategy = TokenEnvironment::create_auto_strategy(None).await?;
1718
1719 tracing::info!(
1720 "Created ApiClient with {} strategy for base URL: {}",
1721 token_strategy.strategy_type(),
1722 base_url
1723 );
1724
1725 Ok(Self {
1726 client,
1727 base_url,
1728 token_strategy,
1729 })
1730 }
1731
1732 /// Creates a new API client with the specified base URL.
1733 ///
1734 /// Automatically selects the appropriate token strategy based on environment detection.
1735 ///
1736 /// # Errors
1737 ///
1738 /// Returns an error if token strategy initialization fails.
1739 ///
1740 /// # Examples
1741 /// ```no_run
1742 /// # use amp_rs::ApiClient;
1743 /// # use reqwest::Url;
1744 /// # #[tokio::main]
1745 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1746 /// let base_url = Url::parse("https://amp-test.blockstream.com/api")?;
1747 /// let client = ApiClient::with_base_url(base_url).await?;
1748 ///
1749 /// // Client is ready to use with the specified URL
1750 /// let assets = client.get_assets().await?;
1751 /// # Ok(())
1752 /// # }
1753 /// ```
1754 pub async fn with_base_url(base_url: Url) -> Result<Self, Error> {
1755 let client = Client::new();
1756
1757 // Automatic strategy selection based on environment
1758 let token_strategy = TokenEnvironment::create_auto_strategy(None).await?;
1759
1760 tracing::info!(
1761 "Created ApiClient with {} strategy for base URL: {}",
1762 token_strategy.strategy_type(),
1763 base_url
1764 );
1765
1766 Ok(Self {
1767 client,
1768 base_url,
1769 token_strategy,
1770 })
1771 }
1772
1773 /// Creates a new API client with a custom token strategy (useful for testing).
1774 ///
1775 /// # Errors
1776 ///
1777 /// Returns an error if the base URL cannot be obtained from environment variables.
1778 pub fn with_token_strategy(token_strategy: Box<dyn TokenStrategy>) -> Result<Self, Error> {
1779 let base_url = get_amp_api_base_url()?;
1780
1781 tracing::info!(
1782 "Created ApiClient with explicit {} strategy for base URL: {}",
1783 token_strategy.strategy_type(),
1784 base_url
1785 );
1786
1787 Ok(Self {
1788 client: Client::new(),
1789 base_url,
1790 token_strategy,
1791 })
1792 }
1793
1794 /// Creates a new API client with a custom token manager (useful for testing).
1795 ///
1796 /// # Errors
1797 ///
1798 /// Returns an error if the base URL cannot be obtained from environment variables.
1799 pub fn with_token_manager(token_manager: Arc<TokenManager>) -> Result<Self, Error> {
1800 let base_url = get_amp_api_base_url()?;
1801 let token_strategy: Box<dyn TokenStrategy> =
1802 Box::new(LiveTokenStrategy::with_token_manager(token_manager));
1803
1804 tracing::info!(
1805 "Created ApiClient with custom token manager for base URL: {}",
1806 base_url
1807 );
1808
1809 Ok(Self {
1810 client: Client::new(),
1811 base_url,
1812 token_strategy,
1813 })
1814 }
1815
1816 /// Creates a new API client for testing with a mock token strategy that always returns a fixed token.
1817 /// This bypasses all token acquisition and management logic and uses complete isolation.
1818 ///
1819 /// # Errors
1820 ///
1821 /// This method is infallible but returns Result for API consistency.
1822 ///
1823 /// # Examples
1824 /// ```
1825 /// # use amp_rs::ApiClient;
1826 /// # use reqwest::Url;
1827 /// # #[tokio::main]
1828 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1829 /// let base_url = Url::parse("http://localhost:8080/api")?;
1830 /// let client = ApiClient::with_mock_token(base_url, "test_token".to_string())?;
1831 ///
1832 /// // Client will always use "test_token" for authentication
1833 /// let token = client.get_token().await?;
1834 /// assert_eq!(token, "test_token");
1835 /// # Ok(())
1836 /// # }
1837 /// ```
1838 pub fn with_mock_token(base_url: Url, mock_token: String) -> Result<Self, Error> {
1839 let client = Client::new();
1840 let token_strategy: Box<dyn TokenStrategy> = Box::new(MockTokenStrategy::new(mock_token));
1841
1842 tracing::info!(
1843 "Created ApiClient with explicit mock token strategy for base URL: {}",
1844 base_url
1845 );
1846
1847 Ok(Self {
1848 client,
1849 base_url,
1850 token_strategy,
1851 })
1852 }
1853
1854 /// Obtains a new authentication token from the AMP API.
1855 ///
1856 /// **Note**: This method is deprecated in favor of the automatic token management
1857 /// provided by `get_token()`. The `TokenManager` handles token acquisition internally
1858 /// with enhanced retry logic and error handling.
1859 ///
1860 /// # Errors
1861 ///
1862 /// Returns an error if:
1863 /// - The `AMP_USERNAME` or `AMP_PASSWORD` environment variables are not set
1864 /// - The HTTP request fails
1865 /// - The token request is rejected by the server
1866 /// - The response cannot be parsed
1867 #[deprecated(note = "Use get_token() instead - it provides automatic token management")]
1868 pub async fn obtain_amp_token(&self) -> Result<String, Error> {
1869 // Delegate to get_token for backward compatibility
1870 self.get_token().await
1871 }
1872
1873 /// Gets current token information for debugging and monitoring.
1874 ///
1875 /// Returns detailed information about the current token including:
1876 /// - Expiry time and remaining duration
1877 /// - Token age since acquisition
1878 /// - Expiry status flags
1879 ///
1880 /// Note: Mock strategies may return limited or no token information.
1881 ///
1882 /// # Returns
1883 /// `Some(TokenInfo)` if a token exists, `None` if no token is stored or strategy doesn't support info
1884 ///
1885 /// # Errors
1886 /// Returns an error if token information retrieval fails
1887 ///
1888 /// # Examples
1889 /// ```no_run
1890 /// # use amp_rs::ApiClient;
1891 /// # #[tokio::main]
1892 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1893 /// let client = ApiClient::new().await?;
1894 ///
1895 /// if let Some(token_info) = client.get_token_info().await? {
1896 /// println!("Token expires at: {}", token_info.expires_at);
1897 /// println!("Token is expired: {}", token_info.is_expired);
1898 /// } else {
1899 /// println!("No token stored or mock strategy in use");
1900 /// }
1901 /// # Ok(())
1902 /// # }
1903 /// ```
1904 pub async fn get_token_info(&self) -> Result<Option<TokenInfo>, Error> {
1905 // Only live strategies support detailed token information
1906 if let Some(live_strategy) = self
1907 .token_strategy
1908 .as_any()
1909 .downcast_ref::<LiveTokenStrategy>()
1910 {
1911 live_strategy.get_token_info().await
1912 } else {
1913 // Mock strategies don't provide detailed token information
1914 tracing::debug!(
1915 "Token info not available for {} strategy",
1916 self.token_strategy.strategy_type()
1917 );
1918 Ok(None)
1919 }
1920 }
1921
1922 /// Clears the stored token (useful for testing scenarios).
1923 ///
1924 /// This method removes the current token from storage, forcing the next
1925 /// `get_token()` call to obtain a fresh token.
1926 ///
1927 /// # Errors
1928 /// Returns an error if token clearing fails
1929 ///
1930 /// # Examples
1931 /// ```no_run
1932 /// # use amp_rs::ApiClient;
1933 /// # #[tokio::main]
1934 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1935 /// let client = ApiClient::new().await?;
1936 ///
1937 /// // Clear any existing token
1938 /// client.clear_token().await?;
1939 ///
1940 /// // Next get_token() call will obtain a fresh token
1941 /// let token = client.get_token().await?;
1942 /// # Ok(())
1943 /// # }
1944 /// ```
1945 pub async fn clear_token(&self) -> Result<(), Error> {
1946 self.token_strategy.clear_token().await
1947 }
1948
1949 /// Forces a token refresh regardless of current token status.
1950 ///
1951 /// This method bypasses the normal proactive refresh logic and immediately
1952 /// attempts to refresh the current token. If no token exists or refresh fails,
1953 /// it falls back to obtaining a new token.
1954 ///
1955 /// # Errors
1956 /// Returns an error if both refresh and obtain operations fail
1957 pub async fn force_refresh(&self) -> Result<String, Error> {
1958 // Clear current token and get a fresh one
1959 self.token_strategy.clear_token().await?;
1960 self.token_strategy.get_token().await
1961 }
1962
1963 /// Resets the global `TokenManager` singleton (useful for testing).
1964 ///
1965 /// This method clears the token from the global `TokenManager` instance.
1966 /// Primarily intended for test scenarios where a clean token state is needed.
1967 ///
1968 /// # Errors
1969 /// Returns an error if the reset operation fails
1970 pub async fn reset_global_token_manager() -> Result<(), Error> {
1971 TokenManager::reset_global_instance().await
1972 }
1973
1974 /// Gets a valid authentication token with automatic token management.
1975 ///
1976 /// This method uses the integrated `TokenManager` to handle:
1977 /// - Proactive token refresh (5 minutes before expiry)
1978 /// - Automatic fallback from refresh to obtain on failure
1979 /// - Retry logic with exponential backoff
1980 /// - Thread-safe token storage
1981 ///
1982 /// # Errors
1983 ///
1984 /// Returns an error if token acquisition or refresh fails after all retries.
1985 ///
1986 /// # Examples
1987 /// ```no_run
1988 /// # use amp_rs::ApiClient;
1989 /// # #[tokio::main]
1990 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1991 /// let client = ApiClient::new().await?;
1992 ///
1993 /// // Get a valid token - automatically handles refresh if needed
1994 /// let token = client.get_token().await?;
1995 /// println!("Got token: {}", &token[..10]); // Print first 10 chars
1996 /// # Ok(())
1997 /// # }
1998 /// ```
1999 pub async fn get_token(&self) -> Result<String, Error> {
2000 self.token_strategy.get_token().await
2001 }
2002
2003 /// Returns the type of token strategy currently in use
2004 ///
2005 /// This is useful for debugging and testing to verify the correct strategy is selected.
2006 ///
2007 /// # Returns
2008 /// A string indicating the strategy type: "mock" or "live"
2009 #[must_use]
2010 pub fn get_strategy_type(&self) -> &'static str {
2011 self.token_strategy.strategy_type()
2012 }
2013
2014 /// Returns whether the current strategy persists tokens
2015 ///
2016 /// This is useful for understanding the token management behavior.
2017 ///
2018 /// # Returns
2019 /// `true` if tokens are persisted to disk, `false` for in-memory only
2020 #[must_use]
2021 pub fn should_persist_tokens(&self) -> bool {
2022 self.token_strategy.should_persist()
2023 }
2024
2025 /// Force cleanup of token files (for test cleanup)
2026 ///
2027 /// This is a static method that can be used to cleanup token files
2028 /// without needing an `ApiClient` instance. Useful for test teardown.
2029 ///
2030 /// # Errors
2031 /// Returns an error if token file cleanup fails
2032 pub async fn force_cleanup_token_files() -> Result<(), Error> {
2033 // Only cleanup if we're not in a live test environment
2034 let environment = TokenEnvironment::detect();
2035 if !environment.is_live() || environment.is_mock() {
2036 TokenManager::force_cleanup_token_files().await?;
2037 tracing::debug!("Token files cleaned up for non-live environment");
2038 } else {
2039 tracing::debug!("Skipping token file cleanup in live environment");
2040 }
2041 Ok(())
2042 }
2043
2044 async fn request_raw(
2045 &self,
2046 method: Method,
2047 path: &[&str],
2048 body: Option<impl serde::Serialize>,
2049 ) -> Result<reqwest::Response, Error> {
2050 let token = self.get_token().await?;
2051 let mut url = self.base_url.clone();
2052 url.path_segments_mut().unwrap().extend(path);
2053
2054 let mut request_builder = self
2055 .client
2056 .request(method, url)
2057 .header(AUTHORIZATION, format!("token {token}"));
2058
2059 if let Some(body) = body {
2060 request_builder = request_builder.json(&body);
2061 }
2062
2063 let response = request_builder.send().await?;
2064
2065 if !response.status().is_success() {
2066 let status = response.status();
2067 let error_text = response
2068 .text()
2069 .await
2070 .unwrap_or_else(|_| "Unknown error".to_string());
2071 return Err(Error::RequestFailed(format!(
2072 "Request to {path:?} failed with status {status}: {error_text}"
2073 )));
2074 }
2075
2076 Ok(response)
2077 }
2078
2079 async fn request_json<T: DeserializeOwned>(
2080 &self,
2081 method: Method,
2082 path: &[&str],
2083 body: Option<impl serde::Serialize>,
2084 ) -> Result<T, Error> {
2085 let response = self.request_raw(method, path, body).await?;
2086 response
2087 .json()
2088 .await
2089 .map_err(|e| Error::ResponseParsingFailed(e.to_string()))
2090 }
2091
2092 async fn request_empty(
2093 &self,
2094 method: Method,
2095 path: &[&str],
2096 body: Option<impl serde::Serialize>,
2097 ) -> Result<(), Error> {
2098 self.request_raw(method, path, body).await?;
2099 Ok(())
2100 }
2101
2102 /// Gets the API changelog.
2103 ///
2104 /// # Errors
2105 ///
2106 /// Returns an error if:
2107 /// - Authentication fails
2108 /// - The HTTP request fails
2109 /// - The server returns an error status
2110 /// - The response cannot be parsed as JSON
2111 pub async fn get_changelog(&self) -> Result<serde_json::Value, Error> {
2112 self.request_json(Method::GET, &["changelog"], None::<&()>)
2113 .await
2114 }
2115
2116 /// Changes the user's password.
2117 ///
2118 /// # Errors
2119 ///
2120 /// Returns an error if:
2121 /// - Authentication fails
2122 /// - The HTTP request fails
2123 /// - The server rejects the password change
2124 /// - The response cannot be parsed
2125 pub async fn user_change_password(
2126 &self,
2127 password: Secret<String>,
2128 ) -> Result<ChangePasswordResponse, Error> {
2129 let request = ChangePasswordRequest {
2130 password: Secret::new(Password(password.expose_secret().clone())),
2131 };
2132 self.request_json(Method::POST, &["user", "change_password"], Some(request))
2133 .await
2134 }
2135
2136 /// Gets a list of all assets.
2137 ///
2138 /// # Errors
2139 ///
2140 /// Returns an error if:
2141 /// - Authentication fails
2142 /// - The HTTP request fails
2143 /// - The server returns an error status
2144 /// - The response cannot be parsed
2145 ///
2146 /// # Examples
2147 /// ```no_run
2148 /// # use amp_rs::ApiClient;
2149 /// # #[tokio::main]
2150 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2151 /// let client = ApiClient::new().await?;
2152 ///
2153 /// let assets = client.get_assets().await?;
2154 /// for asset in assets {
2155 /// println!("Asset: {} ({})", asset.name, asset.ticker.unwrap_or_default());
2156 /// }
2157 /// # Ok(())
2158 /// # }
2159 /// ```
2160 pub async fn get_assets(&self) -> Result<Vec<Asset>, Error> {
2161 self.request_json(Method::GET, &["assets"], None::<&()>)
2162 .await
2163 }
2164
2165 /// Gets a specific asset by UUID.
2166 ///
2167 /// # Errors
2168 ///
2169 /// Returns an error if:
2170 /// - Authentication fails
2171 /// - The HTTP request fails
2172 /// - The asset does not exist
2173 /// - The response cannot be parsed
2174 ///
2175 /// # Examples
2176 /// ```no_run
2177 /// # use amp_rs::ApiClient;
2178 /// # #[tokio::main]
2179 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2180 /// let client = ApiClient::new().await?;
2181 ///
2182 /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
2183 /// let asset = client.get_asset(asset_uuid).await?;
2184 ///
2185 /// println!("Asset: {} ({})", asset.name, asset.ticker.unwrap_or_default());
2186 /// println!("Registered: {}, Locked: {}", asset.is_registered, asset.is_locked);
2187 /// # Ok(())
2188 /// # }
2189 /// ```
2190 pub async fn get_asset(&self, asset_uuid: &str) -> Result<Asset, Error> {
2191 self.request_json(Method::GET, &["assets", asset_uuid], None::<&()>)
2192 .await
2193 }
2194
2195 /// Issues a new asset.
2196 ///
2197 /// # Errors
2198 ///
2199 /// Returns an error if:
2200 /// - Authentication fails
2201 /// - The HTTP request fails
2202 /// - The issuance request is invalid
2203 /// - The response cannot be parsed
2204 ///
2205 /// # Examples
2206 /// ```no_run
2207 /// # use amp_rs::{ApiClient, model::IssuanceRequest};
2208 /// # #[tokio::main]
2209 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2210 /// let client = ApiClient::new().await?;
2211 ///
2212 /// let issuance_request = IssuanceRequest {
2213 /// name: "My Token".to_string(),
2214 /// amount: 1000000,
2215 /// destination_address: "vjU2i2EM2viGEzSywpStMPkTX9U9QSDsLSN63kJJYVpxKJZuxaph8v5r5Jf11aqnfBVdjSbrvcJ2pw26".to_string(),
2216 /// domain: "example.com".to_string(),
2217 /// ticker: "MYTKN".to_string(),
2218 /// pubkey: "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798".to_string(),
2219 /// precision: Some(8),
2220 /// is_confidential: Some(true),
2221 /// is_reissuable: Some(false),
2222 /// reissuance_amount: None,
2223 /// reissuance_address: None,
2224 /// transfer_restricted: Some(false),
2225 /// };
2226 ///
2227 /// let response = client.issue_asset(&issuance_request).await?;
2228 /// println!("Issued asset with UUID: {}", response.asset_uuid);
2229 /// # Ok(())
2230 /// # }
2231 /// ```
2232 pub async fn issue_asset(
2233 &self,
2234 issuance_request: &IssuanceRequest,
2235 ) -> Result<IssuanceResponse, Error> {
2236 self.request_json(Method::POST, &["assets", "issue"], Some(issuance_request))
2237 .await
2238 }
2239
2240 /// Edits an existing asset.
2241 ///
2242 /// # Errors
2243 ///
2244 /// Returns an error if:
2245 /// - Authentication fails
2246 /// - The HTTP request fails
2247 /// - The asset does not exist
2248 /// - The edit request is invalid
2249 /// - The response cannot be parsed
2250 pub async fn edit_asset(
2251 &self,
2252 asset_uuid: &str,
2253 edit_asset_request: &EditAssetRequest,
2254 ) -> Result<Asset, Error> {
2255 self.request_json(
2256 Method::PUT,
2257 &["assets", asset_uuid, "edit"],
2258 Some(edit_asset_request),
2259 )
2260 .await
2261 }
2262
2263 /// # Errors
2264 /// Returns an error if:
2265 /// - The asset does not exist or cannot be found
2266 /// - Authentication fails or token is invalid
2267 /// - Network connectivity issues occur
2268 /// - The server returns an error status
2269 pub async fn delete_asset(&self, asset_uuid: &str) -> Result<(), Error> {
2270 self.request_empty(
2271 Method::DELETE,
2272 &["assets", asset_uuid, "delete"],
2273 None::<&()>,
2274 )
2275 .await
2276 }
2277
2278 /// # Errors
2279 /// Returns an error if:
2280 /// - The transaction ID is invalid or not found
2281 /// - Authentication fails or token is invalid
2282 /// - Network connectivity issues occur
2283 /// - The server returns an error status
2284 /// - The response cannot be parsed
2285 pub async fn get_broadcast_status(&self, txid: &str) -> Result<BroadcastResponse, Error> {
2286 self.request_json(Method::GET, &["tx", "broadcast", txid], None::<&()>)
2287 .await
2288 }
2289
2290 /// # Errors
2291 /// Returns an error if:
2292 /// - The transaction hex is invalid or malformed
2293 /// - The transaction is rejected by the network
2294 /// - Authentication fails or token is invalid
2295 /// - Network connectivity issues occur
2296 /// - The server returns an error status
2297 /// - The response cannot be parsed
2298 pub async fn broadcast_transaction(&self, tx_hex: &str) -> Result<BroadcastResponse, Error> {
2299 self.request_json(Method::POST, &["tx", "broadcast"], Some(tx_hex))
2300 .await
2301 }
2302
2303 /// # Errors
2304 /// Returns an error if:
2305 /// - The asset UUID is invalid or not found
2306 /// - The asset is already registered
2307 /// - Authentication fails or token is invalid
2308 /// - Network connectivity issues occur
2309 /// - The server returns an error status
2310 /// - The response cannot be parsed
2311 pub async fn register_asset(&self, asset_uuid: &str) -> Result<Asset, Error> {
2312 self.request_json(
2313 Method::GET,
2314 &["assets", asset_uuid, "register"],
2315 None::<&()>,
2316 )
2317 .await
2318 }
2319
2320 /// # Errors
2321 /// Returns an error if:
2322 /// - The asset UUID is invalid or not found
2323 /// - The user lacks authorization to register the asset
2324 /// - The asset is already registered
2325 /// - Authentication fails or token is invalid
2326 /// - Network connectivity issues occur
2327 /// - The server returns an error status
2328 /// - The response cannot be parsed
2329 pub async fn register_asset_authorized(&self, asset_uuid: &str) -> Result<Asset, Error> {
2330 self.request_json(
2331 Method::GET,
2332 &["assets", asset_uuid, "register-authorized"],
2333 None::<&()>,
2334 )
2335 .await
2336 }
2337
2338 /// # Errors
2339 /// Returns an error if:
2340 /// - The asset UUID is invalid or not found
2341 /// - The asset is already locked
2342 /// - The user lacks permission to lock the asset
2343 /// - Authentication fails or token is invalid
2344 /// - Network connectivity issues occur
2345 /// - The server returns an error status
2346 /// - The response cannot be parsed
2347 pub async fn lock_asset(&self, asset_uuid: &str) -> Result<Asset, Error> {
2348 self.request_json(Method::PUT, &["assets", asset_uuid, "lock"], None::<&()>)
2349 .await
2350 }
2351
2352 /// # Errors
2353 /// Returns an error if:
2354 /// - The asset UUID is invalid or not found
2355 /// - The asset is not currently locked
2356 /// - The user lacks permission to unlock the asset
2357 /// - Authentication fails or token is invalid
2358 /// - Network connectivity issues occur
2359 /// - The server returns an error status
2360 /// - The response cannot be parsed
2361 pub async fn unlock_asset(&self, asset_uuid: &str) -> Result<Asset, Error> {
2362 self.request_json(Method::PUT, &["assets", asset_uuid, "unlock"], None::<&()>)
2363 .await
2364 }
2365
2366 /// # Errors
2367 /// Returns an error if:
2368 /// - The asset UUID is invalid or not found
2369 /// - The activity parameters are invalid
2370 /// - Authentication fails or token is invalid
2371 /// - Network connectivity issues occur
2372 /// - The server returns an error status
2373 /// - The response cannot be parsed
2374 pub async fn get_asset_activities(
2375 &self,
2376 asset_uuid: &str,
2377 params: &AssetActivityParams,
2378 ) -> Result<Vec<Activity>, Error> {
2379 self.request_json(
2380 Method::GET,
2381 &["assets", asset_uuid, "activities"],
2382 Some(params),
2383 )
2384 .await
2385 }
2386
2387 /// # Errors
2388 /// Returns an error if:
2389 /// - The asset UUID is invalid or not found
2390 /// - The specified height is invalid or out of range
2391 /// - Authentication fails or token is invalid
2392 /// - Network connectivity issues occur
2393 /// - The server returns an error status
2394 /// - The response cannot be parsed
2395 pub async fn get_asset_ownerships(
2396 &self,
2397 asset_uuid: &str,
2398 height: Option<i64>,
2399 ) -> Result<Vec<Ownership>, Error> {
2400 let mut path = vec!["assets", asset_uuid, "ownerships"];
2401 let height_str;
2402 if let Some(h) = height {
2403 height_str = h.to_string();
2404 path.push(&height_str);
2405 }
2406 self.request_json(Method::GET, &path, None::<&()>).await
2407 }
2408
2409 /// # Errors
2410 /// Returns an error if:
2411 /// - The asset UUID is invalid or not found
2412 /// - Authentication fails or token is invalid
2413 /// - Network connectivity issues occur
2414 /// - The server returns an error status
2415 /// - The response cannot be parsed
2416 pub async fn get_asset_balance(&self, asset_uuid: &str) -> Result<Balance, Error> {
2417 self.request_json(Method::GET, &["assets", asset_uuid, "balance"], None::<&()>)
2418 .await
2419 }
2420
2421 /// # Errors
2422 /// Returns an error if:
2423 /// - The asset UUID is invalid or not found
2424 /// - Authentication fails or token is invalid
2425 /// - Network connectivity issues occur
2426 /// - The server returns an error status
2427 /// - The response cannot be parsed
2428 pub async fn get_asset_summary(&self, asset_uuid: &str) -> Result<AssetSummary, Error> {
2429 self.request_json(Method::GET, &["assets", asset_uuid, "summary"], None::<&()>)
2430 .await
2431 }
2432
2433 /// # Errors
2434 /// Returns an error if:
2435 /// - The asset UUID is invalid or not found
2436 /// - Authentication fails or token is invalid
2437 /// - Network connectivity issues occur
2438 /// - The server returns an error status
2439 /// - The response cannot be parsed
2440 pub async fn get_asset_utxos(&self, asset_uuid: &str) -> Result<Vec<Utxo>, Error> {
2441 self.request_json(Method::GET, &["assets", asset_uuid, "utxos"], None::<&()>)
2442 .await
2443 }
2444
2445 /// Gets the memo for a specific asset.
2446 ///
2447 /// # Arguments
2448 /// * `asset_uuid` - The UUID of the asset to retrieve the memo for
2449 ///
2450 /// # Returns
2451 /// The memo string associated with the asset
2452 ///
2453 /// # Errors
2454 /// Returns an error if:
2455 /// - Authentication fails
2456 /// - The HTTP request fails
2457 /// - The server returns an error status
2458 /// - The asset does not exist
2459 /// - The response cannot be parsed
2460 pub async fn get_asset_memo(&self, asset_uuid: &str) -> Result<String, Error> {
2461 self.request_json(Method::GET, &["assets", asset_uuid, "memo"], None::<&()>)
2462 .await
2463 }
2464
2465 /// Sets a memo for the specified asset.
2466 ///
2467 /// # Arguments
2468 /// * `asset_uuid` - The UUID of the asset to set the memo for
2469 /// * `memo` - The memo string to associate with the asset
2470 ///
2471 /// # Returns
2472 /// Returns `Ok(())` on success.
2473 ///
2474 /// # Errors
2475 /// Returns an error if:
2476 /// - Authentication fails
2477 /// - The HTTP request fails
2478 /// - The server returns an error status
2479 /// - The asset does not exist
2480 /// - The memo cannot be set due to validation errors
2481 ///
2482 /// # Example
2483 /// ```rust
2484 /// # use amp_rs::ApiClient;
2485 /// # async fn example(client: &ApiClient) -> Result<(), Box<dyn std::error::Error>> {
2486 /// client.set_asset_memo("asset-uuid-123", "This is a memo for the asset").await?;
2487 /// # Ok(())
2488 /// # }
2489 /// ```
2490 pub async fn set_asset_memo(&self, asset_uuid: &str, memo: &str) -> Result<(), Error> {
2491 let token = self.get_token().await?;
2492 let mut url = self.base_url.clone();
2493 url.path_segments_mut()
2494 .unwrap()
2495 .extend(&["assets", asset_uuid, "memo", "set"]);
2496
2497 let response = self
2498 .client
2499 .request(Method::POST, url)
2500 .header(AUTHORIZATION, format!("token {token}"))
2501 .header("content-type", "application/json")
2502 .body(format!("\"{}\"", memo.replace('"', "\\\"")))
2503 .send()
2504 .await?;
2505
2506 if !response.status().is_success() {
2507 let status = response.status();
2508 let error_text = response
2509 .text()
2510 .await
2511 .unwrap_or_else(|_| "Unknown error".to_string());
2512 return Err(Error::RequestFailed(format!(
2513 "Request to [\"assets\", \"{asset_uuid}\", \"memo\", \"set\"] failed with status {status}: {error_text}"
2514 )));
2515 }
2516
2517 Ok(())
2518 }
2519
2520 /// Blacklists specific UTXOs for an asset to prevent them from being used in transactions.
2521 ///
2522 /// This method adds the specified UTXOs to the asset's blacklist, preventing them from being
2523 /// used in future transactions. This is typically used for security purposes when UTXOs are
2524 /// suspected to be compromised or need to be temporarily disabled.
2525 ///
2526 /// # Arguments
2527 /// * `asset_uuid` - The UUID of the asset to blacklist UTXOs for
2528 /// * `utxos` - A slice of `Outpoint` structs representing the UTXOs to blacklist
2529 ///
2530 /// # Returns
2531 /// Returns a vector of `Utxo` structs representing the blacklisted UTXOs with their updated status.
2532 ///
2533 /// # Errors
2534 /// Returns an error if:
2535 /// - Authentication fails or insufficient permissions
2536 /// - The asset UUID is invalid or does not exist
2537 /// - One or more UTXOs are invalid or already blacklisted
2538 /// - The HTTP request fails
2539 /// - The server returns an error status
2540 /// - The response cannot be parsed
2541 ///
2542 /// # Examples
2543 /// ```no_run
2544 /// # use amp_rs::{ApiClient, model::Outpoint};
2545 /// # #[tokio::main]
2546 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2547 /// let client = ApiClient::new().await?;
2548 ///
2549 /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
2550 /// let utxos = vec![
2551 /// Outpoint {
2552 /// txid: "abc123...".to_string(),
2553 /// vout: 0,
2554 /// },
2555 /// Outpoint {
2556 /// txid: "def456...".to_string(),
2557 /// vout: 1,
2558 /// },
2559 /// ];
2560 ///
2561 /// let blacklisted_utxos = client.blacklist_asset_utxos(asset_uuid, &utxos).await?;
2562 /// println!("Blacklisted {} UTXOs", blacklisted_utxos.len());
2563 /// # Ok(())
2564 /// # }
2565 /// ```
2566 ///
2567 /// # Related Methods
2568 /// - [`whitelist_asset_utxos`](Self::whitelist_asset_utxos) - Remove UTXOs from blacklist
2569 /// - [`get_asset`](Self::get_asset) - Get asset information including UTXO status
2570 pub async fn blacklist_asset_utxos(
2571 &self,
2572 asset_uuid: &str,
2573 utxos: &[Outpoint],
2574 ) -> Result<Vec<Utxo>, Error> {
2575 self.request_json(
2576 Method::POST,
2577 &["assets", asset_uuid, "utxos", "blacklist"],
2578 Some(utxos),
2579 )
2580 .await
2581 }
2582
2583 /// Removes UTXOs from the asset's blacklist, allowing them to be used in transactions again.
2584 ///
2585 /// This method removes the specified UTXOs from the asset's blacklist, restoring their ability
2586 /// to be used in transactions. This is the reverse operation of blacklisting UTXOs.
2587 ///
2588 /// # Arguments
2589 /// * `asset_uuid` - The UUID of the asset to whitelist UTXOs for
2590 /// * `utxos` - A slice of `Outpoint` structs representing the UTXOs to remove from blacklist
2591 ///
2592 /// # Returns
2593 /// Returns a vector of `Utxo` structs representing the whitelisted UTXOs with their updated status.
2594 ///
2595 /// # Errors
2596 /// Returns an error if:
2597 /// - Authentication fails or insufficient permissions
2598 /// - The asset UUID is invalid or does not exist
2599 /// - One or more UTXOs are invalid or not currently blacklisted
2600 /// - The HTTP request fails
2601 /// - The server returns an error status
2602 /// - The response cannot be parsed
2603 ///
2604 /// # Examples
2605 /// ```no_run
2606 /// # use amp_rs::{ApiClient, model::Outpoint};
2607 /// # #[tokio::main]
2608 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2609 /// let client = ApiClient::new().await?;
2610 ///
2611 /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
2612 /// let utxos = vec![
2613 /// Outpoint {
2614 /// txid: "abc123...".to_string(),
2615 /// vout: 0,
2616 /// },
2617 /// ];
2618 ///
2619 /// let whitelisted_utxos = client.whitelist_asset_utxos(asset_uuid, &utxos).await?;
2620 /// println!("Whitelisted {} UTXOs", whitelisted_utxos.len());
2621 /// # Ok(())
2622 /// # }
2623 /// ```
2624 ///
2625 /// # Related Methods
2626 /// - [`blacklist_asset_utxos`](Self::blacklist_asset_utxos) - Add UTXOs to blacklist
2627 /// - [`get_asset`](Self::get_asset) - Get asset information including UTXO status
2628 pub async fn whitelist_asset_utxos(
2629 &self,
2630 asset_uuid: &str,
2631 utxos: &[Outpoint],
2632 ) -> Result<Vec<Utxo>, Error> {
2633 self.request_json(
2634 Method::POST,
2635 &["assets", asset_uuid, "utxos", "whitelist"],
2636 Some(utxos),
2637 )
2638 .await
2639 }
2640
2641 /// Gets the treasury addresses for a specific asset
2642 ///
2643 /// # Arguments
2644 /// * `asset_uuid` - The UUID of the asset to get treasury addresses for
2645 ///
2646 /// # Returns
2647 /// A vector of treasury addresses as strings
2648 ///
2649 /// # Errors
2650 /// Returns an error if:
2651 /// - The asset does not exist
2652 /// - The request fails
2653 /// - The response cannot be parsed
2654 pub async fn get_asset_treasury_addresses(
2655 &self,
2656 asset_uuid: &str,
2657 ) -> Result<Vec<String>, Error> {
2658 self.request_json(
2659 Method::GET,
2660 &["assets", asset_uuid, "treasury-addresses"],
2661 None::<&()>,
2662 )
2663 .await
2664 }
2665
2666 /// Adds treasury addresses to a specific asset
2667 ///
2668 /// # Arguments
2669 /// * `asset_uuid` - The UUID of the asset to add treasury addresses to
2670 /// * `addresses` - A slice of address strings to add as treasury addresses
2671 ///
2672 /// # Returns
2673 /// Returns `Ok(())` on success
2674 ///
2675 /// # Errors
2676 /// Returns an error if:
2677 /// - The asset does not exist
2678 /// - The addresses are invalid
2679 /// - The request fails
2680 /// - Insufficient permissions
2681 pub async fn add_asset_treasury_addresses(
2682 &self,
2683 asset_uuid: &str,
2684 addresses: &[String],
2685 ) -> Result<(), Error> {
2686 self.request_empty(
2687 Method::POST,
2688 &["assets", asset_uuid, "treasury-addresses", "add"],
2689 Some(addresses),
2690 )
2691 .await
2692 }
2693
2694 /// Removes treasury addresses from a specific asset.
2695 ///
2696 /// This method removes the specified addresses from the asset's treasury address list.
2697 /// Treasury addresses are special addresses that can be used for asset management operations
2698 /// such as reissuance and burning.
2699 ///
2700 /// # Arguments
2701 /// * `asset_uuid` - The UUID of the asset to remove treasury addresses from
2702 /// * `addresses` - A slice of address strings to remove from the treasury addresses
2703 ///
2704 /// # Returns
2705 /// Returns `Ok(())` on successful removal.
2706 ///
2707 /// # Errors
2708 /// Returns an error if:
2709 /// - Authentication fails or insufficient permissions
2710 /// - The asset UUID is invalid or does not exist
2711 /// - One or more addresses are invalid or not currently treasury addresses
2712 /// - The HTTP request fails
2713 /// - The server returns an error status
2714 /// - Attempting to remove the last treasury address (if not allowed)
2715 ///
2716 /// # Examples
2717 /// ```no_run
2718 /// # use amp_rs::ApiClient;
2719 /// # #[tokio::main]
2720 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2721 /// let client = ApiClient::new().await?;
2722 ///
2723 /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
2724 /// let addresses = vec![
2725 /// "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh".to_string(),
2726 /// "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4".to_string(),
2727 /// ];
2728 ///
2729 /// client.delete_asset_treasury_addresses(asset_uuid, &addresses).await?;
2730 /// println!("Removed {} treasury addresses", addresses.len());
2731 /// # Ok(())
2732 /// # }
2733 /// ```
2734 ///
2735 /// # Related Methods
2736 /// - [`add_asset_treasury_addresses`](Self::add_asset_treasury_addresses) - Add treasury addresses
2737 /// - [`get_asset_treasury_addresses`](Self::get_asset_treasury_addresses) - Get current treasury addresses
2738 /// - [`reissue_asset`](Self::reissue_asset) - Reissue assets using treasury addresses
2739 pub async fn delete_asset_treasury_addresses(
2740 &self,
2741 asset_uuid: &str,
2742 addresses: &[String],
2743 ) -> Result<(), Error> {
2744 self.request_empty(
2745 Method::DELETE,
2746 &["assets", asset_uuid, "treasury-addresses", "delete"],
2747 Some(addresses),
2748 )
2749 .await
2750 }
2751
2752 /// Gets a list of all registered users.
2753 ///
2754 /// # Errors
2755 /// Returns an error if:
2756 /// - Authentication fails
2757 /// - The HTTP request fails
2758 /// - The server returns an error status
2759 /// - The response cannot be parsed
2760 ///
2761 /// # Examples
2762 /// ```no_run
2763 /// # use amp_rs::ApiClient;
2764 /// # #[tokio::main]
2765 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2766 /// let client = ApiClient::new().await?;
2767 ///
2768 /// let users = client.get_registered_users().await?;
2769 /// for user in users {
2770 /// println!("User: {} (ID: {})", user.name, user.id);
2771 /// }
2772 /// # Ok(())
2773 /// # }
2774 /// ```
2775 pub async fn get_registered_users(
2776 &self,
2777 ) -> Result<Vec<crate::model::RegisteredUserResponse>, Error> {
2778 self.request_json(Method::GET, &["registered_users"], None::<&()>)
2779 .await
2780 }
2781
2782 /// Gets a specific registered user by ID.
2783 ///
2784 /// # Arguments
2785 /// * `user_id` - The ID of the registered user to retrieve
2786 ///
2787 /// # Errors
2788 /// Returns an error if:
2789 /// - Authentication fails
2790 /// - The HTTP request fails
2791 /// - The user ID does not exist
2792 /// - The response cannot be parsed
2793 ///
2794 /// # Examples
2795 /// ```no_run
2796 /// # use amp_rs::ApiClient;
2797 /// # #[tokio::main]
2798 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2799 /// let client = ApiClient::new().await?;
2800 ///
2801 /// let user = client.get_registered_user(1).await?;
2802 /// println!("User: {} (ID: {})", user.name, user.id);
2803 /// # Ok(())
2804 /// # }
2805 /// ```
2806 pub async fn get_registered_user(
2807 &self,
2808 user_id: i64,
2809 ) -> Result<crate::model::RegisteredUserResponse, Error> {
2810 self.request_json(
2811 Method::GET,
2812 &["registered_users", &user_id.to_string()],
2813 None::<&()>,
2814 )
2815 .await
2816 }
2817
2818 /// Creates a new registered user in the AMP system.
2819 ///
2820 /// This method creates a new registered user with the provided information. Registered users
2821 /// can be associated with GAIDs, assigned to categories, and receive asset assignments.
2822 ///
2823 /// # Arguments
2824 /// * `new_user` - A `RegisteredUserAdd` struct containing the user information to create
2825 ///
2826 /// # Returns
2827 /// Returns a `RegisteredUserResponse` containing the created user's information including
2828 /// the assigned user ID.
2829 ///
2830 /// # Errors
2831 /// Returns an error if:
2832 /// - Authentication fails or insufficient permissions
2833 /// - The user data is invalid (e.g., missing required fields, invalid email format)
2834 /// - A user with the same identifier already exists
2835 /// - The HTTP request fails
2836 /// - The server returns an error status
2837 /// - The response cannot be parsed
2838 ///
2839 /// # Examples
2840 /// ```no_run
2841 /// # use amp_rs::{ApiClient, model::RegisteredUserAdd};
2842 /// # #[tokio::main]
2843 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2844 /// let client = ApiClient::new().await?;
2845 ///
2846 /// let new_user = RegisteredUserAdd {
2847 /// name: "John Doe".to_string(),
2848 /// gaid: Some("GAbYScu6jkWUND2jo3L4KJxyvo55d".to_string()),
2849 /// is_company: false,
2850 /// };
2851 ///
2852 /// let created_user = client.add_registered_user(&new_user).await?;
2853 /// println!("Created user: {} with ID {}", created_user.name, created_user.id);
2854 /// # Ok(())
2855 /// # }
2856 /// ```
2857 ///
2858 /// # Related Methods
2859 /// - [`get_registered_users`](Self::get_registered_users) - List all registered users
2860 /// - [`edit_registered_user`](Self::edit_registered_user) - Update user information
2861 /// - [`delete_registered_user`](Self::delete_registered_user) - Remove a user
2862 pub async fn add_registered_user(
2863 &self,
2864 new_user: &crate::model::RegisteredUserAdd,
2865 ) -> Result<crate::model::RegisteredUserResponse, Error> {
2866 self.request_json(Method::POST, &["registered_users", "add"], Some(new_user))
2867 .await
2868 }
2869
2870 /// Removes a registered user from the AMP system.
2871 ///
2872 /// This method permanently deletes a registered user and all associated data. This operation
2873 /// cannot be undone. Any GAIDs associated with the user will be disassociated, and any
2874 /// pending assignments may be affected.
2875 ///
2876 /// # Arguments
2877 /// * `user_id` - The ID of the registered user to delete
2878 ///
2879 /// # Returns
2880 /// Returns `Ok(())` on successful deletion.
2881 ///
2882 /// # Errors
2883 /// Returns an error if:
2884 /// - Authentication fails or insufficient permissions
2885 /// - The user ID is invalid or does not exist
2886 /// - The user has active assignments that prevent deletion
2887 /// - The HTTP request fails
2888 /// - The server returns an error status
2889 ///
2890 /// # Examples
2891 /// ```no_run
2892 /// # use amp_rs::ApiClient;
2893 /// # #[tokio::main]
2894 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2895 /// let client = ApiClient::new().await?;
2896 ///
2897 /// let user_id = 123;
2898 /// client.delete_registered_user(user_id).await?;
2899 /// println!("Successfully deleted user with ID {}", user_id);
2900 /// # Ok(())
2901 /// # }
2902 /// ```
2903 ///
2904 /// # Related Methods
2905 /// - [`get_registered_user`](Self::get_registered_user) - Get user information before deletion
2906 /// - [`add_registered_user`](Self::add_registered_user) - Create a new user
2907 /// - [`get_registered_user_summary`](Self::get_registered_user_summary) - Check user's assignments
2908 pub async fn delete_registered_user(&self, user_id: i64) -> Result<(), Error> {
2909 self.request_empty(
2910 Method::DELETE,
2911 &["registered_users", &user_id.to_string(), "delete"],
2912 None::<&()>,
2913 )
2914 .await
2915 }
2916
2917 /// Updates registered user information.
2918 ///
2919 /// This method allows you to modify the information of an existing registered user.
2920 /// Only the fields provided in the edit data will be updated; other fields remain unchanged.
2921 ///
2922 /// # Arguments
2923 /// * `registered_user_id` - The ID of the registered user to update
2924 /// * `edit_data` - A `RegisteredUserEdit` struct containing the fields to update
2925 ///
2926 /// # Returns
2927 /// Returns a `RegisteredUserResponse` containing the updated user information.
2928 ///
2929 /// # Errors
2930 /// Returns an error if:
2931 /// - Authentication fails or insufficient permissions
2932 /// - The user ID is invalid or does not exist
2933 /// - The edit data contains invalid values (e.g., invalid email format)
2934 /// - The HTTP request fails
2935 /// - The server returns an error status
2936 /// - The response cannot be parsed
2937 ///
2938 /// # Examples
2939 /// ```no_run
2940 /// # use amp_rs::{ApiClient, model::RegisteredUserEdit};
2941 /// # #[tokio::main]
2942 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2943 /// let client = ApiClient::new().await?;
2944 ///
2945 /// let user_id = 123;
2946 /// let edit_data = RegisteredUserEdit {
2947 /// name: Some("Jane Doe".to_string()),
2948 /// };
2949 ///
2950 /// let updated_user = client.edit_registered_user(user_id, &edit_data).await?;
2951 /// println!("Updated user: {}", updated_user.name);
2952 /// # Ok(())
2953 /// # }
2954 /// ```
2955 ///
2956 /// # Related Methods
2957 /// - [`get_registered_user`](Self::get_registered_user) - Get current user information
2958 /// - [`add_registered_user`](Self::add_registered_user) - Create a new user
2959 /// - [`delete_registered_user`](Self::delete_registered_user) - Remove a user
2960 pub async fn edit_registered_user(
2961 &self,
2962 registered_user_id: i64,
2963 edit_data: &crate::model::RegisteredUserEdit,
2964 ) -> Result<crate::model::RegisteredUserResponse, Error> {
2965 self.request_json(
2966 Method::PUT,
2967 &["registered_users", ®istered_user_id.to_string(), "edit"],
2968 Some(edit_data),
2969 )
2970 .await
2971 }
2972
2973 /// Gets comprehensive summary information for a registered user including assets and distributions.
2974 ///
2975 /// This method retrieves detailed summary information about a registered user, including
2976 /// their basic information, associated assets, assignment history, and distribution records.
2977 /// This provides a complete overview of the user's activity and holdings in the system.
2978 ///
2979 /// # Arguments
2980 /// * `registered_user_id` - The ID of the registered user to get summary for
2981 ///
2982 /// # Returns
2983 /// Returns a `RegisteredUserSummary` containing:
2984 /// - Basic user information (name, email, etc.)
2985 /// - List of associated GAIDs
2986 /// - Asset assignments and their status
2987 /// - Distribution history
2988 /// - Balance information
2989 /// - Activity timestamps
2990 ///
2991 /// # Errors
2992 /// Returns an error if:
2993 /// - Authentication fails or insufficient permissions
2994 /// - The user ID is invalid or does not exist
2995 /// - The HTTP request fails
2996 /// - The server returns an error status
2997 /// - The response cannot be parsed
2998 ///
2999 /// # Examples
3000 /// ```no_run
3001 /// # use amp_rs::ApiClient;
3002 /// # #[tokio::main]
3003 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3004 /// let client = ApiClient::new().await?;
3005 ///
3006 /// let user_id = 123;
3007 /// let summary = client.get_registered_user_summary(user_id).await?;
3008 ///
3009 /// println!("Asset UUID: {}", summary.asset_uuid);
3010 /// println!("Asset ID: {}", summary.asset_id);
3011 /// println!("Asset assignments: {}", summary.assignments.len());
3012 /// println!("Distributions received: {}", summary.distributions.len());
3013 /// # Ok(())
3014 /// # }
3015 /// ```
3016 ///
3017 /// # Related Methods
3018 /// - [`get_registered_user`](Self::get_registered_user) - Get basic user information
3019 /// - [`get_registered_user_gaids`](Self::get_registered_user_gaids) - Get only GAIDs
3020 /// - [`get_asset_assignments`](Self::get_asset_assignments) - Get assignments for specific asset
3021 pub async fn get_registered_user_summary(
3022 &self,
3023 registered_user_id: i64,
3024 ) -> Result<crate::model::RegisteredUserSummary, Error> {
3025 self.request_json(
3026 Method::GET,
3027 &[
3028 "registered_users",
3029 ®istered_user_id.to_string(),
3030 "summary",
3031 ],
3032 None::<&()>,
3033 )
3034 .await
3035 }
3036
3037 /// Gets all GAIDs (Green Address IDs) associated with a registered user.
3038 ///
3039 /// This method retrieves a list of all GAIDs that are currently associated with the specified
3040 /// registered user. GAIDs are unique identifiers that can be used to receive assets and
3041 /// track ownership.
3042 ///
3043 /// # Arguments
3044 /// * `registered_user_id` - The ID of the registered user to get GAIDs for
3045 ///
3046 /// # Returns
3047 /// Returns a vector of GAID strings associated with the user.
3048 ///
3049 /// # Errors
3050 /// Returns an error if:
3051 /// - Authentication fails or insufficient permissions
3052 /// - The user ID is invalid or does not exist
3053 /// - The HTTP request fails
3054 /// - The server returns an error status
3055 /// - The response cannot be parsed
3056 ///
3057 /// # Examples
3058 /// ```no_run
3059 /// # use amp_rs::ApiClient;
3060 /// # #[tokio::main]
3061 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3062 /// let client = ApiClient::new().await?;
3063 ///
3064 /// let user_id = 123;
3065 /// let gaids = client.get_registered_user_gaids(user_id).await?;
3066 ///
3067 /// println!("User {} has {} associated GAIDs:", user_id, gaids.len());
3068 /// for gaid in gaids {
3069 /// println!(" - {}", gaid);
3070 /// }
3071 /// # Ok(())
3072 /// # }
3073 /// ```
3074 ///
3075 /// # Related Methods
3076 /// - [`add_gaid_to_registered_user`](Self::add_gaid_to_registered_user) - Associate a GAID with user
3077 /// - [`set_default_gaid_for_registered_user`](Self::set_default_gaid_for_registered_user) - Set default GAID
3078 /// - [`get_gaid_registered_user`](Self::get_gaid_registered_user) - Find user by GAID
3079 /// - [`validate_gaid`](Self::validate_gaid) - Validate GAID format
3080 pub async fn get_registered_user_gaids(
3081 &self,
3082 registered_user_id: i64,
3083 ) -> Result<Vec<String>, Error> {
3084 self.request_json(
3085 Method::GET,
3086 &["registered_users", ®istered_user_id.to_string(), "gaids"],
3087 None::<&()>,
3088 )
3089 .await
3090 }
3091
3092 /// Associates a GAID with a registered user.
3093 ///
3094 /// # Arguments
3095 /// * `registered_user_id` - The ID of the registered user
3096 /// * `gaid` - The GAID to associate with the user
3097 ///
3098 /// # Errors
3099 ///
3100 /// Returns an error if:
3101 /// - Authentication fails
3102 /// - The HTTP request fails
3103 /// - The server returns an error status
3104 /// - The registered user ID is invalid
3105 /// - The GAID is invalid or already associated
3106 pub async fn add_gaid_to_registered_user(
3107 &self,
3108 registered_user_id: i64,
3109 gaid: &str,
3110 ) -> Result<(), Error> {
3111 let request = GaidRequest {
3112 gaid: gaid.to_string(),
3113 };
3114
3115 self.request_empty(
3116 Method::POST,
3117 &[
3118 "registered_users",
3119 ®istered_user_id.to_string(),
3120 "gaids",
3121 "add",
3122 ],
3123 Some(request),
3124 )
3125 .await
3126 }
3127
3128 /// Sets an existing GAID as the default for a registered user.
3129 ///
3130 /// This method allows you to designate a specific GAID as the primary/default
3131 /// GAID for a registered user. The GAID must already be associated with the user.
3132 ///
3133 /// # Arguments
3134 /// * `registered_user_id` - The ID of the registered user
3135 /// * `gaid` - The GAID to set as default
3136 ///
3137 /// # Returns
3138 /// Returns `Ok(())` if the operation is successful.
3139 ///
3140 /// # Errors
3141 /// Returns an error if:
3142 /// - Authentication fails
3143 /// - The HTTP request fails
3144 /// - The server returns an error status
3145 /// - The registered user ID is invalid
3146 /// - The GAID is not associated with the user
3147 pub async fn set_default_gaid_for_registered_user(
3148 &self,
3149 registered_user_id: i64,
3150 gaid: &str,
3151 ) -> Result<(), Error> {
3152 let request = GaidRequest {
3153 gaid: gaid.to_string(),
3154 };
3155
3156 self.request_empty(
3157 Method::POST,
3158 &[
3159 "registered_users",
3160 ®istered_user_id.to_string(),
3161 "gaids",
3162 "set-default",
3163 ],
3164 Some(request),
3165 )
3166 .await
3167 }
3168
3169 /// Retrieves the registered user associated with a GAID
3170 ///
3171 /// # Arguments
3172 /// * `gaid` - The GAID to look up
3173 ///
3174 /// # Returns
3175 /// Returns the registered user data if the GAID is associated with a user
3176 ///
3177 /// # Errors
3178 /// This function will return an error if:
3179 /// - The GAID has no associated user
3180 /// - The GAID is invalid
3181 /// - Network or authentication errors occur
3182 pub async fn get_gaid_registered_user(
3183 &self,
3184 gaid: &str,
3185 ) -> Result<crate::model::RegisteredUserResponse, Error> {
3186 self.request_json(
3187 Method::GET,
3188 &["gaids", gaid, "registered_user"],
3189 None::<&()>,
3190 )
3191 .await
3192 }
3193
3194 /// Gets the balance information for a specific GAID.
3195 ///
3196 /// This method retrieves all asset balances associated with the given GAID,
3197 /// including confirmed balances and any lost outputs.
3198 ///
3199 /// # Arguments
3200 /// * `gaid` - The GAID to query balance for
3201 ///
3202 /// # Returns
3203 /// Returns a `Balance` struct containing confirmed balances and lost outputs
3204 ///
3205 /// # Errors
3206 /// Returns an error if:
3207 /// - The GAID is invalid
3208 /// - Network or authentication errors occur
3209 /// - The response cannot be parsed
3210 ///
3211 /// # Examples
3212 /// ```no_run
3213 /// # use amp_rs::ApiClient;
3214 /// # #[tokio::main]
3215 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3216 /// let client = ApiClient::new().await?;
3217 ///
3218 /// let gaid = "GAbYScu6jkWUND2jo3L4KJxyvo55d";
3219 /// let balance = client.get_gaid_balance(gaid).await?;
3220 ///
3221 /// println!("GAID {} has {} balance entries", gaid, balance.len());
3222 /// for entry in balance {
3223 /// println!("Asset {}: {} units", entry.asset_id, entry.balance);
3224 /// }
3225 /// # Ok(())
3226 /// # }
3227 /// ```
3228 pub async fn get_gaid_balance(&self, gaid: &str) -> Result<Balance, Error> {
3229 self.request_json(Method::GET, &["gaids", gaid, "balance"], None::<&()>)
3230 .await
3231 }
3232
3233 /// Retrieves the specific asset balance for a GAID
3234 ///
3235 /// # Arguments
3236 /// * `gaid` - The GAID to query
3237 /// * `asset_uuid` - The UUID of the asset to query
3238 ///
3239 /// # Returns
3240 /// Returns the specific asset balance information
3241 ///
3242 /// # Errors
3243 /// Returns an error if:
3244 /// - The GAID is invalid
3245 /// - The asset UUID is invalid
3246 /// - Network or authentication errors occur
3247 /// - The response cannot be parsed
3248 pub async fn get_gaid_asset_balance(
3249 &self,
3250 gaid: &str,
3251 asset_uuid: &str,
3252 ) -> Result<Ownership, Error> {
3253 // Try to get the response as a GaidBalanceEntry first, then convert to Ownership
3254 let balance_entry: GaidBalanceEntry = self
3255 .request_json(
3256 Method::GET,
3257 &["gaids", gaid, "balance", asset_uuid],
3258 None::<&()>,
3259 )
3260 .await?;
3261
3262 // Convert GaidBalanceEntry to Ownership format
3263 Ok(Ownership {
3264 owner: gaid.to_string(),
3265 amount: balance_entry.balance,
3266 gaid: Some(gaid.to_string()),
3267 })
3268 }
3269
3270 /// Gets a list of all categories.
3271 ///
3272 /// # Returns
3273 /// Returns a vector of `CategoryResponse` objects
3274 ///
3275 /// # Errors
3276 /// Returns an error if:
3277 /// - Authentication fails
3278 /// - The HTTP request fails
3279 /// - The response cannot be parsed
3280 ///
3281 /// # Examples
3282 /// ```no_run
3283 /// # use amp_rs::ApiClient;
3284 /// # #[tokio::main]
3285 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3286 /// let client = ApiClient::new().await?;
3287 ///
3288 /// let categories = client.get_categories().await?;
3289 /// for category in categories {
3290 /// println!("Category: {} (ID: {})", category.name, category.id);
3291 /// if let Some(desc) = category.description {
3292 /// println!(" Description: {}", desc);
3293 /// }
3294 /// }
3295 /// # Ok(())
3296 /// # }
3297 /// ```
3298 pub async fn get_categories(&self) -> Result<Vec<CategoryResponse>, Error> {
3299 self.request_json(Method::GET, &["categories"], None::<&()>)
3300 .await
3301 }
3302
3303 /// Creates a new category for organizing users and assets.
3304 ///
3305 /// This method creates a new category that can be used to group registered users and assets
3306 /// for organizational purposes. Categories help manage permissions and provide logical
3307 /// groupings for assets and users.
3308 ///
3309 /// # Arguments
3310 /// * `new_category` - A `CategoryAdd` struct containing the category information to create
3311 ///
3312 /// # Returns
3313 /// Returns a `CategoryResponse` containing the created category information including
3314 /// the assigned category ID.
3315 ///
3316 /// # Errors
3317 /// Returns an error if:
3318 /// - Authentication fails or insufficient permissions
3319 /// - The category data is invalid (e.g., missing name, invalid characters)
3320 /// - A category with the same name already exists
3321 /// - The HTTP request fails
3322 /// - The server returns an error status
3323 /// - The response cannot be parsed
3324 ///
3325 /// # Examples
3326 /// ```no_run
3327 /// # use amp_rs::{ApiClient, model::CategoryAdd};
3328 /// # #[tokio::main]
3329 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3330 /// let client = ApiClient::new().await?;
3331 ///
3332 /// let new_category = CategoryAdd {
3333 /// name: "Premium Users".to_string(),
3334 /// description: Some("High-value users with special privileges".to_string()),
3335 /// };
3336 ///
3337 /// let created_category = client.add_category(&new_category).await?;
3338 /// println!("Created category: {} with ID {}", created_category.name, created_category.id);
3339 /// # Ok(())
3340 /// # }
3341 /// ```
3342 ///
3343 /// # Related Methods
3344 /// - [`get_categories`](Self::get_categories) - List all categories
3345 /// - [`edit_category`](Self::edit_category) - Update category information
3346 /// - [`delete_category`](Self::delete_category) - Remove a category
3347 /// - [`add_registered_user_to_category`](Self::add_registered_user_to_category) - Add users to category
3348 pub async fn add_category(
3349 &self,
3350 new_category: &CategoryAdd,
3351 ) -> Result<CategoryResponse, Error> {
3352 self.request_json(Method::POST, &["categories", "add"], Some(new_category))
3353 .await
3354 }
3355
3356 /// Gets a specific category by ID.
3357 ///
3358 /// This method retrieves detailed information about a specific category, including
3359 /// its name, description, and associated users and assets.
3360 ///
3361 /// # Arguments
3362 /// * `category_id` - The ID of the category to retrieve
3363 ///
3364 /// # Returns
3365 /// Returns a `CategoryResponse` containing the category information including:
3366 /// - Category ID, name, and description
3367 /// - List of associated registered users
3368 /// - List of associated assets
3369 /// - Creation and modification timestamps
3370 ///
3371 /// # Errors
3372 /// Returns an error if:
3373 /// - Authentication fails or insufficient permissions
3374 /// - The category ID is invalid or does not exist
3375 /// - The HTTP request fails
3376 /// - The server returns an error status
3377 /// - The response cannot be parsed
3378 ///
3379 /// # Examples
3380 /// ```no_run
3381 /// # use amp_rs::ApiClient;
3382 /// # #[tokio::main]
3383 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3384 /// let client = ApiClient::new().await?;
3385 ///
3386 /// let category_id = 1;
3387 /// let category = client.get_category(category_id).await?;
3388 ///
3389 /// println!("Category: {} (ID: {})", category.name, category.id);
3390 /// if let Some(desc) = category.description {
3391 /// println!("Description: {}", desc);
3392 /// }
3393 /// println!("Users: {}, Assets: {}", category.registered_users.len(), category.assets.len());
3394 /// # Ok(())
3395 /// # }
3396 /// ```
3397 ///
3398 /// # Related Methods
3399 /// - [`get_categories`](Self::get_categories) - List all categories
3400 /// - [`add_category`](Self::add_category) - Create a new category
3401 /// - [`edit_category`](Self::edit_category) - Update category information
3402 /// - [`delete_category`](Self::delete_category) - Remove a category
3403 pub async fn get_category(&self, category_id: i64) -> Result<CategoryResponse, Error> {
3404 self.request_json(
3405 Method::GET,
3406 &["categories", &category_id.to_string()],
3407 None::<&()>,
3408 )
3409 .await
3410 }
3411
3412 /// Updates category information.
3413 ///
3414 /// This method allows you to modify the information of an existing category.
3415 /// Only the fields provided in the edit data will be updated; other fields remain unchanged.
3416 ///
3417 /// # Arguments
3418 /// * `category_id` - The ID of the category to update
3419 /// * `edit_category` - A `CategoryEdit` struct containing the fields to update
3420 ///
3421 /// # Returns
3422 /// Returns a `CategoryResponse` containing the updated category information.
3423 ///
3424 /// # Errors
3425 /// Returns an error if:
3426 /// - Authentication fails or insufficient permissions
3427 /// - The category ID is invalid or does not exist
3428 /// - The edit data contains invalid values (e.g., empty name, invalid characters)
3429 /// - A category with the new name already exists (if name is being changed)
3430 /// - The HTTP request fails
3431 /// - The server returns an error status
3432 /// - The response cannot be parsed
3433 ///
3434 /// # Examples
3435 /// ```no_run
3436 /// # use amp_rs::{ApiClient, model::CategoryEdit};
3437 /// # #[tokio::main]
3438 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3439 /// let client = ApiClient::new().await?;
3440 ///
3441 /// let category_id = 1;
3442 /// let edit_data = CategoryEdit {
3443 /// name: Some("VIP Users".to_string()),
3444 /// description: Some("Very important users with premium access".to_string()),
3445 /// };
3446 ///
3447 /// let updated_category = client.edit_category(category_id, &edit_data).await?;
3448 /// println!("Updated category: {}", updated_category.name);
3449 /// # Ok(())
3450 /// # }
3451 /// ```
3452 ///
3453 /// # Related Methods
3454 /// - [`get_category`](Self::get_category) - Get current category information
3455 /// - [`add_category`](Self::add_category) - Create a new category
3456 /// - [`delete_category`](Self::delete_category) - Remove a category
3457 pub async fn edit_category(
3458 &self,
3459 category_id: i64,
3460 edit_category: &CategoryEdit,
3461 ) -> Result<CategoryResponse, Error> {
3462 self.request_json(
3463 Method::PUT,
3464 &["categories", &category_id.to_string(), "edit"],
3465 Some(edit_category),
3466 )
3467 .await
3468 }
3469
3470 /// Removes a category from the system.
3471 ///
3472 /// This method permanently deletes a category. All users and assets associated with the
3473 /// category will be disassociated, but the users and assets themselves are not deleted.
3474 /// This operation cannot be undone.
3475 ///
3476 /// # Arguments
3477 /// * `category_id` - The ID of the category to delete
3478 ///
3479 /// # Returns
3480 /// Returns `Ok(())` on successful deletion.
3481 ///
3482 /// # Errors
3483 /// Returns an error if:
3484 /// - Authentication fails or insufficient permissions
3485 /// - The category ID is invalid or does not exist
3486 /// - The category is still in use and cannot be deleted (depending on system configuration)
3487 /// - The HTTP request fails
3488 /// - The server returns an error status
3489 ///
3490 /// # Examples
3491 /// ```no_run
3492 /// # use amp_rs::ApiClient;
3493 /// # #[tokio::main]
3494 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3495 /// let client = ApiClient::new().await?;
3496 ///
3497 /// let category_id = 1;
3498 /// client.delete_category(category_id).await?;
3499 /// println!("Successfully deleted category with ID {}", category_id);
3500 /// # Ok(())
3501 /// # }
3502 /// ```
3503 ///
3504 /// # Related Methods
3505 /// - [`get_category`](Self::get_category) - Get category information before deletion
3506 /// - [`add_category`](Self::add_category) - Create a new category
3507 /// - [`remove_registered_user_from_category`](Self::remove_registered_user_from_category) - Remove users first
3508 /// - [`remove_asset_from_category`](Self::remove_asset_from_category) - Remove assets first
3509 pub async fn delete_category(&self, category_id: i64) -> Result<(), Error> {
3510 self.request_empty(
3511 Method::DELETE,
3512 &["categories", &category_id.to_string(), "delete"],
3513 None::<&()>,
3514 )
3515 .await
3516 }
3517
3518 /// Associates a registered user with a category.
3519 ///
3520 /// This method adds a registered user to a category, allowing for organized grouping
3521 /// of users. Users can belong to multiple categories, and categories can contain
3522 /// multiple users.
3523 ///
3524 /// # Arguments
3525 /// * `category_id` - The ID of the category to add the user to
3526 /// * `user_id` - The ID of the registered user to add to the category
3527 ///
3528 /// # Returns
3529 /// Returns a `CategoryResponse` containing the updated category information including
3530 /// the newly added user.
3531 ///
3532 /// # Errors
3533 /// Returns an error if:
3534 /// - Authentication fails or insufficient permissions
3535 /// - The category ID is invalid or does not exist
3536 /// - The user ID is invalid or does not exist
3537 /// - The user is already associated with the category
3538 /// - The HTTP request fails
3539 /// - The server returns an error status
3540 /// - The response cannot be parsed
3541 ///
3542 /// # Examples
3543 /// ```no_run
3544 /// # use amp_rs::ApiClient;
3545 /// # #[tokio::main]
3546 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3547 /// let client = ApiClient::new().await?;
3548 ///
3549 /// let category_id = 1;
3550 /// let user_id = 123;
3551 ///
3552 /// let updated_category = client.add_registered_user_to_category(category_id, user_id).await?;
3553 /// println!("Added user {} to category '{}'", user_id, updated_category.name);
3554 /// println!("Category now has {} users", updated_category.registered_users.len());
3555 /// # Ok(())
3556 /// # }
3557 /// ```
3558 ///
3559 /// # Related Methods
3560 /// - [`remove_registered_user_from_category`](Self::remove_registered_user_from_category) - Remove user from category
3561 /// - [`get_category`](Self::get_category) - Get category information including users
3562 /// - [`get_registered_user`](Self::get_registered_user) - Get user information
3563 pub async fn add_registered_user_to_category(
3564 &self,
3565 category_id: i64,
3566 user_id: i64,
3567 ) -> Result<CategoryResponse, Error> {
3568 self.request_json(
3569 Method::PUT,
3570 &[
3571 "categories",
3572 &category_id.to_string(),
3573 "registered_users",
3574 &user_id.to_string(),
3575 "add",
3576 ],
3577 None::<&()>,
3578 )
3579 .await
3580 }
3581
3582 /// Removes a registered user from a category.
3583 ///
3584 /// This method disassociates a registered user from a category. The user remains in the
3585 /// system but is no longer part of the specified category. This does not affect the user's
3586 /// association with other categories.
3587 ///
3588 /// # Arguments
3589 /// * `category_id` - The ID of the category to remove the user from
3590 /// * `user_id` - The ID of the registered user to remove from the category
3591 ///
3592 /// # Returns
3593 /// Returns a `CategoryResponse` containing the updated category information without
3594 /// the removed user.
3595 ///
3596 /// # Errors
3597 /// Returns an error if:
3598 /// - Authentication fails or insufficient permissions
3599 /// - The category ID is invalid or does not exist
3600 /// - The user ID is invalid or does not exist
3601 /// - The user is not currently associated with the category
3602 /// - The HTTP request fails
3603 /// - The server returns an error status
3604 /// - The response cannot be parsed
3605 ///
3606 /// # Examples
3607 /// ```no_run
3608 /// # use amp_rs::ApiClient;
3609 /// # #[tokio::main]
3610 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3611 /// let client = ApiClient::new().await?;
3612 ///
3613 /// let category_id = 1;
3614 /// let user_id = 123;
3615 ///
3616 /// let updated_category = client.remove_registered_user_from_category(category_id, user_id).await?;
3617 /// println!("Removed user {} from category '{}'", user_id, updated_category.name);
3618 /// println!("Category now has {} users", updated_category.registered_users.len());
3619 /// # Ok(())
3620 /// # }
3621 /// ```
3622 ///
3623 /// # Related Methods
3624 /// - [`add_registered_user_to_category`](Self::add_registered_user_to_category) - Add user to category
3625 /// - [`get_category`](Self::get_category) - Get category information including users
3626 /// - [`get_registered_user`](Self::get_registered_user) - Get user information
3627 pub async fn remove_registered_user_from_category(
3628 &self,
3629 category_id: i64,
3630 user_id: i64,
3631 ) -> Result<CategoryResponse, Error> {
3632 self.request_json(
3633 Method::PUT,
3634 &[
3635 "categories",
3636 &category_id.to_string(),
3637 "registered_users",
3638 &user_id.to_string(),
3639 "remove",
3640 ],
3641 None::<&()>,
3642 )
3643 .await
3644 }
3645
3646 /// Associates an asset with a category.
3647 ///
3648 /// This method adds an asset to a category, allowing for organized grouping of assets.
3649 /// Assets can belong to multiple categories, and categories can contain multiple assets.
3650 /// This helps with asset management and permission organization.
3651 ///
3652 /// # Arguments
3653 /// * `category_id` - The ID of the category to add the asset to
3654 /// * `asset_uuid` - The UUID of the asset to add to the category
3655 ///
3656 /// # Returns
3657 /// Returns a `CategoryResponse` containing the updated category information including
3658 /// the newly added asset.
3659 ///
3660 /// # Errors
3661 /// Returns an error if:
3662 /// - Authentication fails or insufficient permissions
3663 /// - The category ID is invalid or does not exist
3664 /// - The asset UUID is invalid or does not exist
3665 /// - The asset is already associated with the category
3666 /// - The HTTP request fails
3667 /// - The server returns an error status
3668 /// - The response cannot be parsed
3669 ///
3670 /// # Examples
3671 /// ```no_run
3672 /// # use amp_rs::ApiClient;
3673 /// # #[tokio::main]
3674 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3675 /// let client = ApiClient::new().await?;
3676 ///
3677 /// let category_id = 1;
3678 /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
3679 ///
3680 /// let updated_category = client.add_asset_to_category(category_id, asset_uuid).await?;
3681 /// println!("Added asset {} to category '{}'", asset_uuid, updated_category.name);
3682 /// println!("Category now has {} assets", updated_category.assets.len());
3683 /// # Ok(())
3684 /// # }
3685 /// ```
3686 ///
3687 /// # Related Methods
3688 /// - [`remove_asset_from_category`](Self::remove_asset_from_category) - Remove asset from category
3689 /// - [`get_category`](Self::get_category) - Get category information including assets
3690 /// - [`get_asset`](Self::get_asset) - Get asset information
3691 pub async fn add_asset_to_category(
3692 &self,
3693 category_id: i64,
3694 asset_uuid: &str,
3695 ) -> Result<CategoryResponse, Error> {
3696 self.request_json(
3697 Method::PUT,
3698 &[
3699 "categories",
3700 &category_id.to_string(),
3701 "assets",
3702 asset_uuid,
3703 "add",
3704 ],
3705 None::<&()>,
3706 )
3707 .await
3708 }
3709
3710 /// Removes an asset from a category.
3711 ///
3712 /// This method disassociates an asset from a category. The asset remains in the system
3713 /// but is no longer part of the specified category. This does not affect the asset's
3714 /// association with other categories.
3715 ///
3716 /// # Arguments
3717 /// * `category_id` - The ID of the category to remove the asset from
3718 /// * `asset_uuid` - The UUID of the asset to remove from the category
3719 ///
3720 /// # Returns
3721 /// Returns a `CategoryResponse` containing the updated category information without
3722 /// the removed asset.
3723 ///
3724 /// # Errors
3725 /// Returns an error if:
3726 /// - Authentication fails or insufficient permissions
3727 /// - The category ID is invalid or does not exist
3728 /// - The asset UUID is invalid or does not exist
3729 /// - The asset is not currently associated with the category
3730 /// - The HTTP request fails
3731 /// - The server returns an error status
3732 /// - The response cannot be parsed
3733 ///
3734 /// # Examples
3735 /// ```no_run
3736 /// # use amp_rs::ApiClient;
3737 /// # #[tokio::main]
3738 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3739 /// let client = ApiClient::new().await?;
3740 ///
3741 /// let category_id = 1;
3742 /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
3743 ///
3744 /// let updated_category = client.remove_asset_from_category(category_id, asset_uuid).await?;
3745 /// println!("Removed asset {} from category '{}'", asset_uuid, updated_category.name);
3746 /// println!("Category now has {} assets", updated_category.assets.len());
3747 /// # Ok(())
3748 /// # }
3749 /// ```
3750 ///
3751 /// # Related Methods
3752 /// - [`add_asset_to_category`](Self::add_asset_to_category) - Add asset to category
3753 /// - [`get_category`](Self::get_category) - Get category information including assets
3754 /// - [`get_asset`](Self::get_asset) - Get asset information
3755 pub async fn remove_asset_from_category(
3756 &self,
3757 category_id: i64,
3758 asset_uuid: &str,
3759 ) -> Result<CategoryResponse, Error> {
3760 self.request_json(
3761 Method::PUT,
3762 &[
3763 "categories",
3764 &category_id.to_string(),
3765 "assets",
3766 asset_uuid,
3767 "remove",
3768 ],
3769 None::<&()>,
3770 )
3771 .await
3772 }
3773
3774 /// Validates a GAID (Green Address ID).
3775 ///
3776 /// # Arguments
3777 /// * `gaid` - The GAID string to validate
3778 ///
3779 /// # Returns
3780 /// Returns a `ValidateGaidResponse` indicating whether the GAID is valid
3781 ///
3782 /// # Errors
3783 /// Returns an error if:
3784 /// - Authentication fails
3785 /// - The HTTP request fails
3786 /// - The response cannot be parsed
3787 ///
3788 /// # Examples
3789 /// ```no_run
3790 /// # use amp_rs::ApiClient;
3791 /// # #[tokio::main]
3792 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3793 /// let client = ApiClient::new().await?;
3794 ///
3795 /// let gaid = "GAbYScu6jkWUND2jo3L4KJxyvo55d";
3796 /// let validation = client.validate_gaid(gaid).await?;
3797 ///
3798 /// if validation.is_valid {
3799 /// println!("GAID {} is valid", gaid);
3800 /// } else {
3801 /// println!("GAID {} is invalid: {:?}", gaid, validation.error);
3802 /// }
3803 /// # Ok(())
3804 /// # }
3805 /// ```
3806 pub async fn validate_gaid(
3807 &self,
3808 gaid: &str,
3809 ) -> Result<crate::model::ValidateGaidResponse, Error> {
3810 self.request_json(Method::GET, &["gaids", gaid, "validate"], None::<&()>)
3811 .await
3812 }
3813
3814 /// Gets the address associated with a GAID.
3815 ///
3816 /// # Arguments
3817 /// * `gaid` - The GAID to get the address for
3818 ///
3819 /// # Returns
3820 /// Returns an `AddressGaidResponse` containing the address
3821 ///
3822 /// # Errors
3823 /// Returns an error if:
3824 /// - The GAID is invalid
3825 /// - Authentication fails
3826 /// - The HTTP request fails
3827 /// - The response cannot be parsed
3828 ///
3829 /// # Examples
3830 /// ```no_run
3831 /// # use amp_rs::ApiClient;
3832 /// # #[tokio::main]
3833 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3834 /// let client = ApiClient::new().await?;
3835 ///
3836 /// let gaid = "GAbYScu6jkWUND2jo3L4KJxyvo55d";
3837 /// let address_response = client.get_gaid_address(gaid).await?;
3838 ///
3839 /// println!("Address for GAID {}: {}", gaid, address_response.address);
3840 /// # Ok(())
3841 /// # }
3842 /// ```
3843 pub async fn get_gaid_address(
3844 &self,
3845 gaid: &str,
3846 ) -> Result<crate::model::AddressGaidResponse, Error> {
3847 self.request_json(Method::GET, &["gaids", gaid, "address"], None::<&()>)
3848 .await
3849 }
3850
3851 /// Gets a list of all managers.
3852 ///
3853 /// # Returns
3854 /// Returns a vector of `Manager` objects
3855 ///
3856 /// # Errors
3857 /// Returns an error if:
3858 /// - Authentication fails
3859 /// - The HTTP request fails
3860 /// - The response cannot be parsed
3861 ///
3862 /// # Examples
3863 /// ```no_run
3864 /// # use amp_rs::ApiClient;
3865 /// # #[tokio::main]
3866 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3867 /// let client = ApiClient::new().await?;
3868 ///
3869 /// let managers = client.get_managers().await?;
3870 /// for manager in managers {
3871 /// println!("Manager: {} (ID: {})", manager.username, manager.id);
3872 /// }
3873 /// # Ok(())
3874 /// # }
3875 /// ```
3876 pub async fn get_managers(&self) -> Result<Vec<crate::model::Manager>, Error> {
3877 self.request_json(Method::GET, &["managers"], None::<&()>)
3878 .await
3879 }
3880
3881 /// Creates a new manager.
3882 ///
3883 /// # Arguments
3884 /// * `new_manager` - The manager creation request containing username and password
3885 ///
3886 /// # Returns
3887 /// Returns the created `Manager` object
3888 ///
3889 /// # Errors
3890 /// Returns an error if:
3891 /// - Authentication fails
3892 /// - The HTTP request fails
3893 /// - The manager creation request is invalid
3894 /// - The response cannot be parsed
3895 ///
3896 /// # Examples
3897 /// ```no_run
3898 /// # use amp_rs::{ApiClient, model::ManagerCreate};
3899 /// # #[tokio::main]
3900 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3901 /// let client = ApiClient::new().await?;
3902 ///
3903 /// let new_manager = ManagerCreate {
3904 /// username: "new_manager".to_string(),
3905 /// password: "secure_password".to_string(),
3906 /// };
3907 ///
3908 /// let manager = client.create_manager(&new_manager).await?;
3909 /// println!("Created manager: {} (ID: {})", manager.username, manager.id);
3910 /// # Ok(())
3911 /// # }
3912 /// ```
3913 pub async fn create_manager(
3914 &self,
3915 new_manager: &crate::model::ManagerCreate,
3916 ) -> Result<crate::model::Manager, Error> {
3917 self.request_json(Method::POST, &["managers", "create"], Some(new_manager))
3918 .await
3919 }
3920
3921 /// Gets all assignments for a specific asset.
3922 ///
3923 /// # Arguments
3924 /// * `asset_uuid` - The UUID of the asset to get assignments for
3925 ///
3926 /// # Returns
3927 /// Returns a vector of `Assignment` objects
3928 ///
3929 /// # Errors
3930 /// Returns an error if:
3931 /// - Authentication fails
3932 /// - The HTTP request fails
3933 /// - The asset UUID is invalid
3934 /// - The response cannot be parsed
3935 ///
3936 /// # Examples
3937 /// ```no_run
3938 /// # use amp_rs::ApiClient;
3939 /// # #[tokio::main]
3940 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3941 /// let client = ApiClient::new().await?;
3942 ///
3943 /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
3944 /// let assignments = client.get_asset_assignments(asset_uuid).await?;
3945 ///
3946 /// for assignment in assignments {
3947 /// println!("Assignment ID: {}, Amount: {}", assignment.id, assignment.amount);
3948 /// }
3949 /// # Ok(())
3950 /// # }
3951 /// ```
3952 pub async fn get_asset_assignments(&self, asset_uuid: &str) -> Result<Vec<Assignment>, Error> {
3953 self.request_json(
3954 Method::GET,
3955 &["assets", asset_uuid, "assignments"],
3956 None::<&()>,
3957 )
3958 .await
3959 }
3960
3961 /// Creates multiple asset assignments in batch.
3962 ///
3963 /// This method creates multiple asset assignments for the specified asset. Each assignment
3964 /// allocates a specific amount of the asset to a registered user. The assignments are
3965 /// created individually due to API limitations, but this method handles the batch processing
3966 /// automatically.
3967 ///
3968 /// # Arguments
3969 /// * `asset_uuid` - The UUID of the asset to create assignments for
3970 /// * `requests` - A slice of `CreateAssetAssignmentRequest` structs containing assignment details
3971 ///
3972 /// # Returns
3973 /// Returns a vector of `Assignment` structs representing the created assignments with their
3974 /// assigned IDs and status information.
3975 ///
3976 /// # Errors
3977 /// Returns an error if:
3978 /// - Authentication fails or insufficient permissions
3979 /// - The asset UUID is invalid or does not exist
3980 /// - Any assignment request contains invalid data (e.g., invalid user ID, negative amount)
3981 /// - Insufficient asset balance for the total requested assignments
3982 /// - Any individual assignment creation fails
3983 /// - The HTTP request fails
3984 /// - The server returns an error status
3985 /// - The response cannot be parsed
3986 ///
3987 /// # Examples
3988 /// ```no_run
3989 /// # use amp_rs::{ApiClient, model::CreateAssetAssignmentRequest};
3990 /// # #[tokio::main]
3991 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3992 /// let client = ApiClient::new().await?;
3993 ///
3994 /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
3995 /// let requests = vec![
3996 /// CreateAssetAssignmentRequest {
3997 /// registered_user: 123,
3998 /// amount: 1000,
3999 /// vesting_timestamp: None,
4000 /// ready_for_distribution: false,
4001 /// },
4002 /// CreateAssetAssignmentRequest {
4003 /// registered_user: 456,
4004 /// amount: 500,
4005 /// vesting_timestamp: None,
4006 /// ready_for_distribution: true,
4007 /// },
4008 /// ];
4009 ///
4010 /// let assignments = client.create_asset_assignments(asset_uuid, &requests).await?;
4011 /// println!("Created {} assignments", assignments.len());
4012 /// for assignment in assignments {
4013 /// println!("Assignment {}: {} units to user {}",
4014 /// assignment.id, assignment.amount, assignment.registered_user);
4015 /// }
4016 /// # Ok(())
4017 /// # }
4018 /// ```
4019 ///
4020 /// # Related Methods
4021 /// - [`get_asset_assignments`](Self::get_asset_assignments) - List all assignments for an asset
4022 /// - [`delete_asset_assignment`](Self::delete_asset_assignment) - Remove an assignment
4023 /// - [`edit_asset_assignment`](Self::edit_asset_assignment) - Update assignment details
4024 /// - [`set_assignment_ready_for_distribution`](Self::set_assignment_ready_for_distribution) - Mark for distribution
4025 pub async fn create_asset_assignments(
4026 &self,
4027 asset_uuid: &str,
4028 requests: &[CreateAssetAssignmentRequest],
4029 ) -> Result<Vec<Assignment>, Error> {
4030 use crate::model::CreateAssetAssignmentRequestWrapper;
4031
4032 // The API only supports maximum length 1 per request, so we need to break
4033 // multiple assignments into separate CreateAssetAssignmentRequestWrapper instances
4034 let mut all_assignments = Vec::new();
4035
4036 for request in requests {
4037 let wrapper = CreateAssetAssignmentRequestWrapper {
4038 assignments: vec![request.clone()],
4039 };
4040
4041 let assignments: Vec<Assignment> = self
4042 .request_json(
4043 Method::POST,
4044 &["assets", asset_uuid, "assignments", "create"],
4045 Some(&wrapper),
4046 )
4047 .await?;
4048
4049 all_assignments.extend(assignments);
4050 }
4051
4052 Ok(all_assignments)
4053 }
4054
4055 /// Gets a specific asset assignment by asset UUID and assignment ID.
4056 ///
4057 /// This method sends a GET request to retrieve detailed information about a specific asset
4058 /// assignment. Asset assignments represent the allocation of assets to users or entities,
4059 /// including information such as the assigned amount, recipient details, and assignment status.
4060 ///
4061 /// # Arguments
4062 /// * `asset_uuid` - The UUID of the asset for which to retrieve the assignment
4063 /// * `assignment_id` - The ID of the specific assignment to retrieve
4064 ///
4065 /// # Returns
4066 /// Returns an `Assignment` struct containing the assignment details including:
4067 /// - Assignment ID and amount
4068 /// - Recipient information
4069 /// - Assignment status and metadata
4070 /// - Creation and modification timestamps
4071 ///
4072 /// # Errors
4073 /// Returns an error if:
4074 /// - Authentication fails
4075 /// - The HTTP request fails
4076 /// - The server returns an error status
4077 /// - The asset UUID is invalid or does not exist
4078 /// - The assignment ID is invalid or does not exist
4079 /// - The assignment is not accessible to the current user
4080 /// - The response cannot be parsed as a valid Assignment
4081 ///
4082 /// # Example
4083 /// ```no_run
4084 /// # use amp_rs::ApiClient;
4085 /// # #[tokio::main]
4086 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4087 /// let client = ApiClient::new().await?;
4088 ///
4089 /// // Retrieve assignment with ID "123" for asset "550e8400-e29b-41d4-a716-446655440000"
4090 /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
4091 /// let assignment_id = "123";
4092 ///
4093 /// let assignment = client.get_asset_assignment(asset_uuid, assignment_id).await?;
4094 ///
4095 /// println!("Assignment ID: {}", assignment.id);
4096 /// println!("Assigned amount: {}", assignment.amount);
4097 /// println!("Registered user: {}", assignment.registered_user);
4098 /// # Ok(())
4099 /// # }
4100 /// ```
4101 pub async fn get_asset_assignment(
4102 &self,
4103 asset_uuid: &str,
4104 assignment_id: &str,
4105 ) -> Result<Assignment, Error> {
4106 self.request_json(
4107 Method::GET,
4108 &["assets", asset_uuid, "assignments", assignment_id],
4109 None::<&()>,
4110 )
4111 .await
4112 }
4113
4114 /// Gets a specific manager by ID.
4115 ///
4116 /// # Arguments
4117 /// * `manager_id` - The ID of the manager to retrieve
4118 ///
4119 /// # Errors
4120 /// Returns an error if:
4121 /// - Authentication fails
4122 /// - The HTTP request fails
4123 /// - The server returns an error status
4124 /// - The response cannot be parsed as JSON
4125 pub async fn get_manager(&self, manager_id: i64) -> Result<crate::model::Manager, Error> {
4126 self.request_json(
4127 Method::GET,
4128 &["managers", &manager_id.to_string()],
4129 None::<&()>,
4130 )
4131 .await
4132 }
4133
4134 /// Removes a manager's permissions to modify a specific asset.
4135 ///
4136 /// This method revokes a manager's access to a specific asset, preventing them from
4137 /// performing asset management operations such as creating assignments, managing ownership,
4138 /// or modifying asset properties. The manager will no longer be able to access this asset
4139 /// through their management interface.
4140 ///
4141 /// # Arguments
4142 /// * `manager_id` - The ID of the manager to remove permissions from
4143 /// * `asset_uuid` - The UUID of the asset to remove permissions for
4144 ///
4145 /// # Returns
4146 /// Returns `Ok(())` on successful permission removal.
4147 ///
4148 /// # Errors
4149 /// Returns an error if:
4150 /// - Authentication fails or insufficient permissions
4151 /// - The manager ID is invalid or does not exist
4152 /// - The asset UUID is invalid or does not exist
4153 /// - The manager does not currently have permissions for this asset
4154 /// - The HTTP request fails
4155 /// - The server returns an error status
4156 ///
4157 /// # Examples
4158 /// ```no_run
4159 /// # use amp_rs::ApiClient;
4160 /// # #[tokio::main]
4161 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4162 /// let client = ApiClient::new().await?;
4163 ///
4164 /// let manager_id = 123;
4165 /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
4166 ///
4167 /// client.manager_remove_asset(manager_id, asset_uuid).await?;
4168 /// println!("Removed asset {} from manager {}", asset_uuid, manager_id);
4169 /// # Ok(())
4170 /// # }
4171 /// ```
4172 ///
4173 /// # Related Methods
4174 /// - [`add_asset_to_manager`](Self::add_asset_to_manager) - Grant manager permissions for an asset
4175 /// - [`get_manager`](Self::get_manager) - Get manager information including current assets
4176 /// - [`revoke_manager`](Self::revoke_manager) - Remove all asset permissions from manager
4177 /// - [`lock_manager`](Self::lock_manager) - Lock manager account
4178 pub async fn manager_remove_asset(
4179 &self,
4180 manager_id: i64,
4181 asset_uuid: &str,
4182 ) -> Result<(), Error> {
4183 self.request_empty(
4184 Method::POST,
4185 &[
4186 "managers",
4187 &manager_id.to_string(),
4188 "assets",
4189 asset_uuid,
4190 "remove",
4191 ],
4192 None::<&()>,
4193 )
4194 .await
4195 }
4196
4197 /// Revokes all asset permissions for a manager.
4198 ///
4199 /// This method first retrieves the manager's current asset permissions,
4200 /// then removes the manager's access to each asset they currently have access to.
4201 ///
4202 /// # Arguments
4203 /// * `manager_id` - The ID of the manager to revoke permissions for
4204 ///
4205 /// # Errors
4206 /// Returns an error if:
4207 /// - Authentication fails
4208 /// - The HTTP request fails
4209 /// - The server returns an error status
4210 /// - Any individual asset removal fails
4211 pub async fn revoke_manager(&self, manager_id: i64) -> Result<(), Error> {
4212 // First, get the manager to see which assets they have access to
4213 let manager = self.get_manager(manager_id).await?;
4214
4215 // Remove the manager's access to each asset
4216 for asset_uuid in &manager.assets {
4217 self.manager_remove_asset(manager_id, asset_uuid).await?;
4218 }
4219
4220 Ok(())
4221 }
4222
4223 /// Gets the current manager information as raw JSON.
4224 ///
4225 /// This method calls the `/managers/me` endpoint to retrieve information
4226 /// about the currently authenticated manager.
4227 ///
4228 /// # Errors
4229 /// Returns an error if:
4230 /// - Authentication fails
4231 /// - The HTTP request fails
4232 /// - The server returns an error status
4233 /// - The response cannot be parsed as JSON
4234 pub async fn get_current_manager_raw(&self) -> Result<serde_json::Value, Error> {
4235 self.request_json(Method::GET, &["managers", "me"], None::<&()>)
4236 .await
4237 }
4238
4239 /// Locks a manager account to prevent further operations.
4240 ///
4241 /// This method sends a PUT request to lock the specified manager, preventing any further
4242 /// operations on that manager account. This is typically used for security purposes or
4243 /// when a manager needs to be temporarily disabled.
4244 ///
4245 /// # Arguments
4246 /// * `manager_id` - The ID of the manager to lock
4247 ///
4248 /// # Returns
4249 /// Returns `Ok(())` if the manager was successfully locked.
4250 ///
4251 /// # Errors
4252 /// Returns an error if:
4253 /// - Authentication fails
4254 /// - The HTTP request fails
4255 /// - The server returns an error status
4256 /// - The manager ID is invalid or does not exist
4257 /// - The manager is already locked
4258 ///
4259 /// # Example
4260 /// ```no_run
4261 /// # use amp_rs::ApiClient;
4262 /// # #[tokio::main]
4263 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4264 /// let client = ApiClient::new().await?;
4265 ///
4266 /// // Lock manager with ID 123
4267 /// client.lock_manager(123).await?;
4268 /// println!("Manager 123 has been locked successfully");
4269 /// # Ok(())
4270 /// # }
4271 /// ```
4272 pub async fn lock_manager(&self, manager_id: i64) -> Result<(), Error> {
4273 self.request_empty(
4274 Method::PUT,
4275 &["managers", &manager_id.to_string(), "lock"],
4276 None::<&()>,
4277 )
4278 .await
4279 }
4280
4281 /// Unlocks a manager account.
4282 ///
4283 /// # Arguments
4284 /// * `manager_id` - The ID of the manager to unlock
4285 ///
4286 /// # Errors
4287 /// Returns an error if:
4288 /// - Authentication fails
4289 /// - The HTTP request fails
4290 /// - The server returns an error status
4291 pub async fn unlock_manager(&self, manager_id: i64) -> Result<(), Error> {
4292 self.request_empty(
4293 Method::PUT,
4294 &["managers", &manager_id.to_string(), "unlock"],
4295 None::<&()>,
4296 )
4297 .await
4298 }
4299
4300 /// Authorizes a manager to manage a specific asset.
4301 ///
4302 /// This method sends a PUT request to authorize the specified manager to manage the given asset.
4303 /// Once authorized, the manager will have permissions to perform operations on the asset such as
4304 /// creating assignments, managing ownership, and other asset-related operations.
4305 ///
4306 /// # Arguments
4307 /// * `manager_id` - The ID of the manager to authorize
4308 /// * `asset_uuid` - The UUID of the asset to add to the manager's authorized assets
4309 ///
4310 /// # Returns
4311 /// Returns `Ok(())` if the manager was successfully authorized for the asset.
4312 ///
4313 /// # Errors
4314 /// Returns an error if:
4315 /// - Authentication fails or insufficient permissions
4316 /// - The HTTP request fails
4317 /// - The server returns an error status
4318 /// - The manager ID is invalid or does not exist
4319 /// - The asset UUID is invalid or does not exist
4320 /// - The manager is already authorized for this asset
4321 /// - The manager is locked and cannot be modified
4322 ///
4323 /// # Examples
4324 /// ```no_run
4325 /// # use amp_rs::ApiClient;
4326 /// # #[tokio::main]
4327 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4328 /// let client = ApiClient::new().await?;
4329 ///
4330 /// // Authorize manager 123 to manage asset with UUID "550e8400-e29b-41d4-a716-446655440000"
4331 /// let manager_id = 123;
4332 /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
4333 ///
4334 /// client.add_asset_to_manager(manager_id, asset_uuid).await?;
4335 /// println!("Manager {} is now authorized to manage asset {}", manager_id, asset_uuid);
4336 /// # Ok(())
4337 /// # }
4338 /// ```
4339 ///
4340 /// # Related Methods
4341 /// - [`manager_remove_asset`](Self::manager_remove_asset) - Remove manager permissions for an asset
4342 /// - [`get_manager`](Self::get_manager) - Get manager information including current assets
4343 /// - [`get_manager_permissions`](Self::get_manager_permissions) - Get manager's current permissions
4344 /// - [`lock_manager`](Self::lock_manager) - Lock manager account
4345 pub async fn add_asset_to_manager(
4346 &self,
4347 manager_id: i64,
4348 asset_uuid: &str,
4349 ) -> Result<(), Error> {
4350 self.request_empty(
4351 Method::PUT,
4352 &[
4353 "managers",
4354 &manager_id.to_string(),
4355 "assets",
4356 asset_uuid,
4357 "add",
4358 ],
4359 None::<&()>,
4360 )
4361 .await
4362 }
4363
4364 /// Deletes a specific asset assignment.
4365 ///
4366 /// # Arguments
4367 /// * `asset_uuid` - The UUID of the asset
4368 /// * `assignment_id` - The ID of the assignment to delete
4369 ///
4370 /// # Errors
4371 /// Returns an error if:
4372 /// - Authentication fails
4373 /// - The HTTP request fails
4374 /// - The server returns an error status
4375 /// Removes an asset assignment.
4376 ///
4377 /// This method permanently deletes an asset assignment, returning the allocated assets
4378 /// back to the available pool. This operation cannot be undone. If the assignment has
4379 /// already been distributed, this operation may fail.
4380 ///
4381 /// # Arguments
4382 /// * `asset_uuid` - The UUID of the asset containing the assignment
4383 /// * `assignment_id` - The ID of the assignment to delete
4384 ///
4385 /// # Returns
4386 /// Returns `Ok(())` on successful deletion.
4387 ///
4388 /// # Errors
4389 /// Returns an error if:
4390 /// - Authentication fails or insufficient permissions
4391 /// - The asset UUID is invalid or does not exist
4392 /// - The assignment ID is invalid or does not exist
4393 /// - The assignment has already been distributed and cannot be deleted
4394 /// - The assignment is locked and cannot be modified
4395 /// - The HTTP request fails
4396 /// - The server returns an error status
4397 ///
4398 /// # Examples
4399 /// ```no_run
4400 /// # use amp_rs::ApiClient;
4401 /// # #[tokio::main]
4402 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4403 /// let client = ApiClient::new().await?;
4404 ///
4405 /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
4406 /// let assignment_id = "123";
4407 ///
4408 /// client.delete_asset_assignment(asset_uuid, assignment_id).await?;
4409 /// println!("Successfully deleted assignment {}", assignment_id);
4410 /// # Ok(())
4411 /// # }
4412 /// ```
4413 ///
4414 /// # Related Methods
4415 /// - [`get_asset_assignment`](Self::get_asset_assignment) - Get assignment details before deletion
4416 /// - [`create_asset_assignments`](Self::create_asset_assignments) - Create new assignments
4417 /// - [`edit_asset_assignment`](Self::edit_asset_assignment) - Update assignment instead of deleting
4418 /// - [`lock_asset_assignment`](Self::lock_asset_assignment) - Lock assignment to prevent changes
4419 pub async fn delete_asset_assignment(
4420 &self,
4421 asset_uuid: &str,
4422 assignment_id: &str,
4423 ) -> Result<(), Error> {
4424 self.request_empty(
4425 Method::DELETE,
4426 &["assets", asset_uuid, "assignments", assignment_id, "delete"],
4427 None::<&()>,
4428 )
4429 .await
4430 }
4431
4432 /// Locks a specific asset assignment.
4433 ///
4434 /// # Arguments
4435 /// * `asset_uuid` - The UUID of the asset
4436 /// * `assignment_id` - The ID of the assignment to lock
4437 ///
4438 /// # Errors
4439 /// Returns an error if:
4440 /// - Authentication fails
4441 /// - The HTTP request fails
4442 /// - The server returns an error status
4443 pub async fn lock_asset_assignment(
4444 &self,
4445 asset_uuid: &str,
4446 assignment_id: &str,
4447 ) -> Result<Assignment, Error> {
4448 self.request_json(
4449 Method::PUT,
4450 &["assets", asset_uuid, "assignments", assignment_id, "lock"],
4451 None::<&()>,
4452 )
4453 .await
4454 }
4455
4456 /// Unlocks a specific asset assignment.
4457 ///
4458 /// # Arguments
4459 /// * `asset_uuid` - The UUID of the asset
4460 /// * `assignment_id` - The ID of the assignment to unlock
4461 ///
4462 /// # Errors
4463 /// Returns an error if:
4464 /// - Authentication fails
4465 /// - The HTTP request fails
4466 /// - The server returns an error status
4467 pub async fn unlock_asset_assignment(
4468 &self,
4469 asset_uuid: &str,
4470 assignment_id: &str,
4471 ) -> Result<Assignment, Error> {
4472 self.request_json(
4473 Method::PUT,
4474 &["assets", asset_uuid, "assignments", assignment_id, "unlock"],
4475 None::<&()>,
4476 )
4477 .await
4478 }
4479
4480 /// Adds categories to a registered user.
4481 ///
4482 /// # Arguments
4483 /// * `registered_user_id` - The ID of the registered user
4484 /// * `categories` - A slice of category IDs to add to the user
4485 ///
4486 /// # Errors
4487 /// Returns an error if:
4488 /// - Authentication fails
4489 /// - The HTTP request fails
4490 /// - The server returns an error status
4491 /// - The registered user ID is invalid
4492 /// - Any category ID is invalid
4493 pub async fn add_categories_to_registered_user(
4494 &self,
4495 registered_user_id: i64,
4496 categories: &[i64],
4497 ) -> Result<(), Error> {
4498 let request_body = CategoriesRequest {
4499 categories: categories.to_vec(),
4500 };
4501
4502 self.request_empty(
4503 Method::PUT,
4504 &[
4505 "registered_users",
4506 ®istered_user_id.to_string(),
4507 "categories",
4508 "add",
4509 ],
4510 Some(request_body),
4511 )
4512 .await
4513 }
4514
4515 /// Removes categories from a registered user
4516 ///
4517 /// # Arguments
4518 /// * `registered_user_id` - The ID of the registered user
4519 /// * `categories` - A slice of category IDs to remove from the user
4520 ///
4521 /// # Returns
4522 /// Returns `Ok(())` if the categories are successfully removed, or an error if:
4523 /// - Authentication fails
4524 /// - The HTTP request fails
4525 /// - The server returns an error status
4526 /// - The registered user ID is invalid
4527 /// - Any category ID is not associated with the user
4528 pub async fn remove_categories_from_registered_user(
4529 &self,
4530 registered_user_id: i64,
4531 categories: &[i64],
4532 ) -> Result<(), Error> {
4533 let request_body = CategoriesRequest {
4534 categories: categories.to_vec(),
4535 };
4536
4537 self.request_empty(
4538 Method::PUT,
4539 &[
4540 "registered_users",
4541 ®istered_user_id.to_string(),
4542 "categories",
4543 "delete",
4544 ],
4545 Some(request_body),
4546 )
4547 .await
4548 }
4549}
4550
4551fn get_amp_api_base_url() -> Result<Url, Error> {
4552 let url_str = env::var("AMP_API_BASE_URL")
4553 .unwrap_or_else(|_| "https://amp-test.blockstream.com/api".to_string());
4554 Url::parse(&url_str).map_err(Error::from)
4555}
4556
4557/// Creates a token strategy based on automatic environment detection
4558///
4559/// This function detects the current environment and creates the appropriate strategy:
4560/// - Mock strategy for mock environments (isolated, no persistence)
4561/// - Live strategy for live environments (full token management)
4562///
4563/// # Arguments
4564/// * `mock_token` - Optional token to use for mock environments
4565///
4566/// # Errors
4567/// Returns an error if strategy creation fails
4568pub async fn create_auto_token_strategy(
4569 mock_token: Option<String>,
4570) -> Result<Box<dyn TokenStrategy>, Error> {
4571 TokenEnvironment::create_auto_strategy(mock_token).await
4572}
4573
4574/// Creates a mock token strategy with the specified token
4575///
4576/// # Arguments
4577/// * `token` - The mock token to use
4578#[must_use]
4579pub fn create_mock_token_strategy(token: String) -> Box<dyn TokenStrategy> {
4580 Box::new(MockTokenStrategy::new(token))
4581}
4582
4583/// Creates a live token strategy with default configuration
4584///
4585/// # Errors
4586/// Returns an error if the `TokenManager` cannot be initialized
4587pub async fn create_live_token_strategy() -> Result<Box<dyn TokenStrategy>, Error> {
4588 let strategy = LiveTokenStrategy::new().await?;
4589 Ok(Box::new(strategy))
4590}
4591
4592/// Creates a token strategy for the specified environment
4593///
4594/// # Arguments
4595/// * `environment` - The target environment
4596/// * `mock_token` - Optional token to use for mock environments
4597///
4598/// # Errors
4599/// Returns an error if strategy creation fails
4600pub async fn create_token_strategy_for_environment(
4601 environment: TokenEnvironment,
4602 mock_token: Option<String>,
4603) -> Result<Box<dyn TokenStrategy>, Error> {
4604 environment.create_strategy(mock_token).await
4605}
4606
4607#[cfg(test)]
4608mod tests {
4609 use super::*;
4610 use tokio;
4611
4612 #[tokio::test]
4613 async fn test_mock_token_strategy_basic_functionality() {
4614 let mock_token = "mock_token_12_345".to_string();
4615 let strategy = MockTokenStrategy::new(mock_token.clone());
4616
4617 // Test get_token returns the mock token
4618 let result = strategy.get_token().await;
4619 assert!(result.is_ok());
4620 assert_eq!(result.unwrap(), mock_token);
4621
4622 // Test strategy type identification
4623 assert_eq!(strategy.strategy_type(), "mock");
4624
4625 // Test persistence is disabled
4626 assert!(!strategy.should_persist());
4627
4628 // Test clear_token is a no-op (should not fail)
4629 let clear_result = strategy.clear_token().await;
4630 assert!(clear_result.is_ok());
4631
4632 // Verify token is still available after clear (since it's a no-op for mock)
4633 let token_after_clear = strategy.get_token().await;
4634 assert!(token_after_clear.is_ok());
4635 assert_eq!(token_after_clear.unwrap(), mock_token);
4636 }
4637
4638 #[tokio::test]
4639 async fn test_mock_token_strategy_isolation() {
4640 let token1 = "token_instance_1".to_string();
4641 let token2 = "token_instance_2".to_string();
4642
4643 let strategy1 = MockTokenStrategy::new(token1.clone());
4644 let strategy2 = MockTokenStrategy::new(token2.clone());
4645
4646 // Test that different instances are isolated
4647 let result1 = strategy1.get_token().await.unwrap();
4648 let result2 = strategy2.get_token().await.unwrap();
4649
4650 assert_eq!(result1, token1);
4651 assert_eq!(result2, token2);
4652 assert_ne!(result1, result2);
4653
4654 // Test that operations on one don't affect the other
4655 let _ = strategy1.clear_token().await;
4656 let result2_after_clear = strategy2.get_token().await.unwrap();
4657 assert_eq!(result2_after_clear, token2);
4658 }
4659
4660 #[tokio::test]
4661 async fn test_live_token_strategy_creation() {
4662 // Test creating a live strategy with global instance
4663 let strategy_result = LiveTokenStrategy::new().await;
4664 assert!(strategy_result.is_ok());
4665
4666 let strategy = strategy_result.unwrap();
4667 assert_eq!(strategy.strategy_type(), "live");
4668 assert!(strategy.should_persist());
4669 }
4670
4671 #[tokio::test]
4672 async fn test_live_token_strategy_with_custom_manager() {
4673 // Create a custom token manager for testing
4674 let config = RetryConfig::for_tests();
4675 let base_url = Url::parse("http://localhost:8080").unwrap();
4676 let mock_token = "test_live_token".to_string();
4677
4678 let token_manager =
4679 Arc::new(TokenManager::with_mock_token(config, base_url, mock_token.clone()).unwrap());
4680
4681 let strategy = LiveTokenStrategy::with_token_manager(token_manager);
4682
4683 // Test strategy properties
4684 assert_eq!(strategy.strategy_type(), "live");
4685 assert!(strategy.should_persist());
4686
4687 // Test token retrieval
4688 let token_result = strategy.get_token().await;
4689 assert!(token_result.is_ok());
4690 assert_eq!(token_result.unwrap(), mock_token);
4691 }
4692
4693 #[tokio::test]
4694 async fn test_live_token_strategy_clear_token() {
4695 // Create a live strategy with a mock token manager
4696 let config = RetryConfig::for_tests();
4697 let base_url = Url::parse("http://localhost:8080").unwrap();
4698 let mock_token = "test_clear_token".to_string();
4699
4700 let token_manager =
4701 Arc::new(TokenManager::with_mock_token(config, base_url, mock_token.clone()).unwrap());
4702
4703 let strategy = LiveTokenStrategy::with_token_manager(token_manager);
4704
4705 // Verify token is available initially
4706 let initial_token = strategy.get_token().await;
4707 assert!(initial_token.is_ok());
4708 assert_eq!(initial_token.unwrap(), mock_token);
4709
4710 // Clear the token
4711 let clear_result = strategy.clear_token().await;
4712 assert!(clear_result.is_ok());
4713
4714 // Note: After clearing, the TokenManager would try to obtain a new token
4715 // In a real scenario, this would fail without proper credentials
4716 // But our mock token manager will still return the same token
4717 }
4718
4719 #[tokio::test]
4720 async fn test_strategy_type_identification() {
4721 let mock_strategy = MockTokenStrategy::new("test_token".to_string());
4722 let live_strategy = LiveTokenStrategy::new().await.unwrap();
4723
4724 // Test that we can identify strategy types for debugging
4725 assert_eq!(mock_strategy.strategy_type(), "mock");
4726 assert_eq!(live_strategy.strategy_type(), "live");
4727
4728 // Test persistence settings
4729 assert!(!mock_strategy.should_persist());
4730 assert!(live_strategy.should_persist());
4731 }
4732
4733 #[tokio::test]
4734 async fn test_strategy_debug_formatting() {
4735 let mock_strategy = MockTokenStrategy::new("debug_test_token".to_string());
4736 let debug_output = format!("{mock_strategy:?}");
4737
4738 // Verify debug output contains expected information
4739 assert!(debug_output.contains("MockTokenStrategy"));
4740 assert!(debug_output.contains("debug_test_token"));
4741 }
4742
4743 // Environment Detection Tests
4744
4745 #[test]
4746 fn test_token_environment_detect_live_via_amp_tests() {
4747 // Set up environment for live test detection
4748 env::set_var("AMP_TESTS", "live");
4749 env::set_var("AMP_USERNAME", "real_user");
4750 env::set_var("AMP_PASSWORD", "real_pass");
4751 env::remove_var("AMP_API_BASE_URL");
4752
4753 let environment = TokenEnvironment::detect();
4754 assert_eq!(environment, TokenEnvironment::Live);
4755
4756 // Clean up
4757 env::remove_var("AMP_TESTS");
4758 env::remove_var("AMP_USERNAME");
4759 env::remove_var("AMP_PASSWORD");
4760 }
4761
4762 #[test]
4763 fn test_token_environment_detect_mock_via_credentials() {
4764 // Set up environment for mock detection via username
4765 env::remove_var("AMP_TESTS");
4766 env::set_var("AMP_USERNAME", "mock_user");
4767 env::set_var("AMP_PASSWORD", "real_pass");
4768 env::remove_var("AMP_API_BASE_URL");
4769
4770 let environment = TokenEnvironment::detect();
4771 assert_eq!(environment, TokenEnvironment::Mock);
4772
4773 // Test mock detection via password
4774 env::set_var("AMP_USERNAME", "real_user");
4775 env::set_var("AMP_PASSWORD", "mock_pass");
4776
4777 let environment = TokenEnvironment::detect();
4778 assert_eq!(environment, TokenEnvironment::Mock);
4779
4780 // Clean up
4781 env::remove_var("AMP_USERNAME");
4782 env::remove_var("AMP_PASSWORD");
4783 }
4784
4785 #[test]
4786 fn test_token_environment_detect_mock_via_base_url() {
4787 // Set up environment for mock detection via localhost URL
4788 env::remove_var("AMP_TESTS");
4789 env::set_var("AMP_USERNAME", "real_user");
4790 env::set_var("AMP_PASSWORD", "real_pass");
4791 env::set_var("AMP_API_BASE_URL", "http://localhost:8080/api");
4792
4793 let environment = TokenEnvironment::detect();
4794 assert_eq!(environment, TokenEnvironment::Mock);
4795
4796 // Test with 127.0.0.1
4797 env::set_var("AMP_API_BASE_URL", "http://127.0.0.1:3000/api");
4798 let environment = TokenEnvironment::detect();
4799 assert_eq!(environment, TokenEnvironment::Mock);
4800
4801 // Test with mock in URL
4802 env::set_var("AMP_API_BASE_URL", "http://mock-server.example.com/api");
4803 let environment = TokenEnvironment::detect();
4804 assert_eq!(environment, TokenEnvironment::Mock);
4805
4806 // Clean up
4807 env::remove_var("AMP_USERNAME");
4808 env::remove_var("AMP_PASSWORD");
4809 env::remove_var("AMP_API_BASE_URL");
4810 }
4811
4812 #[test]
4813 fn test_token_environment_detect_live_via_real_credentials() {
4814 // Set up environment for live detection via real credentials
4815 env::remove_var("AMP_TESTS");
4816 env::set_var("AMP_USERNAME", "real_user");
4817 env::set_var("AMP_PASSWORD", "real_pass");
4818 env::set_var("AMP_API_BASE_URL", "https://amp-test.blockstream.com/api");
4819
4820 let environment = TokenEnvironment::detect();
4821 assert_eq!(environment, TokenEnvironment::Live);
4822
4823 // Clean up
4824 env::remove_var("AMP_USERNAME");
4825 env::remove_var("AMP_PASSWORD");
4826 env::remove_var("AMP_API_BASE_URL");
4827 }
4828
4829 #[test]
4830 fn test_token_environment_detect_mock_fallback() {
4831 // Set up environment with no credentials (fallback to mock)
4832 env::remove_var("AMP_TESTS");
4833 env::remove_var("AMP_USERNAME");
4834 env::remove_var("AMP_PASSWORD");
4835 env::remove_var("AMP_API_BASE_URL");
4836
4837 let environment = TokenEnvironment::detect();
4838 assert_eq!(environment, TokenEnvironment::Mock);
4839 }
4840
4841 #[test]
4842 fn test_has_mock_credentials() {
4843 // Test mock username detection
4844 assert!(TokenEnvironment::has_mock_credentials(
4845 "mock_user",
4846 "real_pass",
4847 ""
4848 ));
4849 assert!(TokenEnvironment::has_mock_credentials(
4850 "Mock_User",
4851 "real_pass",
4852 ""
4853 ));
4854 assert!(TokenEnvironment::has_mock_credentials(
4855 "user_mock",
4856 "real_pass",
4857 ""
4858 ));
4859
4860 // Test mock password detection
4861 assert!(TokenEnvironment::has_mock_credentials(
4862 "real_user",
4863 "mock_pass",
4864 ""
4865 ));
4866 assert!(TokenEnvironment::has_mock_credentials(
4867 "real_user",
4868 "Mock_Pass",
4869 ""
4870 ));
4871 assert!(TokenEnvironment::has_mock_credentials(
4872 "real_user",
4873 "pass_mock",
4874 ""
4875 ));
4876
4877 // Test mock URL detection
4878 assert!(TokenEnvironment::has_mock_credentials(
4879 "real_user",
4880 "real_pass",
4881 "http://localhost:8080"
4882 ));
4883 assert!(TokenEnvironment::has_mock_credentials(
4884 "real_user",
4885 "real_pass",
4886 "http://127.0.0.1:3000"
4887 ));
4888 assert!(TokenEnvironment::has_mock_credentials(
4889 "real_user",
4890 "real_pass",
4891 "http://mock-server.com"
4892 ));
4893 assert!(TokenEnvironment::has_mock_credentials(
4894 "real_user",
4895 "real_pass",
4896 "http://Mock-Server.com"
4897 ));
4898
4899 // Test non-mock credentials
4900 assert!(!TokenEnvironment::has_mock_credentials(
4901 "real_user",
4902 "real_pass",
4903 "https://amp-test.blockstream.com"
4904 ));
4905 assert!(!TokenEnvironment::has_mock_credentials("", "", ""));
4906 }
4907
4908 #[test]
4909 fn test_token_environment_should_persist_tokens() {
4910 assert!(!TokenEnvironment::Mock.should_persist_tokens());
4911 assert!(TokenEnvironment::Live.should_persist_tokens());
4912
4913 // Auto should delegate to detect()
4914 env::set_var("AMP_TESTS", "live");
4915 assert!(TokenEnvironment::Auto.should_persist_tokens());
4916
4917 env::set_var("AMP_USERNAME", "mock_user");
4918 env::set_var("AMP_PASSWORD", "some_password");
4919 env::remove_var("AMP_TESTS");
4920 env::remove_var("AMP_API_BASE_URL");
4921 assert!(!TokenEnvironment::Auto.should_persist_tokens());
4922
4923 // Clean up
4924 env::remove_var("AMP_USERNAME");
4925 env::remove_var("AMP_PASSWORD");
4926 }
4927
4928 #[test]
4929 fn test_token_environment_is_mock_and_is_live() {
4930 assert!(TokenEnvironment::Mock.is_mock());
4931 assert!(!TokenEnvironment::Mock.is_live());
4932
4933 assert!(!TokenEnvironment::Live.is_mock());
4934 assert!(TokenEnvironment::Live.is_live());
4935
4936 // Auto should delegate to detect()
4937 env::set_var("AMP_USERNAME", "mock_user");
4938 env::set_var("AMP_PASSWORD", "some_password");
4939 env::remove_var("AMP_TESTS");
4940 env::remove_var("AMP_API_BASE_URL");
4941 assert!(TokenEnvironment::Auto.is_mock());
4942 assert!(!TokenEnvironment::Auto.is_live());
4943
4944 env::set_var("AMP_TESTS", "live");
4945 assert!(!TokenEnvironment::Auto.is_mock());
4946 assert!(TokenEnvironment::Auto.is_live());
4947
4948 // Clean up
4949 env::remove_var("AMP_USERNAME");
4950 env::remove_var("AMP_PASSWORD");
4951 env::remove_var("AMP_TESTS");
4952 }
4953
4954 #[tokio::test]
4955 async fn test_token_environment_create_strategy_mock() {
4956 let mock_token = "test_mock_token".to_string();
4957 let strategy = TokenEnvironment::Mock
4958 .create_strategy(Some(mock_token.clone()))
4959 .await
4960 .unwrap();
4961
4962 assert_eq!(strategy.strategy_type(), "mock");
4963 assert!(!strategy.should_persist());
4964
4965 let token = strategy.get_token().await.unwrap();
4966 assert_eq!(token, mock_token);
4967 }
4968
4969 #[tokio::test]
4970 async fn test_token_environment_create_strategy_live() {
4971 let strategy = TokenEnvironment::Live.create_strategy(None).await.unwrap();
4972
4973 assert_eq!(strategy.strategy_type(), "live");
4974 assert!(strategy.should_persist());
4975 }
4976
4977 #[tokio::test]
4978 async fn test_token_environment_create_auto_strategy() {
4979 // Test with mock environment - need both username and password for proper detection
4980 env::set_var("AMP_USERNAME", "mock_user");
4981 env::set_var("AMP_PASSWORD", "some_password");
4982 env::remove_var("AMP_TESTS");
4983 env::remove_var("AMP_API_BASE_URL");
4984
4985 let mock_token = "auto_mock_token".to_string();
4986 let strategy = TokenEnvironment::create_auto_strategy(Some(mock_token.clone()))
4987 .await
4988 .unwrap();
4989
4990 assert_eq!(strategy.strategy_type(), "mock");
4991 let token = strategy.get_token().await.unwrap();
4992 assert_eq!(token, mock_token);
4993
4994 // Clean up
4995 env::remove_var("AMP_USERNAME");
4996 env::remove_var("AMP_PASSWORD");
4997 }
4998
4999 #[tokio::test]
5000 async fn test_mock_token_strategy_factory_methods() {
5001 // Test with_default_token
5002 let strategy = MockTokenStrategy::with_default_token();
5003 assert_eq!(strategy.strategy_type(), "mock");
5004 let token = strategy.get_token().await.unwrap();
5005 assert_eq!(token, "mock_token_default");
5006
5007 // Test for_test
5008 let strategy = MockTokenStrategy::for_test("my_test");
5009 let token = strategy.get_token().await.unwrap();
5010 assert_eq!(token, "mock_token_my_test");
5011 }
5012
5013 #[tokio::test]
5014 async fn test_live_token_strategy_factory_methods() {
5015 // Test for_testing
5016 let strategy = LiveTokenStrategy::for_testing().await.unwrap();
5017 assert_eq!(strategy.strategy_type(), "live");
5018 assert!(strategy.should_persist());
5019 }
5020
5021 #[tokio::test]
5022 async fn test_standalone_factory_functions() {
5023 // Test create_mock_token_strategy
5024 let mock_token = "standalone_mock".to_string();
5025 let strategy = create_mock_token_strategy(mock_token.clone());
5026 assert_eq!(strategy.strategy_type(), "mock");
5027 let token = strategy.get_token().await.unwrap();
5028 assert_eq!(token, mock_token);
5029
5030 // Test create_live_token_strategy
5031 let strategy = create_live_token_strategy().await.unwrap();
5032 assert_eq!(strategy.strategy_type(), "live");
5033
5034 // Test create_auto_token_strategy with mock environment
5035 env::set_var("AMP_USERNAME", "mock_user");
5036 env::set_var("AMP_PASSWORD", "some_password");
5037 env::remove_var("AMP_TESTS");
5038 env::remove_var("AMP_API_BASE_URL");
5039
5040 let auto_mock_token = "auto_standalone_mock".to_string();
5041 let strategy = create_auto_token_strategy(Some(auto_mock_token.clone()))
5042 .await
5043 .unwrap();
5044 assert_eq!(strategy.strategy_type(), "mock");
5045 let token = strategy.get_token().await.unwrap();
5046 assert_eq!(token, auto_mock_token);
5047
5048 // Test create_token_strategy_for_environment
5049 let env_mock_token = "env_mock".to_string();
5050 let strategy = create_token_strategy_for_environment(
5051 TokenEnvironment::Mock,
5052 Some(env_mock_token.clone()),
5053 )
5054 .await
5055 .unwrap();
5056 assert_eq!(strategy.strategy_type(), "mock");
5057 let token = strategy.get_token().await.unwrap();
5058 assert_eq!(token, env_mock_token);
5059
5060 // Clean up
5061 env::remove_var("AMP_USERNAME");
5062 env::remove_var("AMP_PASSWORD");
5063 }
5064
5065 #[test]
5066 fn test_environment_detection_with_various_credential_combinations() {
5067 // Test case 1: AMP_TESTS=live overrides everything
5068 env::set_var("AMP_TESTS", "live");
5069 env::set_var("AMP_USERNAME", "mock_user");
5070 env::set_var("AMP_PASSWORD", "mock_pass");
5071 env::set_var("AMP_API_BASE_URL", "http://localhost:8080");
5072 assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Live);
5073
5074 // Test case 2: Mock username with real password and URL
5075 env::remove_var("AMP_TESTS");
5076 env::set_var("AMP_USERNAME", "mock_user");
5077 env::set_var("AMP_PASSWORD", "real_password");
5078 env::set_var("AMP_API_BASE_URL", "https://amp-test.blockstream.com/api");
5079 assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Mock);
5080
5081 // Test case 3: Real username with mock password
5082 env::set_var("AMP_USERNAME", "real_user");
5083 env::set_var("AMP_PASSWORD", "mock_password");
5084 env::set_var("AMP_API_BASE_URL", "https://amp-test.blockstream.com/api");
5085 assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Mock);
5086
5087 // Test case 4: Real credentials with localhost URL
5088 env::set_var("AMP_USERNAME", "real_user");
5089 env::set_var("AMP_PASSWORD", "real_password");
5090 env::set_var("AMP_API_BASE_URL", "http://localhost:3000/api");
5091 assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Mock);
5092
5093 // Test case 5: All real credentials
5094 env::set_var("AMP_USERNAME", "real_user");
5095 env::set_var("AMP_PASSWORD", "real_password");
5096 env::set_var("AMP_API_BASE_URL", "https://amp-test.blockstream.com/api");
5097 assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Live);
5098
5099 // Test case 6: Empty credentials
5100 env::remove_var("AMP_USERNAME");
5101 env::remove_var("AMP_PASSWORD");
5102 env::remove_var("AMP_API_BASE_URL");
5103 assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Mock);
5104
5105 // Test case 7: Only username set
5106 env::set_var("AMP_USERNAME", "real_user");
5107 env::remove_var("AMP_PASSWORD");
5108 assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Mock);
5109
5110 // Test case 8: Only password set
5111 env::remove_var("AMP_USERNAME");
5112 env::set_var("AMP_PASSWORD", "real_password");
5113 assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Mock);
5114
5115 // Clean up all environment variables
5116 env::remove_var("AMP_TESTS");
5117 env::remove_var("AMP_USERNAME");
5118 env::remove_var("AMP_PASSWORD");
5119 env::remove_var("AMP_API_BASE_URL");
5120 }
5121}