Skip to main content

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, Clone)]
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/// Asset balance response that includes lost outputs (used for checking before operations)
359#[derive(Debug, Deserialize, Serialize)]
360pub struct AssetBalanceResponse {
361    #[serde(flatten)]
362    pub balance: std::collections::HashMap<String, serde_json::Value>,
363    pub lost_outputs: LostOutputs,
364    #[serde(default)]
365    pub reissuance_lost_outputs: LostOutputs,
366}
367
368#[derive(Debug, Deserialize, Serialize)]
369pub struct AssetLostOutputs {
370    pub lost_outputs: LostOutputs,
371    pub reissuance_lost_outputs: LostOutputs,
372}
373
374#[derive(Debug, Deserialize, Serialize)]
375pub struct AssetSummary {
376    pub asset_id: String,
377    pub reissuance_token_id: Option<String>,
378    pub issued: i64,
379    pub reissued: i64,
380    pub assigned: i64,
381    pub distributed: i64,
382    pub burned: i64,
383    pub blacklisted: i64,
384    pub registered_users: i64,
385    pub active_registered_users: i64,
386    pub active_green_subaccounts: i64,
387    #[serde(rename = "reissuance_tokens")]
388    pub reissuance_tokens: i64,
389}
390
391#[derive(Debug, Deserialize, Serialize)]
392pub struct Utxo {
393    pub txid: String,
394    pub vout: i64,
395    pub asset: String,
396    pub amount: i64,
397    pub registered_user: Option<i64>,
398    pub gaid: Option<String>,
399    pub blacklisted: bool,
400}
401
402#[derive(Debug, Deserialize, Serialize)]
403pub struct Reissuance {
404    pub txid: String,
405    pub vout: i64,
406    pub destination_address: String,
407    pub reissuance_amount: i64,
408    pub confirmed_in_block: String,
409    pub created: String,
410}
411
412#[derive(Debug, Serialize)]
413pub struct ReissueRequest {
414    pub amount_to_reissue: i64,
415}
416
417/// Response from reissue-request endpoint
418#[derive(Debug, Deserialize, Serialize, Clone)]
419pub struct ReissueRequestResponse {
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 reissuance_utxos: Vec<Outpoint>,
427}
428
429/// Request payload for reissue-confirm endpoint
430#[derive(Debug, Serialize)]
431pub struct ReissueConfirmRequest {
432    pub details: serde_json::Value,
433    pub listissuances: Vec<serde_json::Value>,
434    pub reissuance_output: serde_json::Value,
435}
436
437/// Response from reissue-confirm endpoint
438#[derive(Debug, Deserialize, Serialize)]
439pub struct ReissueResponse {
440    pub txid: String,
441    pub vin: i64,
442    pub reissuance_amount: i64,
443}
444
445#[derive(Debug, Serialize)]
446pub struct BurnRequest {
447    pub amount: i64,
448}
449
450#[derive(Debug, Deserialize, Serialize)]
451pub struct BurnCreate {
452    pub command: String,
453    pub min_supported_client_script_version: i64,
454    pub base_url: String,
455    pub asset_uuid: String,
456    pub asset_id: String,
457    pub amount: f64,
458    pub utxos: Vec<Outpoint>,
459}
460
461#[derive(Debug, Deserialize, Serialize)]
462pub struct BurnResponse {
463    pub success: bool,
464    #[serde(default)]
465    pub message: Option<String>,
466}
467
468#[derive(Debug, Serialize)]
469pub struct BurnConfirmRequest {
470    pub tx_data: serde_json::Value,
471    pub change_data: Vec<serde_json::Value>,
472}
473
474#[derive(Debug, Serialize)]
475pub struct SetAssetMemoRequest {
476    pub memo: String,
477}
478
479#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
480#[serde(rename_all = "lowercase")]
481pub enum Permission {
482    View,
483    Receive,
484    Transfer,
485    Assign,
486    Distribute,
487    Reissue,
488    Burn,
489    Acquire,
490    Manage,
491    Permissions,
492}
493
494#[derive(Debug, Deserialize, Serialize)]
495pub struct BroadcastResponse {
496    pub txid: String,
497    pub hex: String,
498}
499
500/// Enhanced token data structure with secure storage and timestamp tracking
501#[derive(Debug, Clone, Serialize, Deserialize)]
502pub struct TokenData {
503    #[serde(with = "secret_serde")]
504    pub token: Secret<String>,
505    pub expires_at: DateTime<Utc>,
506    pub obtained_at: DateTime<Utc>,
507}
508
509impl TokenData {
510    /// Creates a new `TokenData` instance
511    ///
512    /// # Examples
513    /// ```
514    /// # use amp_rs::model::TokenData;
515    /// # use chrono::{Utc, Duration};
516    /// let expires_at = Utc::now() + Duration::hours(24);
517    /// let token_data = TokenData::new("my_token".to_string(), expires_at);
518    /// assert!(!token_data.is_expired());
519    /// ```
520    #[must_use]
521    pub fn new(token: String, expires_at: DateTime<Utc>) -> Self {
522        Self {
523            token: Secret::new(token),
524            expires_at,
525            obtained_at: Utc::now(),
526        }
527    }
528
529    /// Checks if the token is expired
530    ///
531    /// # Examples
532    /// ```
533    /// # use amp_rs::model::TokenData;
534    /// # use chrono::{Utc, Duration};
535    /// // Create an expired token
536    /// let expires_at = Utc::now() - Duration::hours(1);
537    /// let token_data = TokenData::new("expired_token".to_string(), expires_at);
538    /// assert!(token_data.is_expired());
539    ///
540    /// // Create a valid token
541    /// let expires_at = Utc::now() + Duration::hours(1);
542    /// let token_data = TokenData::new("valid_token".to_string(), expires_at);
543    /// assert!(!token_data.is_expired());
544    /// ```
545    #[must_use]
546    pub fn is_expired(&self) -> bool {
547        Utc::now() > self.expires_at
548    }
549
550    /// Checks if the token expires within the given threshold
551    ///
552    /// # Examples
553    /// ```
554    /// # use amp_rs::model::TokenData;
555    /// # use chrono::{Utc, Duration};
556    /// // Token expires in 30 minutes
557    /// let expires_at = Utc::now() + Duration::minutes(30);
558    /// let token_data = TokenData::new("token".to_string(), expires_at);
559    ///
560    /// // Check if it expires within 1 hour
561    /// assert!(token_data.expires_soon(Duration::hours(1)));
562    ///
563    /// // Check if it expires within 15 minutes
564    /// assert!(!token_data.expires_soon(Duration::minutes(15)));
565    /// ```
566    #[must_use]
567    pub fn expires_soon(&self, threshold: Duration) -> bool {
568        Utc::now() + threshold > self.expires_at
569    }
570
571    /// Returns the age of the token
572    ///
573    /// # Examples
574    /// ```no_run
575    /// # use amp_rs::model::TokenData;
576    /// # use chrono::{Utc, Duration};
577    /// let expires_at = Utc::now() + Duration::hours(24);
578    /// let token_data = TokenData::new("token".to_string(), expires_at);
579    ///
580    /// // Token age should be very small (just created)
581    /// let age = token_data.age();
582    /// assert!(age < Duration::seconds(1));
583    /// ```
584    #[must_use]
585    pub fn age(&self) -> Duration {
586        Utc::now() - self.obtained_at
587    }
588}
589
590/// Token information for debugging and monitoring
591#[derive(Debug, Clone)]
592pub struct TokenInfo {
593    pub expires_at: DateTime<Utc>,
594    pub obtained_at: DateTime<Utc>,
595    pub expires_in: Duration,
596    pub age: Duration,
597    pub is_expired: bool,
598    pub expires_soon: bool,
599}
600
601impl From<&TokenData> for TokenInfo {
602    fn from(token_data: &TokenData) -> Self {
603        let now = Utc::now();
604        let expires_in = token_data.expires_at - now;
605        let expires_soon_threshold = Duration::minutes(5);
606
607        Self {
608            expires_at: token_data.expires_at,
609            obtained_at: token_data.obtained_at,
610            expires_in,
611            age: token_data.age(),
612            is_expired: token_data.is_expired(),
613            expires_soon: token_data.expires_soon(expires_soon_threshold),
614        }
615    }
616}
617
618/// Assignment for asset distribution workflow
619#[derive(Debug, Clone, Serialize, Deserialize)]
620pub struct AssetDistributionAssignment {
621    pub user_id: String,
622    pub address: String,
623    pub amount: f64,
624}
625
626/// Assignment for distribution creation API request
627#[derive(Debug, Clone, Serialize, Deserialize)]
628pub struct DistributionAssignmentRequest {
629    pub user_uuid: String,
630    pub amount: f64,
631    pub address: String,
632}
633
634/// Request payload for distribution creation API
635#[derive(Debug, Clone, Serialize, Deserialize)]
636pub struct CreateDistributionRequest {
637    pub assignments: Vec<DistributionAssignmentRequest>,
638}
639
640/// UTXO information from Elements node
641#[derive(Debug, Clone, Serialize, Deserialize)]
642pub struct Unspent {
643    pub txid: String,
644    pub vout: u32,
645    pub amount: f64,
646    pub asset: String,
647    pub address: String,
648    pub spendable: bool,
649    #[serde(skip_serializing_if = "Option::is_none")]
650    pub confirmations: Option<u32>,
651    #[serde(skip_serializing_if = "Option::is_none")]
652    pub scriptpubkey: Option<String>,
653    #[serde(skip_serializing_if = "Option::is_none")]
654    pub redeemscript: Option<String>,
655    #[serde(skip_serializing_if = "Option::is_none")]
656    pub witnessscript: Option<String>,
657    /// Amount blinder for confidential transactions (Elements specific)
658    #[serde(skip_serializing_if = "Option::is_none")]
659    pub amountblinder: Option<String>,
660    /// Asset blinder for confidential transactions (Elements specific)
661    #[serde(skip_serializing_if = "Option::is_none")]
662    pub assetblinder: Option<String>,
663}
664
665/// Transaction details from Elements node (full gettransaction response)
666#[derive(Debug, Clone, Serialize, Deserialize)]
667pub struct TransactionDetail {
668    pub txid: String,
669    pub confirmations: u32,
670    #[serde(skip_serializing_if = "Option::is_none")]
671    pub blockheight: Option<u64>,
672    pub hex: String,
673    #[serde(skip_serializing_if = "Option::is_none")]
674    pub blockhash: Option<String>,
675    #[serde(skip_serializing_if = "Option::is_none")]
676    pub blocktime: Option<i64>,
677    #[serde(skip_serializing_if = "Option::is_none")]
678    pub time: Option<i64>,
679    #[serde(skip_serializing_if = "Option::is_none")]
680    pub timereceived: Option<i64>,
681    /// The details field from gettransaction (array of transaction outputs)
682    #[serde(skip_serializing_if = "Option::is_none")]
683    pub details: Option<Vec<serde_json::Value>>,
684}
685
686/// Transaction output detail from Elements gettransaction details array
687#[derive(Debug, Clone, Serialize, Deserialize)]
688pub struct TransactionOutputDetail {
689    pub account: String,
690    pub address: String,
691    pub category: String,
692    pub amount: f64,
693    pub vout: u32,
694    #[serde(skip_serializing_if = "Option::is_none")]
695    pub fee: Option<f64>,
696    #[serde(skip_serializing_if = "Option::is_none")]
697    pub confirmations: Option<u32>,
698    #[serde(skip_serializing_if = "Option::is_none")]
699    pub blockhash: Option<String>,
700    #[serde(skip_serializing_if = "Option::is_none")]
701    pub blockindex: Option<u32>,
702    #[serde(skip_serializing_if = "Option::is_none")]
703    pub blocktime: Option<i64>,
704    #[serde(skip_serializing_if = "Option::is_none")]
705    pub txid: Option<String>,
706    #[serde(skip_serializing_if = "Option::is_none")]
707    pub time: Option<i64>,
708    #[serde(skip_serializing_if = "Option::is_none")]
709    pub timereceived: Option<i64>,
710    #[serde(skip_serializing_if = "Option::is_none")]
711    pub asset: Option<String>,
712    #[serde(skip_serializing_if = "Option::is_none")]
713    pub assetblinder: Option<String>,
714    #[serde(skip_serializing_if = "Option::is_none")]
715    pub amountblinder: Option<String>,
716}
717
718/// Transaction input for raw transaction creation
719#[derive(Debug, Clone, Serialize, Deserialize)]
720pub struct TxInput {
721    pub txid: String,
722    pub vout: u32,
723    #[serde(skip_serializing_if = "Option::is_none")]
724    pub sequence: Option<u32>,
725}
726
727/// Response from distribution creation API
728#[derive(Debug, Clone, Serialize, Deserialize)]
729pub struct DistributionResponse {
730    pub distribution_uuid: String,
731    pub map_address_amount: std::collections::HashMap<String, f64>,
732    pub map_address_asset: std::collections::HashMap<String, String>,
733    pub asset_id: String,
734}
735
736/// Address information from listreceivedbyaddress RPC
737#[derive(Debug, Clone, Serialize, Deserialize)]
738pub struct ReceivedByAddress {
739    #[serde(skip_serializing_if = "Option::is_none")]
740    pub address: Option<String>,
741    #[serde(skip_serializing_if = "Option::is_none")]
742    pub amount: Option<std::collections::HashMap<String, f64>>,
743    #[serde(skip_serializing_if = "Option::is_none")]
744    pub confirmations: Option<u32>,
745    #[serde(skip_serializing_if = "Option::is_none")]
746    pub label: Option<String>,
747    #[serde(skip_serializing_if = "Option::is_none")]
748    pub txids: Option<Vec<String>>,
749}
750
751/// Transaction data for distribution confirmation
752#[derive(Debug, Clone, Serialize, Deserialize)]
753pub struct DistributionTxData {
754    pub details: TransactionDetail,
755    pub txid: String,
756}
757
758/// Transaction data for AMP API confirmation (matches Python implementation)
759#[derive(Debug, Clone, Serialize, Deserialize)]
760pub struct AmpTxData {
761    /// The details field from gettransaction (array of transaction outputs)
762    pub details: serde_json::Value,
763    pub txid: String,
764}
765
766/// Request payload for distribution confirmation API
767#[derive(Debug, Clone, Serialize, Deserialize)]
768pub struct ConfirmDistributionRequest {
769    pub tx_data: AmpTxData,
770    pub change_data: Vec<Unspent>,
771}
772
773#[cfg(test)]
774mod tests {
775    use super::*;
776
777    #[test]
778    fn test_asset_distribution_assignment_creation() {
779        let assignment = AssetDistributionAssignment {
780            user_id: "user123".to_string(),
781            address: "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
782            amount: 100.5,
783        };
784
785        assert_eq!(assignment.user_id, "user123");
786        assert_eq!(
787            assignment.address,
788            "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq"
789        );
790        assert_eq!(assignment.amount, 100.5);
791    }
792
793    #[test]
794    fn test_asset_distribution_assignment_serialization() {
795        let assignment = AssetDistributionAssignment {
796            user_id: "user123".to_string(),
797            address: "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
798            amount: 100.5,
799        };
800
801        // Test serialization
802        let json = serde_json::to_string(&assignment).unwrap();
803        assert!(json.contains("user123"));
804        assert!(json.contains("lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq"));
805        assert!(json.contains("100.5"));
806
807        // Test deserialization
808        let deserialized: AssetDistributionAssignment = serde_json::from_str(&json).unwrap();
809        assert_eq!(deserialized.user_id, assignment.user_id);
810        assert_eq!(deserialized.address, assignment.address);
811        assert_eq!(deserialized.amount, assignment.amount);
812    }
813
814    #[test]
815    fn test_asset_distribution_assignment_clone() {
816        let assignment = AssetDistributionAssignment {
817            user_id: "user123".to_string(),
818            address: "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
819            amount: 100.5,
820        };
821
822        let cloned = assignment.clone();
823        assert_eq!(assignment.user_id, cloned.user_id);
824        assert_eq!(assignment.address, cloned.address);
825        assert_eq!(assignment.amount, cloned.amount);
826    }
827
828    #[test]
829    fn test_unspent_creation_and_serialization() {
830        let unspent = Unspent {
831            txid: "abc123def456".to_string(),
832            vout: 1,
833            amount: 50.0,
834            asset: "asset_id_hex".to_string(),
835            address: "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
836            spendable: true,
837            confirmations: Some(6),
838            scriptpubkey: Some("76a914...88ac".to_string()),
839            redeemscript: None,
840            witnessscript: None,
841            amountblinder: Some(
842                "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
843            ),
844            assetblinder: Some(
845                "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210".to_string(),
846            ),
847        };
848
849        // Test serialization
850        let json = serde_json::to_string(&unspent).unwrap();
851        assert!(json.contains("abc123def456"));
852        assert!(json.contains("50.0"));
853        assert!(json.contains("asset_id_hex"));
854
855        // Test deserialization
856        let deserialized: Unspent = serde_json::from_str(&json).unwrap();
857        assert_eq!(deserialized.txid, unspent.txid);
858        assert_eq!(deserialized.vout, unspent.vout);
859        assert_eq!(deserialized.amount, unspent.amount);
860        assert_eq!(deserialized.asset, unspent.asset);
861        assert_eq!(deserialized.confirmations, unspent.confirmations);
862    }
863
864    #[test]
865    fn test_transaction_detail_creation() {
866        let tx_detail = TransactionDetail {
867            txid: "def456abc123".to_string(),
868            confirmations: 3,
869            blockheight: Some(12345),
870            hex: "020000000001...".to_string(),
871            blockhash: Some("block_hash_hex".to_string()),
872            details: Some(vec![]),
873            blocktime: Some(1640995200),
874            time: Some(1640995200),
875            timereceived: Some(1640995180),
876        };
877
878        assert_eq!(tx_detail.txid, "def456abc123");
879        assert_eq!(tx_detail.confirmations, 3);
880        assert_eq!(tx_detail.blockheight, Some(12345));
881
882        // Test serialization
883        let json = serde_json::to_string(&tx_detail).unwrap();
884        assert!(json.contains("def456abc123"));
885        assert!(json.contains("\"confirmations\":3"));
886    }
887
888    #[test]
889    fn test_tx_input_creation() {
890        let tx_input = TxInput {
891            txid: "input_txid_123".to_string(),
892            vout: 2,
893            sequence: Some(0xffffffff),
894        };
895
896        assert_eq!(tx_input.txid, "input_txid_123");
897        assert_eq!(tx_input.vout, 2);
898        assert_eq!(tx_input.sequence, Some(0xffffffff));
899
900        // Test serialization
901        let json = serde_json::to_string(&tx_input).unwrap();
902        assert!(json.contains("input_txid_123"));
903        assert!(json.contains("\"vout\":2"));
904        assert!(json.contains("4294967295")); // 0xffffffff in decimal
905    }
906
907    #[test]
908    fn test_distribution_response_creation() {
909        let mut map_address_amount = HashMap::new();
910        map_address_amount.insert("address1".to_string(), 100.0);
911        map_address_amount.insert("address2".to_string(), 50.0);
912
913        let mut map_address_asset = HashMap::new();
914        map_address_asset.insert("address1".to_string(), "asset_id_1".to_string());
915        map_address_asset.insert("address2".to_string(), "asset_id_1".to_string());
916
917        let distribution_response = DistributionResponse {
918            distribution_uuid: "dist_uuid_123".to_string(),
919            map_address_amount,
920            map_address_asset,
921            asset_id: "main_asset_id".to_string(),
922        };
923
924        assert_eq!(distribution_response.distribution_uuid, "dist_uuid_123");
925        assert_eq!(distribution_response.asset_id, "main_asset_id");
926        assert_eq!(distribution_response.map_address_amount.len(), 2);
927        assert_eq!(distribution_response.map_address_asset.len(), 2);
928
929        // Test serialization
930        let json = serde_json::to_string(&distribution_response).unwrap();
931        assert!(json.contains("dist_uuid_123"));
932        assert!(json.contains("main_asset_id"));
933        assert!(json.contains("address1"));
934        assert!(json.contains("100.0"));
935    }
936
937    #[test]
938    fn test_distribution_assignment_request_creation() {
939        let assignment_request = DistributionAssignmentRequest {
940            user_uuid: "user_uuid_123".to_string(),
941            amount: 150.0,
942            address: "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq".to_string(),
943        };
944
945        assert_eq!(assignment_request.user_uuid, "user_uuid_123");
946        assert_eq!(assignment_request.amount, 150.0);
947        assert_eq!(
948            assignment_request.address,
949            "lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq"
950        );
951
952        // Test serialization
953        let json = serde_json::to_string(&assignment_request).unwrap();
954        assert!(json.contains("user_uuid_123"));
955        assert!(json.contains("150.0"));
956        assert!(json.contains("lq1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f9lq"));
957
958        // Test deserialization
959        let deserialized: DistributionAssignmentRequest = serde_json::from_str(&json).unwrap();
960        assert_eq!(deserialized.user_uuid, assignment_request.user_uuid);
961        assert_eq!(deserialized.amount, assignment_request.amount);
962        assert_eq!(deserialized.address, assignment_request.address);
963    }
964
965    #[test]
966    fn test_create_distribution_request_creation() {
967        let assignments = vec![
968            DistributionAssignmentRequest {
969                user_uuid: "user1".to_string(),
970                amount: 100.0,
971                address: "address1".to_string(),
972            },
973            DistributionAssignmentRequest {
974                user_uuid: "user2".to_string(),
975                amount: 50.0,
976                address: "address2".to_string(),
977            },
978        ];
979
980        let create_request = CreateDistributionRequest {
981            assignments: assignments.clone(),
982        };
983
984        assert_eq!(create_request.assignments.len(), 2);
985        assert_eq!(create_request.assignments[0].user_uuid, "user1");
986        assert_eq!(create_request.assignments[1].amount, 50.0);
987
988        // Test serialization
989        let json = serde_json::to_string(&create_request).unwrap();
990        assert!(json.contains("user1"));
991        assert!(json.contains("user2"));
992        assert!(json.contains("100.0"));
993        assert!(json.contains("50.0"));
994        assert!(json.contains("assignments"));
995
996        // Test deserialization
997        let deserialized: CreateDistributionRequest = serde_json::from_str(&json).unwrap();
998        assert_eq!(deserialized.assignments.len(), 2);
999        assert_eq!(deserialized.assignments[0].user_uuid, "user1");
1000        assert_eq!(deserialized.assignments[1].user_uuid, "user2");
1001    }
1002
1003    #[test]
1004    fn test_distribution_request_serialization_format() {
1005        let assignment = DistributionAssignmentRequest {
1006            user_uuid: "test_user".to_string(),
1007            amount: 123.45,
1008            address: "test_address".to_string(),
1009        };
1010
1011        let request = CreateDistributionRequest {
1012            assignments: vec![assignment],
1013        };
1014
1015        let json = serde_json::to_string(&request).unwrap();
1016
1017        // Verify the JSON structure matches the API specification
1018        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1019
1020        assert!(parsed.get("assignments").is_some());
1021        let assignments_array = parsed["assignments"].as_array().unwrap();
1022        assert_eq!(assignments_array.len(), 1);
1023
1024        let first_assignment = &assignments_array[0];
1025        assert_eq!(first_assignment["user_uuid"], "test_user");
1026        assert_eq!(first_assignment["amount"], 123.45);
1027        assert_eq!(first_assignment["address"], "test_address");
1028    }
1029
1030    #[test]
1031    fn test_distribution_tx_data_creation() {
1032        let tx_detail = TransactionDetail {
1033            txid: "test_txid_123".to_string(),
1034            confirmations: 2,
1035            blockheight: Some(12345),
1036            hex: "020000000001...".to_string(),
1037            blockhash: Some("block_hash_hex".to_string()),
1038            blocktime: Some(1640995200),
1039            time: Some(1640995200),
1040            timereceived: Some(1640995180),
1041            details: Some(vec![]),
1042        };
1043
1044        let tx_data = DistributionTxData {
1045            details: tx_detail.clone(),
1046            txid: "test_txid_123".to_string(),
1047        };
1048
1049        assert_eq!(tx_data.txid, "test_txid_123");
1050        assert_eq!(tx_data.details.txid, "test_txid_123");
1051        assert_eq!(tx_data.details.confirmations, 2);
1052
1053        // Test serialization
1054        let json = serde_json::to_string(&tx_data).unwrap();
1055        assert!(json.contains("test_txid_123"));
1056        assert!(json.contains("\"confirmations\":2"));
1057
1058        // Test deserialization
1059        let deserialized: DistributionTxData = serde_json::from_str(&json).unwrap();
1060        assert_eq!(deserialized.txid, tx_data.txid);
1061        assert_eq!(
1062            deserialized.details.confirmations,
1063            tx_data.details.confirmations
1064        );
1065    }
1066
1067    #[test]
1068    fn test_confirm_distribution_request_creation() {
1069        let _tx_detail = TransactionDetail {
1070            txid: "confirm_test_txid".to_string(),
1071            confirmations: 3,
1072            blockheight: Some(54321),
1073            hex: "020000000002...".to_string(),
1074            blockhash: Some("confirm_block_hash".to_string()),
1075            blocktime: Some(1640995300),
1076            time: Some(1640995300),
1077            timereceived: Some(1640995280),
1078            details: Some(vec![]),
1079        };
1080
1081        let tx_data = AmpTxData {
1082            details: serde_json::json!([]),
1083            txid: "confirm_test_txid".to_string(),
1084        };
1085
1086        let change_utxo = Unspent {
1087            txid: "change_txid_123".to_string(),
1088            vout: 1,
1089            amount: 25.0,
1090            asset: "change_asset_id".to_string(),
1091            address: "change_address".to_string(),
1092            spendable: true,
1093            confirmations: Some(3),
1094            scriptpubkey: Some("76a914...88ac".to_string()),
1095            redeemscript: None,
1096            witnessscript: None,
1097            amountblinder: Some(
1098                "1111111111111111111111111111111111111111111111111111111111111111".to_string(),
1099            ),
1100            assetblinder: Some(
1101                "2222222222222222222222222222222222222222222222222222222222222222".to_string(),
1102            ),
1103        };
1104
1105        let confirm_request = ConfirmDistributionRequest {
1106            tx_data,
1107            change_data: vec![change_utxo],
1108        };
1109
1110        assert_eq!(confirm_request.tx_data.txid, "confirm_test_txid");
1111        assert_eq!(confirm_request.change_data.len(), 1);
1112        assert_eq!(confirm_request.change_data[0].txid, "change_txid_123");
1113
1114        // Test serialization
1115        let json = serde_json::to_string(&confirm_request).unwrap();
1116        assert!(json.contains("confirm_test_txid"));
1117        assert!(json.contains("change_txid_123"));
1118        assert!(json.contains("tx_data"));
1119        assert!(json.contains("change_data"));
1120
1121        // Test deserialization
1122        let deserialized: ConfirmDistributionRequest = serde_json::from_str(&json).unwrap();
1123        assert_eq!(deserialized.tx_data.txid, confirm_request.tx_data.txid);
1124        assert_eq!(deserialized.change_data.len(), 1);
1125        assert_eq!(deserialized.change_data[0].txid, "change_txid_123");
1126    }
1127
1128    #[test]
1129    fn test_confirm_distribution_request_serialization_format() {
1130        let _tx_detail = TransactionDetail {
1131            txid: "format_test_txid".to_string(),
1132            confirmations: 2,
1133            blockheight: Some(98765),
1134            hex: "format_test_hex".to_string(),
1135            blockhash: None,
1136            blocktime: None,
1137            time: None,
1138            timereceived: None,
1139            details: Some(vec![]),
1140        };
1141
1142        let tx_data = AmpTxData {
1143            details: serde_json::json!([]),
1144            txid: "format_test_txid".to_string(),
1145        };
1146
1147        let confirm_request = ConfirmDistributionRequest {
1148            tx_data,
1149            change_data: vec![],
1150        };
1151
1152        let json = serde_json::to_string(&confirm_request).unwrap();
1153
1154        // Verify the JSON structure matches the API specification
1155        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1156
1157        assert!(parsed.get("tx_data").is_some());
1158        assert!(parsed.get("change_data").is_some());
1159
1160        let tx_data_obj = &parsed["tx_data"];
1161        assert!(tx_data_obj.get("details").is_some());
1162        assert!(tx_data_obj.get("txid").is_some());
1163        assert_eq!(tx_data_obj["txid"], "format_test_txid");
1164
1165        let change_data_array = parsed["change_data"].as_array().unwrap();
1166        assert_eq!(change_data_array.len(), 0);
1167    }
1168}
1169
1170/// Custom serialization module for Secret<String>
1171pub mod secret_serde {
1172    use super::{Deserialize, Deserializer, Secret, Serialize, Serializer};
1173
1174    /// # Errors
1175    /// Returns an error if serialization fails
1176    pub fn serialize<S>(secret: &Secret<String>, serializer: S) -> Result<S::Ok, S::Error>
1177    where
1178        S: Serializer,
1179    {
1180        use secrecy::ExposeSecret;
1181        secret.expose_secret().serialize(serializer)
1182    }
1183
1184    /// # Errors
1185    /// Returns an error if deserialization fails
1186    pub fn deserialize<'de, D>(deserializer: D) -> Result<Secret<String>, D::Error>
1187    where
1188        D: Deserializer<'de>,
1189    {
1190        let s = String::deserialize(deserializer)?;
1191        Ok(Secret::new(s))
1192    }
1193}