Skip to main content

circle_user_controlled_wallets/models/
transaction.rs

1//! Transaction resource models for the Circle User-Controlled Wallets API.
2//!
3//! Contains request parameters and response types for transaction management
4//! endpoints, including estimation, acceleration, and cancellation.
5
6use serde::{Deserialize, Serialize};
7
8use super::{
9    common::{Blockchain, CustodyType, FeeLevel, PageParams, TransactionFee},
10    wallet::Nft,
11};
12
13// ── State / type enums ────────────────────────────────────────────────────────
14
15/// Current on-chain state of a transaction.
16#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
17#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
18pub enum TransactionState {
19    /// Transaction was cancelled before broadcast.
20    Cancelled,
21    /// At least one confirmation received.
22    Confirmed,
23    /// Transaction is finalized.
24    Complete,
25    /// Transaction was denied by a compliance rule.
26    Denied,
27    /// Transaction failed on-chain.
28    Failed,
29    /// Submission initiated internally.
30    Initiated,
31    /// Mempool cleared.
32    Cleared,
33    /// Waiting in the internal queue.
34    Queued,
35    /// Broadcast to the network.
36    Sent,
37    /// Stuck in the mempool with insufficient gas.
38    Stuck,
39}
40
41/// Direction of a transaction relative to the wallet.
42#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
43#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
44pub enum TransactionType {
45    /// Incoming transaction.
46    Inbound,
47    /// Outgoing transaction.
48    Outbound,
49}
50
51/// High-level operation type for a transaction.
52#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
53#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
54pub enum Operation {
55    /// Simple token or coin transfer.
56    Transfer,
57    /// Call to an existing smart-contract function.
58    ContractExecution,
59    /// Deployment of a new smart contract.
60    ContractDeployment,
61}
62
63// ── Risk / screening enums ────────────────────────────────────────────────────
64
65/// Recommended action from the compliance engine.
66#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
67#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
68pub enum RiskAction {
69    /// Transaction may proceed.
70    Approve,
71    /// Transaction requires manual review.
72    Review,
73    /// Originating wallet should be frozen.
74    FreezeWallet,
75    /// Transaction must be denied.
76    Deny,
77}
78
79/// Risk severity level assigned by the screening engine.
80#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
81#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
82pub enum RiskScore {
83    /// Risk could not be determined.
84    Unknown,
85    /// Low risk.
86    Low,
87    /// Moderate risk.
88    Medium,
89    /// High risk.
90    High,
91    /// Severe risk.
92    Severe,
93    /// Address is on a block-list.
94    Blocklist,
95}
96
97/// Category of risk flagged by the screening engine.
98#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
99#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
100pub enum RiskCategory {
101    /// OFAC or other sanctions list.
102    Sanctions,
103    /// Entity that facilitates sanctioned activity.
104    SanctionsDesignatedFacilitator,
105    /// Administratively designated sanctions target.
106    SanctionsAdminDesignated,
107    /// Sector-specific sanctions.
108    SanctionsSector,
109    /// Entity providing financial services to sanctioned counterparties.
110    FinancialServiceProvider,
111    /// Coin mixer or privacy wallet.
112    MixerOrPrivacyWallet,
113    /// Ransomware-associated address.
114    Ransomware,
115    /// Child exploitation-related content or activity.
116    Child,
117    /// Terrorist financing activity.
118    TerroristFinancing,
119    /// Online fraud shop.
120    FraudShop,
121    /// Cryptocurrency exchange.
122    Exchange,
123    /// Unhosted (self-custodied) wallet.
124    Unhosted,
125    /// Darknet market or service.
126    Darknet,
127    /// Gambling platform.
128    Gambling,
129}
130
131/// Source type for a risk signal.
132#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
133#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
134pub enum RiskType {
135    /// Direct ownership risk.
136    Ownership,
137    /// Counterparty risk.
138    Counterparty,
139    /// Indirect exposure risk.
140    Indirect,
141}
142
143// ── Screening structs ─────────────────────────────────────────────────────────
144
145/// A single risk signal from the compliance screening engine.
146#[derive(Debug, Clone, Deserialize, Serialize)]
147#[serde(rename_all = "camelCase")]
148pub struct RiskSignal {
149    /// Source entity type (`ADDRESS`, `BLOCKCHAIN`, or `ASSET`).
150    pub source: String,
151    /// The actual source value (address string, chain name, etc.).
152    pub source_value: String,
153    /// Assigned risk score.
154    pub risk_score: RiskScore,
155    /// Categories of risk identified.
156    pub risk_categories: Vec<RiskCategory>,
157    /// Relationship type of the risk.
158    #[serde(rename = "type")]
159    pub risk_type: RiskType,
160}
161
162/// Compliance screening decision for a transaction.
163#[derive(Debug, Clone, Deserialize, Serialize)]
164#[serde(rename_all = "camelCase")]
165pub struct TransactionScreeningDecision {
166    /// ISO 8601 timestamp when the screening ran.
167    pub screening_date: String,
168    /// Name of the compliance rule that triggered, if any.
169    pub rule_name: Option<String>,
170    /// Actions recommended by the screening engine.
171    pub actions: Option<Vec<RiskAction>>,
172    /// Individual risk signals that contributed to the decision.
173    pub reasons: Option<Vec<RiskSignal>>,
174}
175
176// ── Transaction ───────────────────────────────────────────────────────────────
177
178/// A user-controlled wallet transaction.
179#[derive(Debug, Clone, Deserialize, Serialize)]
180#[serde(rename_all = "camelCase")]
181pub struct Transaction {
182    /// Circle-assigned transaction ID.
183    pub id: String,
184    /// Current on-chain state.
185    pub state: TransactionState,
186    /// Blockchain this transaction is on.
187    pub blockchain: Blockchain,
188    /// Direction of the transaction.
189    pub transaction_type: TransactionType,
190    /// ISO 8601 creation timestamp.
191    pub create_date: String,
192    /// ISO 8601 last-updated timestamp.
193    pub update_date: String,
194
195    /// ABI function signature for contract calls.
196    pub abi_function_signature: Option<String>,
197    /// ABI parameters for contract calls.
198    pub abi_parameters: Option<Vec<serde_json::Value>>,
199    /// Token amounts transferred (decimal strings).
200    pub amounts: Option<Vec<String>>,
201    /// USD equivalent of the transferred amount.
202    pub amount_in_usd: Option<String>,
203    /// Block hash for the confirmed transaction.
204    pub block_hash: Option<String>,
205    /// Block height for the confirmed transaction.
206    pub block_height: Option<i64>,
207    /// Target contract address.
208    pub contract_address: Option<String>,
209    /// Custody type of the source wallet.
210    pub custody_type: Option<CustodyType>,
211    /// Recipient address.
212    pub destination_address: Option<String>,
213    /// Short reason string for failures.
214    pub error_reason: Option<String>,
215    /// Extended error information.
216    pub error_details: Option<String>,
217    /// Estimated fee at submission time.
218    pub estimated_fee: Option<TransactionFee>,
219    /// Requested fee level.
220    pub fee_level: Option<FeeLevel>,
221    /// ISO 8601 timestamp of the first on-chain confirmation.
222    pub first_confirm_date: Option<String>,
223    /// Actual network fee paid (decimal string).
224    pub network_fee: Option<String>,
225    /// Network fee expressed in USD.
226    pub network_fee_in_usd: Option<String>,
227    /// NFTs transferred in this transaction.
228    pub nfts: Option<Vec<Nft>>,
229    /// High-level operation type.
230    pub operation: Option<Operation>,
231    /// Application-defined reference identifier.
232    pub ref_id: Option<String>,
233    /// Source address.
234    pub source_address: Option<String>,
235    /// Circle token ID transferred.
236    pub token_id: Option<String>,
237    /// On-chain transaction hash.
238    pub tx_hash: Option<String>,
239    /// ID of the end-user who initiated this transaction.
240    pub user_id: Option<String>,
241    /// ID of the source wallet.
242    pub wallet_id: Option<String>,
243    /// Compliance screening evaluation.
244    pub transaction_screening_evaluation: Option<TransactionScreeningDecision>,
245}
246
247// ── Response wrappers ─────────────────────────────────────────────────────────
248
249/// `data` payload wrapping a list of transactions.
250#[derive(Debug, Clone, Deserialize, Serialize)]
251#[serde(rename_all = "camelCase")]
252pub struct TransactionsData {
253    /// List of transactions.
254    pub transactions: Vec<Transaction>,
255}
256
257/// Response envelope for list-transactions.
258#[derive(Debug, Clone, Deserialize, Serialize)]
259#[serde(rename_all = "camelCase")]
260pub struct Transactions {
261    /// Paginated transactions.
262    pub data: TransactionsData,
263}
264
265/// `data` payload wrapping a single transaction.
266#[derive(Debug, Clone, Deserialize, Serialize)]
267#[serde(rename_all = "camelCase")]
268pub struct TransactionData {
269    /// The transaction record.
270    pub transaction: Transaction,
271}
272
273/// Response envelope for a single-transaction lookup.
274#[derive(Debug, Clone, Deserialize, Serialize)]
275#[serde(rename_all = "camelCase")]
276pub struct TransactionResponse {
277    /// Transaction data.
278    pub data: TransactionData,
279}
280
281/// Fee information for the lowest-nonce transaction query.
282#[derive(Debug, Clone, Deserialize, Serialize)]
283#[serde(rename_all = "camelCase")]
284pub struct LowestNonceTransactionFeeInfo {
285    /// Suggested high-end fee for the replacement transaction.
286    pub new_high_estimated_fee: TransactionFee,
287    /// Difference in fee between old and new.
288    pub fee_difference_amount: String,
289}
290
291/// `data` payload for the lowest-nonce transaction endpoint.
292#[derive(Debug, Clone, Deserialize, Serialize)]
293#[serde(rename_all = "camelCase")]
294pub struct LowestNonceTransactionData {
295    /// The stuck transaction.
296    pub transaction: Transaction,
297    /// Fee information for replacing it.
298    pub fee_info: LowestNonceTransactionFeeInfo,
299}
300
301/// Response envelope for `getLowestNonceTransaction`.
302#[derive(Debug, Clone, Deserialize, Serialize)]
303#[serde(rename_all = "camelCase")]
304pub struct GetLowestNonceTransactionResponse {
305    /// Lowest-nonce transaction data.
306    pub data: LowestNonceTransactionData,
307}
308
309/// Fee estimation breakdown for a transaction.
310#[derive(Debug, Clone, Default, Deserialize, Serialize)]
311#[serde(rename_all = "camelCase")]
312pub struct EstimateFeeData {
313    /// High-priority fee estimate.
314    pub high: Option<TransactionFee>,
315    /// Low-priority fee estimate.
316    pub low: Option<TransactionFee>,
317    /// Medium-priority fee estimate.
318    pub medium: Option<TransactionFee>,
319    /// ERC-4337 call gas limit.
320    pub call_gas_limit: Option<String>,
321    /// ERC-4337 verification gas limit.
322    pub verification_gas_limit: Option<String>,
323    /// ERC-4337 pre-verification gas.
324    pub pre_verification_gas: Option<String>,
325}
326
327/// Response envelope for fee estimation endpoints.
328#[derive(Debug, Clone, Deserialize, Serialize)]
329#[serde(rename_all = "camelCase")]
330pub struct EstimateTransactionFee {
331    /// Fee estimation data.
332    pub data: EstimateFeeData,
333}
334
335/// `data` payload for address validation.
336#[derive(Debug, Clone, Deserialize, Serialize)]
337#[serde(rename_all = "camelCase")]
338pub struct ValidateAddressData {
339    /// `true` if the address is valid for the given blockchain.
340    pub is_valid: bool,
341}
342
343/// Response envelope for `validateAddress`.
344#[derive(Debug, Clone, Deserialize, Serialize)]
345#[serde(rename_all = "camelCase")]
346pub struct ValidateAddressResponse {
347    /// Validation result.
348    pub data: ValidateAddressData,
349}
350
351// ── Request bodies ────────────────────────────────────────────────────────────
352
353/// Request body for `createTransferTransaction`.
354#[derive(Debug, Clone, Deserialize, Serialize)]
355#[serde(rename_all = "camelCase")]
356pub struct CreateTransferTxRequest {
357    /// Client-generated idempotency key (UUID).
358    pub idempotency_key: String,
359    /// Source wallet ID.
360    pub wallet_id: String,
361    /// Destination address.
362    pub destination_address: String,
363    /// Amounts to transfer (decimal strings).
364    pub amounts: Option<Vec<String>>,
365    /// Gas fee level preference.
366    pub fee_level: Option<FeeLevel>,
367    /// Custom gas limit override.
368    pub gas_limit: Option<String>,
369    /// Custom gas price override (legacy).
370    pub gas_price: Option<String>,
371    /// EIP-1559 max fee per gas override.
372    pub max_fee: Option<String>,
373    /// EIP-1559 priority fee override.
374    pub priority_fee: Option<String>,
375    /// NFT token IDs to transfer.
376    pub nft_token_ids: Option<Vec<String>>,
377    /// Application-defined reference identifier.
378    pub ref_id: Option<String>,
379    /// Circle token ID to transfer.
380    pub token_id: Option<String>,
381    /// On-chain token contract address (alternative to token_id).
382    pub token_address: Option<String>,
383    /// Blockchain to use (for cross-chain transfers).
384    pub blockchain: Option<Blockchain>,
385}
386
387/// Request body for `accelerateTransaction`.
388#[derive(Debug, Clone, Deserialize, Serialize)]
389#[serde(rename_all = "camelCase")]
390pub struct AccelerateTxRequest {
391    /// Client-generated idempotency key (UUID).
392    pub idempotency_key: String,
393}
394
395/// Request body for `cancelTransaction`.
396#[derive(Debug, Clone, Deserialize, Serialize)]
397#[serde(rename_all = "camelCase")]
398pub struct CancelTxRequest {
399    /// Client-generated idempotency key (UUID).
400    pub idempotency_key: String,
401}
402
403/// Request body for `createContractExecutionTransaction`.
404#[derive(Debug, Clone, Deserialize, Serialize)]
405#[serde(rename_all = "camelCase")]
406pub struct CreateContractExecutionTxRequest {
407    /// Client-generated idempotency key (UUID).
408    pub idempotency_key: String,
409    /// Source wallet ID.
410    pub wallet_id: String,
411    /// Target contract address.
412    pub contract_address: String,
413    /// ABI function signature to call.
414    pub abi_function_signature: Option<String>,
415    /// ABI parameters for the function call.
416    pub abi_parameters: Option<Vec<serde_json::Value>>,
417    /// Raw ABI-encoded call data (alternative to abi_function_signature).
418    pub call_data: Option<String>,
419    /// Amount of native coin to send with the call.
420    pub amount: Option<String>,
421    /// Gas fee level preference.
422    pub fee_level: Option<FeeLevel>,
423    /// Custom gas limit override.
424    pub gas_limit: Option<String>,
425    /// Custom gas price override.
426    pub gas_price: Option<String>,
427    /// EIP-1559 max fee per gas override.
428    pub max_fee: Option<String>,
429    /// EIP-1559 priority fee override.
430    pub priority_fee: Option<String>,
431    /// Application-defined reference identifier.
432    pub ref_id: Option<String>,
433}
434
435/// Request body for `createWalletUpgradeTransaction`.
436#[derive(Debug, Clone, Deserialize, Serialize)]
437#[serde(rename_all = "camelCase")]
438pub struct CreateWalletUpgradeTxRequest {
439    /// Client-generated idempotency key (UUID).
440    pub idempotency_key: String,
441    /// Wallet to upgrade.
442    pub wallet_id: String,
443    /// Target SCA core version (e.g. `"circle_6900_singleowner_v2"`).
444    pub new_sca_core: String,
445    /// Gas fee level preference.
446    pub fee_level: Option<FeeLevel>,
447    /// Custom gas limit override.
448    pub gas_limit: Option<String>,
449    /// Custom gas price override.
450    pub gas_price: Option<String>,
451    /// EIP-1559 max fee per gas override.
452    pub max_fee: Option<String>,
453    /// EIP-1559 priority fee override.
454    pub priority_fee: Option<String>,
455    /// Application-defined reference identifier.
456    pub ref_id: Option<String>,
457}
458
459/// Request body for `estimateTransferFee`.
460#[derive(Debug, Clone, Deserialize, Serialize)]
461#[serde(rename_all = "camelCase")]
462pub struct EstimateTransferFeeRequest {
463    /// Amounts to transfer (decimal strings).
464    pub amounts: Vec<String>,
465    /// Destination address.
466    pub destination_address: String,
467    /// NFT token IDs to transfer.
468    pub nft_token_ids: Option<Vec<String>>,
469    /// Source address (used when wallet_id is not provided).
470    pub source_address: Option<String>,
471    /// Circle token ID.
472    pub token_id: Option<String>,
473    /// On-chain token contract address.
474    pub token_address: Option<String>,
475    /// Blockchain for the transfer.
476    pub blockchain: Option<Blockchain>,
477    /// Source wallet ID.
478    pub wallet_id: Option<String>,
479}
480
481/// Request body for `estimateContractExecutionFee`.
482#[derive(Debug, Clone, Deserialize, Serialize)]
483#[serde(rename_all = "camelCase")]
484pub struct EstimateContractExecFeeRequest {
485    /// Target contract address.
486    pub contract_address: String,
487    /// ABI function signature.
488    pub abi_function_signature: Option<String>,
489    /// ABI parameters.
490    pub abi_parameters: Option<Vec<serde_json::Value>>,
491    /// Raw ABI-encoded call data.
492    pub call_data: Option<String>,
493    /// Amount of native coin to send.
494    pub amount: Option<String>,
495    /// Blockchain for the call.
496    pub blockchain: Option<Blockchain>,
497    /// Source address.
498    pub source_address: Option<String>,
499    /// Source wallet ID.
500    pub wallet_id: Option<String>,
501}
502
503/// Request body for `validateAddress`.
504#[derive(Debug, Clone, Deserialize, Serialize)]
505#[serde(rename_all = "camelCase")]
506pub struct ValidateAddressRequest {
507    /// Address to validate.
508    pub address: String,
509    /// Blockchain on which the address should be valid.
510    pub blockchain: Blockchain,
511}
512
513/// Query parameters for `listTransactions`.
514#[derive(Debug, Clone, Default, Deserialize, Serialize)]
515#[serde(rename_all = "camelCase")]
516pub struct ListTransactionsParams {
517    /// Filter by blockchain.
518    pub blockchain: Option<Blockchain>,
519    /// Filter by destination address.
520    pub destination_address: Option<String>,
521    /// If `true`, include all transactions for the wallet set.
522    pub include_all: Option<bool>,
523    /// Filter by operation type.
524    pub operation: Option<Operation>,
525    /// Filter by transaction state.
526    pub state: Option<TransactionState>,
527    /// Filter by on-chain transaction hash.
528    pub tx_hash: Option<String>,
529    /// Filter by transaction type (inbound/outbound).
530    pub tx_type: Option<TransactionType>,
531    /// Filter by user ID.
532    pub user_id: Option<String>,
533    /// Comma-separated list of wallet IDs to filter by.
534    pub wallet_ids: Option<String>,
535    /// Start of date range (ISO 8601).
536    pub from: Option<String>,
537    /// End of date range (ISO 8601).
538    pub to: Option<String>,
539    /// Pagination cursors.
540    #[serde(flatten)]
541    pub page: PageParams,
542}
543
544/// Query parameters for `getLowestNonceTransaction`.
545#[derive(Debug, Clone, Default, Deserialize, Serialize)]
546#[serde(rename_all = "camelCase")]
547pub struct GetLowestNonceTxParams {
548    /// Blockchain to search on.
549    pub blockchain: Option<Blockchain>,
550    /// On-chain address to search for.
551    pub address: Option<String>,
552    /// Wallet ID to search for.
553    pub wallet_id: Option<String>,
554}
555
556// ── Tests ─────────────────────────────────────────────────────────────────────
557
558#[cfg(test)]
559mod tests {
560    use super::*;
561
562    #[test]
563    fn transaction_state_screaming() -> Result<(), Box<dyn std::error::Error>> {
564        assert_eq!(serde_json::to_string(&TransactionState::Complete)?, "\"COMPLETE\"");
565        assert_eq!(serde_json::to_string(&TransactionState::Confirmed)?, "\"CONFIRMED\"");
566        assert_eq!(serde_json::to_string(&TransactionState::Queued)?, "\"QUEUED\"");
567        Ok(())
568    }
569
570    #[test]
571    fn risk_action_screaming() -> Result<(), Box<dyn std::error::Error>> {
572        assert_eq!(serde_json::to_string(&RiskAction::FreezeWallet)?, "\"FREEZE_WALLET\"");
573        Ok(())
574    }
575
576    #[test]
577    fn validate_address_request_camel_case() -> Result<(), Box<dyn std::error::Error>> {
578        let req =
579            ValidateAddressRequest { address: "0xabc".to_string(), blockchain: Blockchain::Eth };
580        let s = serde_json::to_string(&req)?;
581        assert!(s.contains("\"address\""), "address key in {s}");
582        assert!(s.contains("\"blockchain\""), "blockchain key in {s}");
583        Ok(())
584    }
585
586    #[test]
587    fn transfer_tx_request_camel_case() -> Result<(), Box<dyn std::error::Error>> {
588        let req = CreateTransferTxRequest {
589            idempotency_key: "key".to_string(),
590            wallet_id: "w1".to_string(),
591            destination_address: "0xdest".to_string(),
592            amounts: Some(vec!["1.0".to_string()]),
593            fee_level: Some(FeeLevel::High),
594            gas_limit: None,
595            gas_price: None,
596            max_fee: None,
597            priority_fee: None,
598            nft_token_ids: None,
599            ref_id: None,
600            token_id: None,
601            token_address: None,
602            blockchain: None,
603        };
604        let s = serde_json::to_string(&req)?;
605        assert!(s.contains("idempotencyKey"), "{s}");
606        assert!(s.contains("walletId"), "{s}");
607        assert!(s.contains("destinationAddress"), "{s}");
608        Ok(())
609    }
610}