Skip to main content

circle_developer_controlled_wallets/models/
transaction.rs

1//! Transaction resource models for the Circle Developer-Controlled Wallets API.
2//!
3//! Contains request parameters and response types for transaction management
4//! endpoints including transfers, contract execution, signing, and fee estimation.
5
6use super::common::{Blockchain, CustodyType, FeeLevel, TransactionFee};
7
8/// Transaction lifecycle state.
9#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
10#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
11pub enum TransactionState {
12    /// Transaction was cancelled.
13    Cancelled,
14    /// Transaction has received required confirmations.
15    Confirmed,
16    /// Transaction is complete.
17    Complete,
18    /// Transaction was denied by screening.
19    Denied,
20    /// Transaction failed on-chain.
21    Failed,
22    /// Transaction was just initiated.
23    Initiated,
24    /// Transaction cleared pre-chain checks.
25    Cleared,
26    /// Transaction is queued for broadcast.
27    Queued,
28    /// Transaction was sent to the network.
29    Sent,
30    /// Transaction is stuck (e.g. low gas).
31    Stuck,
32}
33
34/// Transaction directional type.
35#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
36#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
37pub enum TransactionType {
38    /// Incoming transaction.
39    Inbound,
40    /// Outgoing transaction.
41    Outbound,
42}
43
44/// The operation performed by the transaction.
45#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
46#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
47pub enum Operation {
48    /// Token or native asset transfer.
49    Transfer,
50    /// Smart contract function call.
51    ContractExecution,
52    /// Smart contract deployment.
53    ContractDeployment,
54}
55
56/// Risk score label.
57#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
58#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
59pub enum RiskScore {
60    /// Risk is unknown.
61    Unknown,
62    /// Low risk.
63    Low,
64    /// Medium risk.
65    Medium,
66    /// High risk.
67    High,
68    /// Severe risk.
69    Severe,
70    /// Address is on a blocklist.
71    Blocklist,
72}
73
74/// Risk category label.
75#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
76#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
77pub enum RiskCategory {
78    /// Sanctions-related risk.
79    Sanctions,
80    /// Child sexual abuse material.
81    Csam,
82    /// Illicit behavior.
83    IllicitBehavior,
84    /// Gambling.
85    Gambling,
86    /// Terrorist financing.
87    TerroristFinancing,
88    /// Unsupported.
89    Unsupported,
90    /// Frozen address.
91    Frozen,
92    /// Other risk.
93    Other,
94    /// High-risk industry.
95    HighRiskIndustry,
96    /// Politically exposed person.
97    Pep,
98    /// Trusted entity.
99    Trusted,
100    /// Hacking.
101    Hacking,
102    /// Human trafficking.
103    HumanTrafficking,
104    /// Special measures.
105    SpecialMeasures,
106}
107
108/// Risk exposure type.
109#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
110#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
111pub enum RiskType {
112    /// Direct ownership.
113    Ownership,
114    /// Counterparty relationship.
115    Counterparty,
116    /// Indirect relationship.
117    Indirect,
118}
119
120/// Recommended action for a screened transaction.
121#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
122#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
123pub enum RiskAction {
124    /// Approve the transaction.
125    Approve,
126    /// Flag for manual review.
127    Review,
128    /// Freeze the wallet.
129    FreezeWallet,
130    /// Deny the transaction.
131    Deny,
132}
133
134/// An individual risk signal from the screening evaluation.
135#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
136#[serde(rename_all = "camelCase")]
137pub struct RiskSignal {
138    /// Risk signal source (e.g. screening provider name).
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub source: Option<String>,
141    /// Source-specific value.
142    #[serde(skip_serializing_if = "Option::is_none")]
143    pub source_value: Option<String>,
144    /// Risk score for this signal.
145    #[serde(skip_serializing_if = "Option::is_none")]
146    pub risk_score: Option<RiskScore>,
147    /// Risk categories for this signal.
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub risk_categories: Option<Vec<RiskCategory>>,
150    /// Risk exposure type.
151    #[serde(rename = "type")]
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub risk_type: Option<RiskType>,
154}
155
156/// Transaction screening and compliance decision.
157#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
158#[serde(rename_all = "camelCase")]
159pub struct TransactionScreeningDecision {
160    /// ISO-8601 timestamp when screening occurred.
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub screening_date: Option<String>,
163    /// Name of the compliance rule that triggered.
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub rule_name: Option<String>,
166    /// Recommended actions.
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub actions: Option<Vec<RiskAction>>,
169    /// Reasons for the decision (risk signals).
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub reasons: Option<Vec<RiskSignal>>,
172}
173
174/// A developer-controlled transaction resource.
175#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
176#[serde(rename_all = "camelCase")]
177pub struct Transaction {
178    /// Unique transaction ID.
179    pub id: String,
180    /// Current lifecycle state.
181    pub state: TransactionState,
182    /// Blockchain network.
183    #[serde(skip_serializing_if = "Option::is_none")]
184    pub blockchain: Option<Blockchain>,
185    /// Transaction direction.
186    #[serde(skip_serializing_if = "Option::is_none")]
187    pub transaction_type: Option<TransactionType>,
188    /// ISO-8601 creation timestamp.
189    pub create_date: String,
190    /// ISO-8601 last-update timestamp.
191    pub update_date: String,
192    /// ABI function signature (contract executions).
193    #[serde(skip_serializing_if = "Option::is_none")]
194    pub abi_function_signature: Option<String>,
195    /// ABI parameters (contract executions).
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub abi_parameters: Option<Vec<serde_json::Value>>,
198    /// Token amounts being transferred.
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub amounts: Option<Vec<String>>,
201    /// Transfer amount in USD.
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub amount_in_usd: Option<String>,
204    /// Block hash of the confirming block.
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub block_hash: Option<String>,
207    /// Block height of the confirming block.
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub block_height: Option<u64>,
210    /// Contract address (for contract executions/deployments).
211    #[serde(skip_serializing_if = "Option::is_none")]
212    pub contract_address: Option<String>,
213    /// Custody type.
214    #[serde(skip_serializing_if = "Option::is_none")]
215    pub custody_type: Option<CustodyType>,
216    /// Destination wallet address.
217    #[serde(skip_serializing_if = "Option::is_none")]
218    pub destination_address: Option<String>,
219    /// Human-readable error reason.
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub error_reason: Option<String>,
222    /// Detailed error description.
223    #[serde(skip_serializing_if = "Option::is_none")]
224    pub error_details: Option<String>,
225    /// Fee estimate used at submission time.
226    #[serde(skip_serializing_if = "Option::is_none")]
227    pub estimated_fee: Option<TransactionFee>,
228    /// Fee priority level.
229    #[serde(skip_serializing_if = "Option::is_none")]
230    pub fee_level: Option<FeeLevel>,
231    /// ISO-8601 first confirmation timestamp.
232    #[serde(skip_serializing_if = "Option::is_none")]
233    pub first_confirm_date: Option<String>,
234    /// Actual network fee paid.
235    #[serde(skip_serializing_if = "Option::is_none")]
236    pub network_fee: Option<String>,
237    /// Network fee denominated in USD.
238    #[serde(skip_serializing_if = "Option::is_none")]
239    pub network_fee_in_usd: Option<String>,
240    /// NFT token IDs being transferred.
241    #[serde(skip_serializing_if = "Option::is_none")]
242    pub nfts: Option<Vec<String>>,
243    /// Operation type.
244    #[serde(skip_serializing_if = "Option::is_none")]
245    pub operation: Option<Operation>,
246    /// External reference ID.
247    #[serde(skip_serializing_if = "Option::is_none")]
248    pub ref_id: Option<String>,
249    /// Source wallet address.
250    #[serde(skip_serializing_if = "Option::is_none")]
251    pub source_address: Option<String>,
252    /// Token ID for the asset being transferred.
253    #[serde(skip_serializing_if = "Option::is_none")]
254    pub token_id: Option<String>,
255    /// On-chain transaction hash.
256    #[serde(skip_serializing_if = "Option::is_none")]
257    pub tx_hash: Option<String>,
258    /// User ID (user-controlled wallets).
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub user_id: Option<String>,
261    /// Source wallet ID.
262    #[serde(skip_serializing_if = "Option::is_none")]
263    pub wallet_id: Option<String>,
264    /// Compliance screening evaluation result.
265    #[serde(skip_serializing_if = "Option::is_none")]
266    pub transaction_screening_evaluation: Option<TransactionScreeningDecision>,
267}
268
269/// Inner data of a list-transactions response.
270#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
271#[serde(rename_all = "camelCase")]
272pub struct TransactionsData {
273    /// Transactions matching the query.
274    pub transactions: Vec<Transaction>,
275}
276
277/// Response wrapper for the list-transactions endpoint.
278#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
279pub struct Transactions {
280    /// Response data.
281    pub data: TransactionsData,
282}
283
284/// Inner data of a single transaction response.
285#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
286#[serde(rename_all = "camelCase")]
287pub struct TransactionData {
288    /// The transaction.
289    pub transaction: Transaction,
290}
291
292/// Response wrapper for get/create/cancel/accelerate transaction endpoints.
293#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
294pub struct TransactionResponse {
295    /// Response data.
296    pub data: TransactionData,
297}
298
299/// Query parameters for the list-transactions endpoint.
300#[derive(Debug, Default, Clone, serde::Serialize)]
301#[serde(rename_all = "camelCase")]
302pub struct ListTransactionsParams {
303    /// Filter by blockchain.
304    #[serde(skip_serializing_if = "Option::is_none")]
305    pub blockchain: Option<Blockchain>,
306    /// Filter by custody type.
307    #[serde(skip_serializing_if = "Option::is_none")]
308    pub custody_type: Option<CustodyType>,
309    /// Filter by destination address.
310    #[serde(skip_serializing_if = "Option::is_none")]
311    pub destination_address: Option<String>,
312    /// Include all transactions.
313    #[serde(skip_serializing_if = "Option::is_none")]
314    pub include_all: Option<bool>,
315    /// Filter by operation type.
316    #[serde(skip_serializing_if = "Option::is_none")]
317    pub operation: Option<Operation>,
318    /// Filter by external reference ID.
319    #[serde(skip_serializing_if = "Option::is_none")]
320    pub ref_id: Option<String>,
321    /// Filter by source address.
322    #[serde(skip_serializing_if = "Option::is_none")]
323    pub source_address: Option<String>,
324    /// Filter by state.
325    #[serde(skip_serializing_if = "Option::is_none")]
326    pub state: Option<TransactionState>,
327    /// Filter by token contract address.
328    #[serde(skip_serializing_if = "Option::is_none")]
329    pub token_address: Option<String>,
330    /// Filter by transaction hash.
331    #[serde(skip_serializing_if = "Option::is_none")]
332    pub transaction_hash: Option<String>,
333    /// Filter by transaction type.
334    #[serde(skip_serializing_if = "Option::is_none")]
335    pub transaction_type: Option<TransactionType>,
336    /// Filter by specific wallet IDs (comma-separated).
337    #[serde(skip_serializing_if = "Option::is_none")]
338    pub wallet_ids: Option<String>,
339    /// Start of date-time range (ISO-8601).
340    #[serde(skip_serializing_if = "Option::is_none")]
341    pub from: Option<String>,
342    /// End of date-time range (ISO-8601).
343    #[serde(skip_serializing_if = "Option::is_none")]
344    pub to: Option<String>,
345    /// Cursor to page before.
346    #[serde(skip_serializing_if = "Option::is_none")]
347    pub page_before: Option<String>,
348    /// Cursor to page after.
349    #[serde(skip_serializing_if = "Option::is_none")]
350    pub page_after: Option<String>,
351    /// Page size (1–50).
352    #[serde(skip_serializing_if = "Option::is_none")]
353    pub page_size: Option<u32>,
354}
355
356/// Request body for creating a developer-controlled transfer transaction.
357#[derive(Debug, Clone, serde::Serialize)]
358#[serde(rename_all = "camelCase")]
359pub struct CreateTransferTxRequest {
360    /// Idempotency key (UUID).
361    pub idempotency_key: String,
362    /// Encrypted entity secret ciphertext.
363    pub entity_secret_ciphertext: String,
364    /// Source wallet ID.
365    pub wallet_id: String,
366    /// Blockchain (required for native asset transfers).
367    #[serde(skip_serializing_if = "Option::is_none")]
368    pub blockchain: Option<Blockchain>,
369    /// Token ID for the asset being transferred.
370    #[serde(skip_serializing_if = "Option::is_none")]
371    pub token_id: Option<String>,
372    /// Destination address.
373    pub destination_address: String,
374    /// Token amounts to transfer.
375    #[serde(skip_serializing_if = "Option::is_none")]
376    pub amounts: Option<Vec<String>>,
377    /// NFT token IDs to transfer.
378    #[serde(skip_serializing_if = "Option::is_none")]
379    pub nft_token_ids: Option<Vec<String>>,
380    /// External reference ID.
381    #[serde(skip_serializing_if = "Option::is_none")]
382    pub ref_id: Option<String>,
383    /// Fee priority level.
384    #[serde(skip_serializing_if = "Option::is_none")]
385    pub fee_level: Option<FeeLevel>,
386    /// Custom gas limit.
387    #[serde(skip_serializing_if = "Option::is_none")]
388    pub gas_limit: Option<String>,
389    /// Custom gas price.
390    #[serde(skip_serializing_if = "Option::is_none")]
391    pub gas_price: Option<String>,
392    /// Max fee per gas (EIP-1559).
393    #[serde(skip_serializing_if = "Option::is_none")]
394    pub max_fee: Option<String>,
395    /// Max priority fee per gas (EIP-1559).
396    #[serde(skip_serializing_if = "Option::is_none")]
397    pub priority_fee: Option<String>,
398}
399
400/// Request body for creating a contract execution transaction.
401#[derive(Debug, Clone, serde::Serialize)]
402#[serde(rename_all = "camelCase")]
403pub struct CreateContractExecutionTxRequest {
404    /// Idempotency key (UUID).
405    pub idempotency_key: String,
406    /// Encrypted entity secret ciphertext.
407    pub entity_secret_ciphertext: String,
408    /// Source wallet ID.
409    pub wallet_id: String,
410    /// Blockchain network for the call.
411    #[serde(skip_serializing_if = "Option::is_none")]
412    pub blockchain: Option<Blockchain>,
413    /// Contract address to call.
414    pub contract_address: String,
415    /// ABI function signature (e.g. `transfer(address,uint256)`).
416    #[serde(skip_serializing_if = "Option::is_none")]
417    pub abi_function_signature: Option<String>,
418    /// ABI-encoded parameters.
419    #[serde(skip_serializing_if = "Option::is_none")]
420    pub abi_parameters: Option<Vec<serde_json::Value>>,
421    /// Raw call data (alternative to abi_function_signature + abi_parameters).
422    #[serde(skip_serializing_if = "Option::is_none")]
423    pub call_data: Option<String>,
424    /// Fee priority level.
425    #[serde(skip_serializing_if = "Option::is_none")]
426    pub fee_level: Option<FeeLevel>,
427    /// Custom gas limit.
428    #[serde(skip_serializing_if = "Option::is_none")]
429    pub gas_limit: Option<String>,
430    /// Max fee per gas (EIP-1559).
431    #[serde(skip_serializing_if = "Option::is_none")]
432    pub max_fee: Option<String>,
433    /// Max priority fee per gas (EIP-1559).
434    #[serde(skip_serializing_if = "Option::is_none")]
435    pub priority_fee: Option<String>,
436    /// External reference ID.
437    #[serde(skip_serializing_if = "Option::is_none")]
438    pub ref_id: Option<String>,
439    /// ETH value to send with the call.
440    #[serde(skip_serializing_if = "Option::is_none")]
441    pub amount: Option<String>,
442}
443
444/// Request body for cancelling a transaction.
445#[derive(Debug, Clone, serde::Serialize)]
446#[serde(rename_all = "camelCase")]
447pub struct CancelTxRequest {
448    /// Encrypted entity secret ciphertext.
449    pub entity_secret_ciphertext: String,
450}
451
452/// Request body for accelerating a transaction.
453#[derive(Debug, Clone, serde::Serialize)]
454#[serde(rename_all = "camelCase")]
455pub struct AccelerateTxRequest {
456    /// Encrypted entity secret ciphertext.
457    pub entity_secret_ciphertext: String,
458}
459
460/// Request body for validating a blockchain address.
461#[derive(Debug, Clone, serde::Serialize)]
462#[serde(rename_all = "camelCase")]
463pub struct ValidateAddressRequest {
464    /// Blockchain to validate against.
465    pub blockchain: Blockchain,
466    /// Address string to validate.
467    pub address: String,
468}
469
470/// Inner data of the validate-address response.
471#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
472#[serde(rename_all = "camelCase")]
473pub struct ValidateAddressData {
474    /// Whether the address is valid for the given blockchain.
475    pub is_valid: bool,
476}
477
478/// Response wrapper for the validate-address endpoint.
479#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
480pub struct ValidateAddressResponse {
481    /// Response data.
482    pub data: ValidateAddressData,
483}
484
485/// Request body for estimating transfer fees.
486#[derive(Debug, Clone, Default, serde::Serialize)]
487#[serde(rename_all = "camelCase")]
488pub struct EstimateTransferFeeRequest {
489    /// Source wallet address.
490    #[serde(skip_serializing_if = "Option::is_none")]
491    pub source_address: Option<String>,
492    /// Blockchain network.
493    #[serde(skip_serializing_if = "Option::is_none")]
494    pub blockchain: Option<Blockchain>,
495    /// Destination address.
496    #[serde(skip_serializing_if = "Option::is_none")]
497    pub destination_address: Option<String>,
498    /// Token amounts to estimate fees for.
499    #[serde(skip_serializing_if = "Option::is_none")]
500    pub amounts: Option<Vec<String>>,
501    /// NFT token IDs.
502    #[serde(skip_serializing_if = "Option::is_none")]
503    pub nfts: Option<Vec<String>>,
504    /// Token ID.
505    #[serde(skip_serializing_if = "Option::is_none")]
506    pub token_id: Option<String>,
507    /// Fee priority level.
508    #[serde(skip_serializing_if = "Option::is_none")]
509    pub fee_level: Option<FeeLevel>,
510    /// Custom gas limit.
511    #[serde(skip_serializing_if = "Option::is_none")]
512    pub gas_limit: Option<String>,
513    /// Custom gas price.
514    #[serde(skip_serializing_if = "Option::is_none")]
515    pub gas_price: Option<String>,
516}
517
518/// Fee estimate breakdown for low, medium, and high priority.
519#[derive(Debug, Clone, serde::Deserialize)]
520#[serde(rename_all = "camelCase")]
521pub struct EstimateFeeData {
522    /// Low-priority fee estimate.
523    #[serde(skip_serializing_if = "Option::is_none")]
524    pub low: Option<TransactionFee>,
525    /// Medium-priority fee estimate.
526    #[serde(skip_serializing_if = "Option::is_none")]
527    pub medium: Option<TransactionFee>,
528    /// High-priority fee estimate.
529    #[serde(skip_serializing_if = "Option::is_none")]
530    pub high: Option<TransactionFee>,
531    /// Call gas limit for SCA transactions.
532    #[serde(skip_serializing_if = "Option::is_none")]
533    pub call_gas_limit: Option<String>,
534    /// Verification gas limit for SCA transactions.
535    #[serde(skip_serializing_if = "Option::is_none")]
536    pub verification_gas_limit: Option<String>,
537    /// Pre-verification gas for SCA transactions.
538    #[serde(skip_serializing_if = "Option::is_none")]
539    pub pre_verification_gas: Option<String>,
540}
541
542/// Response wrapper for fee estimation endpoints.
543#[derive(Debug, Clone, serde::Deserialize)]
544pub struct EstimateFeeResponse {
545    /// Response data.
546    pub data: EstimateFeeData,
547}
548
549#[cfg(test)]
550mod tests {
551    use super::*;
552
553    #[test]
554    fn transaction_response_deserializes() -> Result<(), Box<dyn std::error::Error>> {
555        let json = r#"{
556            "data": {
557                "transaction": {
558                    "id": "tx-id-1",
559                    "state": "COMPLETE",
560                    "blockchain": "ETH",
561                    "createDate": "2024-01-01T00:00:00Z",
562                    "updateDate": "2024-01-02T00:00:00Z",
563                    "txHash": "0xdeadbeef",
564                    "operation": "TRANSFER"
565                }
566            }
567        }"#;
568        let resp: TransactionResponse = serde_json::from_str(json)?;
569        assert_eq!(resp.data.transaction.id, "tx-id-1");
570        assert_eq!(resp.data.transaction.state, TransactionState::Complete);
571        assert_eq!(resp.data.transaction.tx_hash.as_deref(), Some("0xdeadbeef"));
572        Ok(())
573    }
574
575    #[test]
576    fn transactions_list_deserializes() -> Result<(), Box<dyn std::error::Error>> {
577        let json = r#"{
578            "data": {
579                "transactions": [
580                    {
581                        "id": "tx-1",
582                        "state": "SENT",
583                        "createDate": "2024-01-01T00:00:00Z",
584                        "updateDate": "2024-01-01T00:00:00Z"
585                    }
586                ]
587            }
588        }"#;
589        let resp: Transactions = serde_json::from_str(json)?;
590        assert_eq!(resp.data.transactions.len(), 1);
591        assert_eq!(resp.data.transactions[0].state, TransactionState::Sent);
592        Ok(())
593    }
594
595    #[test]
596    fn validate_address_response_deserializes() -> Result<(), Box<dyn std::error::Error>> {
597        let json = r#"{"data": {"isValid": true}}"#;
598        let resp: ValidateAddressResponse = serde_json::from_str(json)?;
599        assert!(resp.data.is_valid);
600        Ok(())
601    }
602
603    #[test]
604    fn create_transfer_request_serializes() -> Result<(), Box<dyn std::error::Error>> {
605        let req = CreateTransferTxRequest {
606            idempotency_key: "key".to_string(),
607            entity_secret_ciphertext: "cipher".to_string(),
608            wallet_id: "wallet-1".to_string(),
609            blockchain: None,
610            token_id: Some("token-1".to_string()),
611            destination_address: "0xdest".to_string(),
612            amounts: Some(vec!["1.0".to_string()]),
613            nft_token_ids: None,
614            ref_id: None,
615            fee_level: Some(FeeLevel::Medium),
616            gas_limit: None,
617            gas_price: None,
618            max_fee: None,
619            priority_fee: None,
620        };
621        let json = serde_json::to_string(&req)?;
622        assert!(json.contains("walletId"));
623        assert!(json.contains("destinationAddress"));
624        assert!(json.contains("MEDIUM"));
625        Ok(())
626    }
627
628    #[test]
629    fn transaction_state_all_variants_deserialize() -> Result<(), Box<dyn std::error::Error>> {
630        for (s, expected) in [
631            ("\"CANCELLED\"", TransactionState::Cancelled),
632            ("\"CONFIRMED\"", TransactionState::Confirmed),
633            ("\"COMPLETE\"", TransactionState::Complete),
634            ("\"INITIATED\"", TransactionState::Initiated),
635            ("\"STUCK\"", TransactionState::Stuck),
636        ] {
637            let state: TransactionState = serde_json::from_str(s)?;
638            assert_eq!(state, expected);
639        }
640        Ok(())
641    }
642}