amp_rs/
model.rs

1use chrono::{DateTime, Duration, Utc};
2use secrecy::{DebugSecret, Secret, SerializableSecret};
3use serde::{Deserialize, Deserializer, Serialize, Serializer};
4
5#[cfg(test)]
6use std::collections::HashMap;
7
8use zeroize::Zeroize;
9
10/// Request payload for AMP token acquisition
11#[derive(Debug, Serialize)]
12pub struct TokenRequest {
13    pub username: String,
14    pub password: String,
15}
16
17/// Response from AMP token acquisition
18#[derive(Debug, Deserialize)]
19pub struct TokenResponse {
20    pub token: String,
21}
22
23#[derive(Clone, Serialize, Deserialize)]
24pub struct Password(pub String);
25
26impl Zeroize for Password {
27    fn zeroize(&mut self) {
28        self.0.zeroize();
29    }
30}
31
32impl From<String> for Password {
33    fn from(s: String) -> Self {
34        Self(s)
35    }
36}
37
38impl SerializableSecret for Password {}
39
40impl DebugSecret for Password {}
41
42#[derive(Debug, Serialize)]
43pub struct ChangePasswordRequest {
44    pub password: Secret<Password>,
45}
46
47#[derive(Debug, Deserialize)]
48pub struct ChangePasswordResponse {
49    pub username: String,
50    pub password: Secret<Password>,
51    pub token: Secret<String>,
52}
53
54#[derive(Debug, Deserialize, Serialize, Clone)]
55#[allow(clippy::struct_excessive_bools)]
56pub struct Asset {
57    pub name: String,
58    pub asset_uuid: String,
59    pub issuer: i64,
60    pub asset_id: String,
61    pub reissuance_token_id: Option<String>,
62    pub requirements: Vec<i64>,
63    pub ticker: Option<String>,
64    pub precision: i64,
65    pub domain: Option<String>,
66    pub pubkey: Option<String>,
67    pub is_registered: bool,
68    pub is_authorized: bool,
69    pub is_locked: bool,
70    pub issuer_authorization_endpoint: Option<String>,
71    pub transfer_restricted: bool,
72}
73
74#[derive(Debug, Serialize)]
75pub struct IssuanceRequest {
76    pub name: String,
77    pub amount: i64,
78    pub destination_address: String,
79    pub domain: String,
80    pub ticker: String,
81    pub pubkey: String,
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub precision: Option<i64>,
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub is_confidential: Option<bool>,
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub is_reissuable: Option<bool>,
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub reissuance_amount: Option<i64>,
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub reissuance_address: Option<String>,
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub transfer_restricted: Option<bool>,
94}
95
96#[derive(Debug, Deserialize)]
97pub struct IssuanceResponse {
98    pub name: String,
99    pub amount: i64,
100    pub destination_address: String,
101    pub domain: String,
102    pub ticker: String,
103    pub pubkey: String,
104    pub is_confidential: bool,
105    pub is_reissuable: bool,
106    pub reissuance_amount: i64,
107    pub reissuance_address: String,
108    pub asset_id: String,
109    pub reissuance_token_id: Option<String>,
110    pub asset_uuid: String,
111    pub txid: String,
112    pub vin: i64,
113    pub asset_vout: i64,
114    pub reissuance_vout: Option<i64>,
115    pub issuer_authorization_endpoint: Option<String>,
116    pub transfer_restricted: bool,
117    pub issuance_assetblinder: String,
118    pub issuance_tokenblinder: Option<String>,
119}
120
121/// Response from asset registration with the Blockstream Asset Registry
122/// Response from asset registration with the Blockstream Asset Registry
123#[derive(Debug, Deserialize, Serialize, Clone)]
124pub struct RegisterAssetResponse {
125    /// Indicates whether the registration was successful
126    pub success: bool,
127    /// Optional message providing additional context
128    pub message: Option<String>,
129    /// The full asset data if registration was successful (HTTP 200 with asset data)
130    #[serde(flatten)]
131    pub asset_data: Option<Asset>,
132}
133
134#[derive(Debug, Serialize)]
135pub struct EditAssetRequest {
136    pub issuer_authorization_endpoint: String,
137}
138
139#[derive(Debug, Deserialize)]
140pub struct RegisteredUserResponse {
141    pub id: i64,
142    #[serde(rename = "GAID")]
143    pub gaid: Option<String>,
144    pub is_company: bool,
145    pub name: String,
146    pub categories: Vec<i64>,
147    pub creator: i64,
148}
149
150#[derive(Debug, Serialize)]
151pub struct RegisteredUserAdd {
152    pub name: String,
153    #[serde(rename = "GAID")]
154    pub gaid: Option<String>,
155    pub is_company: bool,
156}
157
158#[derive(Debug, Serialize)]
159pub struct RegisteredUserEdit {
160    pub name: Option<String>,
161}
162
163#[derive(Debug, Serialize)]
164pub struct GaidRequest {
165    #[serde(rename = "GAID")]
166    pub gaid: String,
167}
168
169#[derive(Debug, Serialize)]
170pub struct CategoriesRequest {
171    pub categories: Vec<i64>,
172}
173
174#[derive(Debug, Deserialize)]
175pub struct CategoryResponse {
176    pub id: i64,
177    pub name: String,
178    pub description: Option<String>,
179    pub registered_users: Vec<i64>,
180    pub assets: Vec<String>,
181}
182
183#[derive(Debug, Serialize)]
184pub struct CategoryAdd {
185    pub name: String,
186    pub description: Option<String>,
187}
188
189#[derive(Debug, Serialize)]
190pub struct CategoryEdit {
191    pub name: Option<String>,
192    pub description: Option<String>,
193}
194
195#[derive(Debug, Deserialize)]
196pub struct ValidateGaidResponse {
197    pub is_valid: bool,
198    pub error: Option<String>,
199}
200
201#[derive(Debug, Deserialize)]
202pub struct AddressGaidResponse {
203    pub address: String,
204    pub error: Option<String>,
205}
206
207#[derive(Debug, Deserialize)]
208pub struct Manager {
209    pub username: String,
210    pub id: i64,
211    pub is_locked: bool,
212    pub assets: Vec<String>,
213}
214
215#[derive(Debug, Serialize)]
216pub struct ManagerCreate {
217    pub username: String,
218    pub password: String,
219}
220
221#[derive(Debug, Deserialize, Serialize)]
222#[serde(rename_all = "UPPERCASE")]
223pub enum Status {
224    Unconfirmed,
225    Confirmed,
226}
227
228#[derive(Debug, Deserialize, Serialize)]
229pub struct DistributionAssignment {
230    pub registered_user: i64,
231    pub amount: i64,
232    pub vout: i64,
233}
234
235#[derive(Debug, Deserialize, Serialize)]
236pub struct Transaction {
237    pub txid: String,
238    pub transaction_status: Status,
239    pub included_blockheight: i64,
240    pub confirmed_datetime: String,
241    pub assignments: Vec<DistributionAssignment>,
242}
243
244#[derive(Debug, Deserialize, Serialize)]
245pub struct Distribution {
246    pub distribution_uuid: String,
247    pub distribution_status: Status,
248    pub transactions: Vec<Transaction>,
249}
250
251#[derive(Debug, Serialize, Deserialize, Clone)]
252pub struct CreateAssetAssignmentRequest {
253    pub registered_user: i64,
254    pub amount: i64,
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub vesting_timestamp: Option<i64>, // Unix timestamp in seconds, nullable
257    #[serde(default = "default_ready_for_distribution")]
258    pub ready_for_distribution: bool, // Defaults to false
259}
260
261const fn default_ready_for_distribution() -> bool {
262    false
263}
264
265#[derive(Debug, Serialize)]
266pub struct CreateAssetAssignmentRequestWrapper {
267    pub assignments: Vec<CreateAssetAssignmentRequest>,
268}
269
270#[derive(Debug, Deserialize, Serialize)]
271pub struct Assignment {
272    pub id: i64,
273    pub registered_user: i64,
274    pub amount: i64,
275    pub receiving_address: Option<String>,
276    pub distribution_uuid: Option<String>,
277    pub ready_for_distribution: bool,
278    pub vesting_datetime: Option<String>,
279    pub vesting_timestamp: Option<i64>,
280    pub has_vested: bool,
281    pub is_distributed: bool,
282    pub creator: i64,
283    #[serde(rename = "GAID")]
284    pub gaid: Option<String>,
285    // Legacy field for backward compatibility
286    #[serde(skip_serializing_if = "Option::is_none")]
287    pub investor: Option<i64>,
288}
289
290#[derive(Debug, Deserialize, Serialize)]
291pub struct RegisteredUserSummary {
292    pub asset_uuid: String,
293    pub asset_id: String,
294    pub assignments: Vec<Assignment>,
295    pub assignments_sum: i64,
296    pub distributions: Vec<Distribution>,
297    pub distributions_sum: i64,
298    pub balance: i64,
299}
300
301#[derive(Debug, Deserialize, Serialize)]
302pub struct Activity {
303    #[serde(rename = "type")]
304    pub activity_type: String,
305    pub datetime: String,
306    pub description: String,
307    pub txid: String,
308    pub vout: i64,
309    pub blockheight: i64,
310    pub asset_blinder: String,
311    pub amount_blinder: String,
312    #[serde(rename = "registered user")]
313    pub registered_user: Option<i64>,
314    pub amount: i64,
315}
316
317#[derive(Debug, Serialize, Default)]
318pub struct AssetActivityParams {
319    #[serde(skip_serializing_if = "Option::is_none")]
320    pub start: Option<i64>,
321    #[serde(skip_serializing_if = "Option::is_none")]
322    pub count: Option<i64>,
323    #[serde(skip_serializing_if = "Option::is_none")]
324    pub sortcolumn: Option<String>,
325    #[serde(skip_serializing_if = "Option::is_none")]
326    pub sortorder: Option<String>,
327    #[serde(skip_serializing_if = "Option::is_none")]
328    pub height_start: Option<i64>,
329    #[serde(skip_serializing_if = "Option::is_none")]
330    pub height_stop: Option<i64>,
331}
332
333#[derive(Debug, Deserialize, Serialize)]
334pub struct Ownership {
335    pub owner: String,
336    pub amount: i64,
337    #[serde(rename = "GAID")]
338    pub gaid: Option<String>,
339}
340
341#[derive(Debug, Deserialize, Serialize)]
342pub struct Outpoint {
343    pub txid: String,
344    pub vout: i64,
345}
346
347pub type LostOutputs = Vec<Outpoint>;
348
349#[derive(Debug, Deserialize, Serialize)]
350pub struct GaidBalanceEntry {
351    pub asset_uuid: String,
352    pub asset_id: String,
353    pub balance: i64,
354}
355
356pub type Balance = Vec<GaidBalanceEntry>;
357
358#[derive(Debug, Deserialize, Serialize)]
359pub struct AssetLostOutputs {
360    pub lost_outputs: LostOutputs,
361    pub reissuance_lost_outputs: LostOutputs,
362}
363
364#[derive(Debug, Deserialize, Serialize)]
365pub struct AssetSummary {
366    pub asset_id: String,
367    pub reissuance_token_id: Option<String>,
368    pub issued: i64,
369    pub reissued: i64,
370    pub assigned: i64,
371    pub distributed: i64,
372    pub burned: i64,
373    pub blacklisted: i64,
374    pub registered_users: i64,
375    pub active_registered_users: i64,
376    pub active_green_subaccounts: i64,
377    #[serde(rename = "reissuance_tokens")]
378    pub reissuance_tokens: i64,
379}
380
381#[derive(Debug, Deserialize, Serialize)]
382pub struct Utxo {
383    pub txid: String,
384    pub vout: i64,
385    pub asset: String,
386    pub amount: i64,
387    pub registered_user: Option<i64>,
388    pub gaid: Option<String>,
389    pub blacklisted: bool,
390}
391
392#[derive(Debug, Deserialize, Serialize)]
393pub struct Reissuance {
394    pub txid: String,
395    pub vout: i64,
396    pub destination_address: String,
397    pub reissuance_amount: i64,
398    pub confirmed_in_block: String,
399    pub created: String,
400}
401
402#[derive(Debug, Serialize)]
403pub struct ReissueRequest {
404    pub amount_to_reissue: i64,
405}
406
407#[derive(Debug, Serialize)]
408pub struct ReissueConfirmRequest {
409    pub details: serde_json::Value,
410    pub listissuances: Vec<serde_json::Value>,
411    pub reissuance_output: serde_json::Value,
412}
413
414#[derive(Debug, Serialize)]
415pub struct BurnRequest {
416    pub amount: i64,
417}
418
419#[derive(Debug, Deserialize, Serialize)]
420pub struct BurnCreate {
421    pub command: String,
422    pub min_supported_client_script_version: i64,
423    pub base_url: String,
424    pub asset_uuid: String,
425    pub asset_id: String,
426    pub amount: f64,
427    pub utxos: Vec<Outpoint>,
428}
429
430#[derive(Debug, Serialize)]
431pub struct BurnConfirmRequest {
432    pub tx_data: serde_json::Value,
433    pub change_data: Vec<serde_json::Value>,
434}
435
436#[derive(Debug, Serialize)]
437pub struct SetAssetMemoRequest {
438    pub memo: String,
439}
440
441#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
442#[serde(rename_all = "lowercase")]
443pub enum Permission {
444    View,
445    Receive,
446    Transfer,
447    Assign,
448    Distribute,
449    Reissue,
450    Burn,
451    Acquire,
452    Manage,
453    Permissions,
454}
455
456#[derive(Debug, Deserialize, Serialize)]
457pub struct BroadcastResponse {
458    pub txid: String,
459    pub hex: String,
460}
461
462/// Enhanced token data structure with secure storage and timestamp tracking
463#[derive(Debug, Clone, Serialize, Deserialize)]
464pub struct TokenData {
465    #[serde(with = "secret_serde")]
466    pub token: Secret<String>,
467    pub expires_at: DateTime<Utc>,
468    pub obtained_at: DateTime<Utc>,
469}
470
471impl TokenData {
472    /// Creates a new `TokenData` instance
473    ///
474    /// # Examples
475    /// ```
476    /// # use amp_rs::model::TokenData;
477    /// # use chrono::{Utc, Duration};
478    /// let expires_at = Utc::now() + Duration::hours(24);
479    /// let token_data = TokenData::new("my_token".to_string(), expires_at);
480    /// assert!(!token_data.is_expired());
481    /// ```
482    #[must_use]
483    pub fn new(token: String, expires_at: DateTime<Utc>) -> Self {
484        Self {
485            token: Secret::new(token),
486            expires_at,
487            obtained_at: Utc::now(),
488        }
489    }
490
491    /// Checks if the token is expired
492    ///
493    /// # Examples
494    /// ```
495    /// # use amp_rs::model::TokenData;
496    /// # use chrono::{Utc, Duration};
497    /// // Create an expired token
498    /// let expires_at = Utc::now() - Duration::hours(1);
499    /// let token_data = TokenData::new("expired_token".to_string(), expires_at);
500    /// assert!(token_data.is_expired());
501    ///
502    /// // Create a valid token
503    /// let expires_at = Utc::now() + Duration::hours(1);
504    /// let token_data = TokenData::new("valid_token".to_string(), expires_at);
505    /// assert!(!token_data.is_expired());
506    /// ```
507    #[must_use]
508    pub fn is_expired(&self) -> bool {
509        Utc::now() > self.expires_at
510    }
511
512    /// Checks if the token expires within the given threshold
513    ///
514    /// # Examples
515    /// ```
516    /// # use amp_rs::model::TokenData;
517    /// # use chrono::{Utc, Duration};
518    /// // Token expires in 30 minutes
519    /// let expires_at = Utc::now() + Duration::minutes(30);
520    /// let token_data = TokenData::new("token".to_string(), expires_at);
521    ///
522    /// // Check if it expires within 1 hour
523    /// assert!(token_data.expires_soon(Duration::hours(1)));
524    ///
525    /// // Check if it expires within 15 minutes
526    /// assert!(!token_data.expires_soon(Duration::minutes(15)));
527    /// ```
528    #[must_use]
529    pub fn expires_soon(&self, threshold: Duration) -> bool {
530        Utc::now() + threshold > self.expires_at
531    }
532
533    /// Returns the age of the token
534    ///
535    /// # Examples
536    /// ```
537    /// # use amp_rs::model::TokenData;
538    /// # use chrono::{Utc, Duration};
539    /// let expires_at = Utc::now() + Duration::hours(24);
540    /// let token_data = TokenData::new("token".to_string(), expires_at);
541    ///
542    /// // Token age should be very small (just created)
543    /// let age = token_data.age();
544    /// assert!(age < Duration::seconds(1));
545    /// ```
546    #[must_use]
547    pub fn age(&self) -> Duration {
548        Utc::now() - self.obtained_at
549    }
550}
551
552/// Token information for debugging and monitoring
553#[derive(Debug, Clone)]
554pub struct TokenInfo {
555    pub expires_at: DateTime<Utc>,
556    pub obtained_at: DateTime<Utc>,
557    pub expires_in: Duration,
558    pub age: Duration,
559    pub is_expired: bool,
560    pub expires_soon: bool,
561}
562
563impl From<&TokenData> for TokenInfo {
564    fn from(token_data: &TokenData) -> Self {
565        let now = Utc::now();
566        let expires_in = token_data.expires_at - now;
567        let expires_soon_threshold = Duration::minutes(5);
568
569        Self {
570            expires_at: token_data.expires_at,
571            obtained_at: token_data.obtained_at,
572            expires_in,
573            age: token_data.age(),
574            is_expired: token_data.is_expired(),
575            expires_soon: token_data.expires_soon(expires_soon_threshold),
576        }
577    }
578}
579
580/// Assignment for asset distribution workflow
581#[derive(Debug, Clone, Serialize, Deserialize)]
582pub struct AssetDistributionAssignment {
583    pub user_id: String,
584    pub address: String,
585    pub amount: f64,
586}
587
588/// Assignment for distribution creation API request
589#[derive(Debug, Clone, Serialize, Deserialize)]
590pub struct DistributionAssignmentRequest {
591    pub user_uuid: String,
592    pub amount: f64,
593    pub address: String,
594}
595
596/// Request payload for distribution creation API
597#[derive(Debug, Clone, Serialize, Deserialize)]
598pub struct CreateDistributionRequest {
599    pub assignments: Vec<DistributionAssignmentRequest>,
600}
601
602/// UTXO information from Elements node
603#[derive(Debug, Clone, Serialize, Deserialize)]
604pub struct Unspent {
605    pub txid: String,
606    pub vout: u32,
607    pub amount: f64,
608    pub asset: String,
609    pub address: String,
610    pub spendable: bool,
611    #[serde(skip_serializing_if = "Option::is_none")]
612    pub confirmations: Option<u32>,
613    #[serde(skip_serializing_if = "Option::is_none")]
614    pub scriptpubkey: Option<String>,
615    #[serde(skip_serializing_if = "Option::is_none")]
616    pub redeemscript: Option<String>,
617    #[serde(skip_serializing_if = "Option::is_none")]
618    pub witnessscript: Option<String>,
619    /// Amount blinder for confidential transactions (Elements specific)
620    #[serde(skip_serializing_if = "Option::is_none")]
621    pub amountblinder: Option<String>,
622    /// Asset blinder for confidential transactions (Elements specific)
623    #[serde(skip_serializing_if = "Option::is_none")]
624    pub assetblinder: Option<String>,
625}
626
627/// Transaction details from Elements node (full gettransaction response)
628#[derive(Debug, Clone, Serialize, Deserialize)]
629pub struct TransactionDetail {
630    pub txid: String,
631    pub confirmations: u32,
632    #[serde(skip_serializing_if = "Option::is_none")]
633    pub blockheight: Option<u64>,
634    pub hex: String,
635    #[serde(skip_serializing_if = "Option::is_none")]
636    pub blockhash: Option<String>,
637    #[serde(skip_serializing_if = "Option::is_none")]
638    pub blocktime: Option<i64>,
639    #[serde(skip_serializing_if = "Option::is_none")]
640    pub time: Option<i64>,
641    #[serde(skip_serializing_if = "Option::is_none")]
642    pub timereceived: Option<i64>,
643    /// The details field from gettransaction (array of transaction outputs)
644    #[serde(skip_serializing_if = "Option::is_none")]
645    pub details: Option<Vec<serde_json::Value>>,
646}
647
648/// Transaction output detail from Elements gettransaction details array
649#[derive(Debug, Clone, Serialize, Deserialize)]
650pub struct TransactionOutputDetail {
651    pub account: String,
652    pub address: String,
653    pub category: String,
654    pub amount: f64,
655    pub vout: u32,
656    #[serde(skip_serializing_if = "Option::is_none")]
657    pub fee: Option<f64>,
658    #[serde(skip_serializing_if = "Option::is_none")]
659    pub confirmations: Option<u32>,
660    #[serde(skip_serializing_if = "Option::is_none")]
661    pub blockhash: Option<String>,
662    #[serde(skip_serializing_if = "Option::is_none")]
663    pub blockindex: Option<u32>,
664    #[serde(skip_serializing_if = "Option::is_none")]
665    pub blocktime: Option<i64>,
666    #[serde(skip_serializing_if = "Option::is_none")]
667    pub txid: Option<String>,
668    #[serde(skip_serializing_if = "Option::is_none")]
669    pub time: Option<i64>,
670    #[serde(skip_serializing_if = "Option::is_none")]
671    pub timereceived: Option<i64>,
672    #[serde(skip_serializing_if = "Option::is_none")]
673    pub asset: Option<String>,
674    #[serde(skip_serializing_if = "Option::is_none")]
675    pub assetblinder: Option<String>,
676    #[serde(skip_serializing_if = "Option::is_none")]
677    pub amountblinder: Option<String>,
678}
679
680/// Transaction input for raw transaction creation
681#[derive(Debug, Clone, Serialize, Deserialize)]
682pub struct TxInput {
683    pub txid: String,
684    pub vout: u32,
685    #[serde(skip_serializing_if = "Option::is_none")]
686    pub sequence: Option<u32>,
687}
688
689/// Response from distribution creation API
690#[derive(Debug, Clone, Serialize, Deserialize)]
691pub struct DistributionResponse {
692    pub distribution_uuid: String,
693    pub map_address_amount: std::collections::HashMap<String, f64>,
694    pub map_address_asset: std::collections::HashMap<String, String>,
695    pub asset_id: String,
696}
697
698/// Address information from listreceivedbyaddress RPC
699#[derive(Debug, Clone, Serialize, Deserialize)]
700pub struct ReceivedByAddress {
701    #[serde(skip_serializing_if = "Option::is_none")]
702    pub address: Option<String>,
703    #[serde(skip_serializing_if = "Option::is_none")]
704    pub amount: Option<std::collections::HashMap<String, f64>>,
705    #[serde(skip_serializing_if = "Option::is_none")]
706    pub confirmations: Option<u32>,
707    #[serde(skip_serializing_if = "Option::is_none")]
708    pub label: Option<String>,
709    #[serde(skip_serializing_if = "Option::is_none")]
710    pub txids: Option<Vec<String>>,
711}
712
713/// Transaction data for distribution confirmation
714#[derive(Debug, Clone, Serialize, Deserialize)]
715pub struct DistributionTxData {
716    pub details: TransactionDetail,
717    pub txid: String,
718}
719
720/// Transaction data for AMP API confirmation (matches Python implementation)
721#[derive(Debug, Clone, Serialize, Deserialize)]
722pub struct AmpTxData {
723    /// The details field from gettransaction (array of transaction outputs)
724    pub details: serde_json::Value,
725    pub txid: String,
726}
727
728/// Request payload for distribution confirmation API
729#[derive(Debug, Clone, Serialize, Deserialize)]
730pub struct ConfirmDistributionRequest {
731    pub tx_data: AmpTxData,
732    pub change_data: Vec<Unspent>,
733}
734
735#[cfg(test)]
736mod tests {
737    use super::*;
738
739    #[test]
740    fn test_asset_distribution_assignment_creation() {
741        let assignment = AssetDistributionAssignment {
742            user_id: "user123".to_string(),
743            address: "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
744            amount: 100.5,
745        };
746
747        assert_eq!(assignment.user_id, "user123");
748        assert_eq!(
749            assignment.address,
750            "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq"
751        );
752        assert_eq!(assignment.amount, 100.5);
753    }
754
755    #[test]
756    fn test_asset_distribution_assignment_serialization() {
757        let assignment = AssetDistributionAssignment {
758            user_id: "user123".to_string(),
759            address: "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
760            amount: 100.5,
761        };
762
763        // Test serialization
764        let json = serde_json::to_string(&assignment).unwrap();
765        assert!(json.contains("user123"));
766        assert!(json.contains("lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq"));
767        assert!(json.contains("100.5"));
768
769        // Test deserialization
770        let deserialized: AssetDistributionAssignment = serde_json::from_str(&json).unwrap();
771        assert_eq!(deserialized.user_id, assignment.user_id);
772        assert_eq!(deserialized.address, assignment.address);
773        assert_eq!(deserialized.amount, assignment.amount);
774    }
775
776    #[test]
777    fn test_asset_distribution_assignment_clone() {
778        let assignment = AssetDistributionAssignment {
779            user_id: "user123".to_string(),
780            address: "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
781            amount: 100.5,
782        };
783
784        let cloned = assignment.clone();
785        assert_eq!(assignment.user_id, cloned.user_id);
786        assert_eq!(assignment.address, cloned.address);
787        assert_eq!(assignment.amount, cloned.amount);
788    }
789
790    #[test]
791    fn test_unspent_creation_and_serialization() {
792        let unspent = Unspent {
793            txid: "abc123def456".to_string(),
794            vout: 1,
795            amount: 50.0,
796            asset: "asset_id_hex".to_string(),
797            address: "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
798            spendable: true,
799            confirmations: Some(6),
800            scriptpubkey: Some("76a914...88ac".to_string()),
801            redeemscript: None,
802            witnessscript: None,
803            amountblinder: Some(
804                "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
805            ),
806            assetblinder: Some(
807                "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210".to_string(),
808            ),
809        };
810
811        // Test serialization
812        let json = serde_json::to_string(&unspent).unwrap();
813        assert!(json.contains("abc123def456"));
814        assert!(json.contains("50.0"));
815        assert!(json.contains("asset_id_hex"));
816
817        // Test deserialization
818        let deserialized: Unspent = serde_json::from_str(&json).unwrap();
819        assert_eq!(deserialized.txid, unspent.txid);
820        assert_eq!(deserialized.vout, unspent.vout);
821        assert_eq!(deserialized.amount, unspent.amount);
822        assert_eq!(deserialized.asset, unspent.asset);
823        assert_eq!(deserialized.confirmations, unspent.confirmations);
824    }
825
826    #[test]
827    fn test_transaction_detail_creation() {
828        let tx_detail = TransactionDetail {
829            txid: "def456abc123".to_string(),
830            confirmations: 3,
831            blockheight: Some(12345),
832            hex: "020000000001...".to_string(),
833            blockhash: Some("block_hash_hex".to_string()),
834            details: Some(vec![]),
835            blocktime: Some(1640995200),
836            time: Some(1640995200),
837            timereceived: Some(1640995180),
838        };
839
840        assert_eq!(tx_detail.txid, "def456abc123");
841        assert_eq!(tx_detail.confirmations, 3);
842        assert_eq!(tx_detail.blockheight, Some(12345));
843
844        // Test serialization
845        let json = serde_json::to_string(&tx_detail).unwrap();
846        assert!(json.contains("def456abc123"));
847        assert!(json.contains("\"confirmations\":3"));
848    }
849
850    #[test]
851    fn test_tx_input_creation() {
852        let tx_input = TxInput {
853            txid: "input_txid_123".to_string(),
854            vout: 2,
855            sequence: Some(0xffffffff),
856        };
857
858        assert_eq!(tx_input.txid, "input_txid_123");
859        assert_eq!(tx_input.vout, 2);
860        assert_eq!(tx_input.sequence, Some(0xffffffff));
861
862        // Test serialization
863        let json = serde_json::to_string(&tx_input).unwrap();
864        assert!(json.contains("input_txid_123"));
865        assert!(json.contains("\"vout\":2"));
866        assert!(json.contains("4294967295")); // 0xffffffff in decimal
867    }
868
869    #[test]
870    fn test_distribution_response_creation() {
871        let mut map_address_amount = HashMap::new();
872        map_address_amount.insert("address1".to_string(), 100.0);
873        map_address_amount.insert("address2".to_string(), 50.0);
874
875        let mut map_address_asset = HashMap::new();
876        map_address_asset.insert("address1".to_string(), "asset_id_1".to_string());
877        map_address_asset.insert("address2".to_string(), "asset_id_1".to_string());
878
879        let distribution_response = DistributionResponse {
880            distribution_uuid: "dist_uuid_123".to_string(),
881            map_address_amount,
882            map_address_asset,
883            asset_id: "main_asset_id".to_string(),
884        };
885
886        assert_eq!(distribution_response.distribution_uuid, "dist_uuid_123");
887        assert_eq!(distribution_response.asset_id, "main_asset_id");
888        assert_eq!(distribution_response.map_address_amount.len(), 2);
889        assert_eq!(distribution_response.map_address_asset.len(), 2);
890
891        // Test serialization
892        let json = serde_json::to_string(&distribution_response).unwrap();
893        assert!(json.contains("dist_uuid_123"));
894        assert!(json.contains("main_asset_id"));
895        assert!(json.contains("address1"));
896        assert!(json.contains("100.0"));
897    }
898
899    #[test]
900    fn test_distribution_assignment_request_creation() {
901        let assignment_request = DistributionAssignmentRequest {
902            user_uuid: "user_uuid_123".to_string(),
903            amount: 150.0,
904            address: "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
905        };
906
907        assert_eq!(assignment_request.user_uuid, "user_uuid_123");
908        assert_eq!(assignment_request.amount, 150.0);
909        assert_eq!(
910            assignment_request.address,
911            "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq"
912        );
913
914        // Test serialization
915        let json = serde_json::to_string(&assignment_request).unwrap();
916        assert!(json.contains("user_uuid_123"));
917        assert!(json.contains("150.0"));
918        assert!(json.contains("lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq"));
919
920        // Test deserialization
921        let deserialized: DistributionAssignmentRequest = serde_json::from_str(&json).unwrap();
922        assert_eq!(deserialized.user_uuid, assignment_request.user_uuid);
923        assert_eq!(deserialized.amount, assignment_request.amount);
924        assert_eq!(deserialized.address, assignment_request.address);
925    }
926
927    #[test]
928    fn test_create_distribution_request_creation() {
929        let assignments = vec![
930            DistributionAssignmentRequest {
931                user_uuid: "user1".to_string(),
932                amount: 100.0,
933                address: "address1".to_string(),
934            },
935            DistributionAssignmentRequest {
936                user_uuid: "user2".to_string(),
937                amount: 50.0,
938                address: "address2".to_string(),
939            },
940        ];
941
942        let create_request = CreateDistributionRequest {
943            assignments: assignments.clone(),
944        };
945
946        assert_eq!(create_request.assignments.len(), 2);
947        assert_eq!(create_request.assignments[0].user_uuid, "user1");
948        assert_eq!(create_request.assignments[1].amount, 50.0);
949
950        // Test serialization
951        let json = serde_json::to_string(&create_request).unwrap();
952        assert!(json.contains("user1"));
953        assert!(json.contains("user2"));
954        assert!(json.contains("100.0"));
955        assert!(json.contains("50.0"));
956        assert!(json.contains("assignments"));
957
958        // Test deserialization
959        let deserialized: CreateDistributionRequest = serde_json::from_str(&json).unwrap();
960        assert_eq!(deserialized.assignments.len(), 2);
961        assert_eq!(deserialized.assignments[0].user_uuid, "user1");
962        assert_eq!(deserialized.assignments[1].user_uuid, "user2");
963    }
964
965    #[test]
966    fn test_distribution_request_serialization_format() {
967        let assignment = DistributionAssignmentRequest {
968            user_uuid: "test_user".to_string(),
969            amount: 123.45,
970            address: "test_address".to_string(),
971        };
972
973        let request = CreateDistributionRequest {
974            assignments: vec![assignment],
975        };
976
977        let json = serde_json::to_string(&request).unwrap();
978
979        // Verify the JSON structure matches the API specification
980        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
981
982        assert!(parsed.get("assignments").is_some());
983        let assignments_array = parsed["assignments"].as_array().unwrap();
984        assert_eq!(assignments_array.len(), 1);
985
986        let first_assignment = &assignments_array[0];
987        assert_eq!(first_assignment["user_uuid"], "test_user");
988        assert_eq!(first_assignment["amount"], 123.45);
989        assert_eq!(first_assignment["address"], "test_address");
990    }
991
992    #[test]
993    fn test_distribution_tx_data_creation() {
994        let tx_detail = TransactionDetail {
995            txid: "test_txid_123".to_string(),
996            confirmations: 2,
997            blockheight: Some(12345),
998            hex: "020000000001...".to_string(),
999            blockhash: Some("block_hash_hex".to_string()),
1000            blocktime: Some(1640995200),
1001            time: Some(1640995200),
1002            timereceived: Some(1640995180),
1003            details: Some(vec![]),
1004        };
1005
1006        let tx_data = DistributionTxData {
1007            details: tx_detail.clone(),
1008            txid: "test_txid_123".to_string(),
1009        };
1010
1011        assert_eq!(tx_data.txid, "test_txid_123");
1012        assert_eq!(tx_data.details.txid, "test_txid_123");
1013        assert_eq!(tx_data.details.confirmations, 2);
1014
1015        // Test serialization
1016        let json = serde_json::to_string(&tx_data).unwrap();
1017        assert!(json.contains("test_txid_123"));
1018        assert!(json.contains("\"confirmations\":2"));
1019
1020        // Test deserialization
1021        let deserialized: DistributionTxData = serde_json::from_str(&json).unwrap();
1022        assert_eq!(deserialized.txid, tx_data.txid);
1023        assert_eq!(
1024            deserialized.details.confirmations,
1025            tx_data.details.confirmations
1026        );
1027    }
1028
1029    #[test]
1030    fn test_confirm_distribution_request_creation() {
1031        let _tx_detail = TransactionDetail {
1032            txid: "confirm_test_txid".to_string(),
1033            confirmations: 3,
1034            blockheight: Some(54321),
1035            hex: "020000000002...".to_string(),
1036            blockhash: Some("confirm_block_hash".to_string()),
1037            blocktime: Some(1640995300),
1038            time: Some(1640995300),
1039            timereceived: Some(1640995280),
1040            details: Some(vec![]),
1041        };
1042
1043        let tx_data = AmpTxData {
1044            details: serde_json::json!([]),
1045            txid: "confirm_test_txid".to_string(),
1046        };
1047
1048        let change_utxo = Unspent {
1049            txid: "change_txid_123".to_string(),
1050            vout: 1,
1051            amount: 25.0,
1052            asset: "change_asset_id".to_string(),
1053            address: "change_address".to_string(),
1054            spendable: true,
1055            confirmations: Some(3),
1056            scriptpubkey: Some("76a914...88ac".to_string()),
1057            redeemscript: None,
1058            witnessscript: None,
1059            amountblinder: Some(
1060                "1111111111111111111111111111111111111111111111111111111111111111".to_string(),
1061            ),
1062            assetblinder: Some(
1063                "2222222222222222222222222222222222222222222222222222222222222222".to_string(),
1064            ),
1065        };
1066
1067        let confirm_request = ConfirmDistributionRequest {
1068            tx_data,
1069            change_data: vec![change_utxo],
1070        };
1071
1072        assert_eq!(confirm_request.tx_data.txid, "confirm_test_txid");
1073        assert_eq!(confirm_request.change_data.len(), 1);
1074        assert_eq!(confirm_request.change_data[0].txid, "change_txid_123");
1075
1076        // Test serialization
1077        let json = serde_json::to_string(&confirm_request).unwrap();
1078        assert!(json.contains("confirm_test_txid"));
1079        assert!(json.contains("change_txid_123"));
1080        assert!(json.contains("tx_data"));
1081        assert!(json.contains("change_data"));
1082
1083        // Test deserialization
1084        let deserialized: ConfirmDistributionRequest = serde_json::from_str(&json).unwrap();
1085        assert_eq!(deserialized.tx_data.txid, confirm_request.tx_data.txid);
1086        assert_eq!(deserialized.change_data.len(), 1);
1087        assert_eq!(deserialized.change_data[0].txid, "change_txid_123");
1088    }
1089
1090    #[test]
1091    fn test_confirm_distribution_request_serialization_format() {
1092        let _tx_detail = TransactionDetail {
1093            txid: "format_test_txid".to_string(),
1094            confirmations: 2,
1095            blockheight: Some(98765),
1096            hex: "format_test_hex".to_string(),
1097            blockhash: None,
1098            blocktime: None,
1099            time: None,
1100            timereceived: None,
1101            details: Some(vec![]),
1102        };
1103
1104        let tx_data = AmpTxData {
1105            details: serde_json::json!([]),
1106            txid: "format_test_txid".to_string(),
1107        };
1108
1109        let confirm_request = ConfirmDistributionRequest {
1110            tx_data,
1111            change_data: vec![],
1112        };
1113
1114        let json = serde_json::to_string(&confirm_request).unwrap();
1115
1116        // Verify the JSON structure matches the API specification
1117        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1118
1119        assert!(parsed.get("tx_data").is_some());
1120        assert!(parsed.get("change_data").is_some());
1121
1122        let tx_data_obj = &parsed["tx_data"];
1123        assert!(tx_data_obj.get("details").is_some());
1124        assert!(tx_data_obj.get("txid").is_some());
1125        assert_eq!(tx_data_obj["txid"], "format_test_txid");
1126
1127        let change_data_array = parsed["change_data"].as_array().unwrap();
1128        assert_eq!(change_data_array.len(), 0);
1129    }
1130}
1131
1132/// Custom serialization module for Secret<String>
1133pub mod secret_serde {
1134    use super::{Deserialize, Deserializer, Secret, Serialize, Serializer};
1135
1136    /// # Errors
1137    /// Returns an error if serialization fails
1138    pub fn serialize<S>(secret: &Secret<String>, serializer: S) -> Result<S::Ok, S::Error>
1139    where
1140        S: Serializer,
1141    {
1142        use secrecy::ExposeSecret;
1143        secret.expose_secret().serialize(serializer)
1144    }
1145
1146    /// # Errors
1147    /// Returns an error if deserialization fails
1148    pub fn deserialize<'de, D>(deserializer: D) -> Result<Secret<String>, D::Error>
1149    where
1150        D: Deserializer<'de>,
1151    {
1152        let s = String::deserialize(deserializer)?;
1153        Ok(Secret::new(s))
1154    }
1155}