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