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