Skip to main content

circle_user_controlled_wallets/models/
common.rs

1//! Common types shared across the User-Controlled Wallets API.
2//!
3//! Includes shared pagination, blockchain, error, and identifier types used
4//! across user-controlled wallet endpoints.
5
6use serde::{Deserialize, Serialize};
7
8// ── Error body ──────────────────────────────────────────────────────────────
9
10/// API error response body returned by Circle on non-2xx responses.
11#[derive(Debug, Clone, Deserialize, Serialize)]
12#[serde(rename_all = "camelCase")]
13pub struct ApiErrorBody {
14    /// Numeric error code from Circle.
15    pub code: i32,
16    /// Human-readable error message from Circle.
17    pub message: String,
18}
19
20// ── Blockchain ───────────────────────────────────────────────────────────────
21
22/// Blockchain network identifier used throughout the Circle API.
23#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
24#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
25pub enum Blockchain {
26    /// Ethereum mainnet.
27    Eth,
28    /// Ethereum Sepolia testnet.
29    #[serde(rename = "ETH-SEPOLIA")]
30    EthSepolia,
31    /// Avalanche C-Chain mainnet.
32    Avax,
33    /// Avalanche Fuji testnet.
34    #[serde(rename = "AVAX-FUJI")]
35    AvaxFuji,
36    /// Polygon (Matic) mainnet.
37    Matic,
38    /// Polygon Amoy testnet.
39    #[serde(rename = "MATIC-AMOY")]
40    MaticAmoy,
41    /// Solana mainnet.
42    Sol,
43    /// Solana devnet.
44    #[serde(rename = "SOL-DEVNET")]
45    SolDevnet,
46    /// Arbitrum One mainnet.
47    Arb,
48    /// Arbitrum Sepolia testnet.
49    #[serde(rename = "ARB-SEPOLIA")]
50    ArbSepolia,
51    /// NEAR Protocol mainnet.
52    Near,
53    /// NEAR Protocol testnet.
54    #[serde(rename = "NEAR-TESTNET")]
55    NearTestnet,
56    /// Generic EVM-compatible chain.
57    Evm,
58    /// Generic EVM testnet.
59    #[serde(rename = "EVM-TESTNET")]
60    EvmTestnet,
61    /// Unichain mainnet.
62    Uni,
63    /// Unichain Sepolia testnet.
64    #[serde(rename = "UNI-SEPOLIA")]
65    UniSepolia,
66    /// Base mainnet.
67    Base,
68    /// Base Sepolia testnet.
69    #[serde(rename = "BASE-SEPOLIA")]
70    BaseSepolia,
71    /// Optimism mainnet.
72    Op,
73    /// Optimism Sepolia testnet.
74    #[serde(rename = "OP-SEPOLIA")]
75    OpSepolia,
76    /// Aptos mainnet.
77    Aptos,
78    /// Aptos testnet.
79    #[serde(rename = "APTOS-TESTNET")]
80    AptosTestnet,
81    /// ARC testnet.
82    #[serde(rename = "ARC-TESTNET")]
83    ArcTestnet,
84    /// Monad mainnet.
85    Monad,
86    /// Monad testnet.
87    #[serde(rename = "MONAD-TESTNET")]
88    MonadTestnet,
89}
90
91// ── Token standard ───────────────────────────────────────────────────────────
92
93/// Token standard identifier.
94#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
95#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
96pub enum TokenStandard {
97    /// ERC-20 fungible token.
98    Erc20,
99    /// ERC-721 non-fungible token.
100    Erc721,
101    /// ERC-1155 multi-token standard.
102    Erc1155,
103    /// Solana fungible token.
104    Fungible,
105    /// Solana fungible asset.
106    FungibleAsset,
107    /// Solana non-fungible token.
108    NonFungible,
109    /// Solana non-fungible edition.
110    NonFungibleEdition,
111    /// Solana programmable non-fungible token.
112    ProgrammableNonFungible,
113    /// Solana programmable non-fungible edition.
114    ProgrammableNonFungibleEdition,
115}
116
117// ── Account / Custody types ──────────────────────────────────────────────────
118
119/// Wallet account type.
120#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
121#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
122pub enum AccountType {
123    /// Smart Contract Account (ERC-4337 / ERC-6900).
124    Sca,
125    /// Externally Owned Account.
126    Eoa,
127}
128
129/// Custody type for a wallet.
130#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
131#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
132pub enum CustodyType {
133    /// Wallet belongs to the developer.
134    Developer,
135    /// Wallet belongs to an end-user.
136    Enduser,
137}
138
139// ── Wallet state ─────────────────────────────────────────────────────────────
140
141/// Operational state of a wallet.
142#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
143#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
144pub enum WalletState {
145    /// Wallet is active and can transact.
146    Live,
147    /// Wallet has been frozen.
148    Frozen,
149}
150
151// ── Fee level ─────────────────────────────────────────────────────────────────
152
153/// Gas fee level preference for a transaction.
154#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
155#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
156pub enum FeeLevel {
157    /// Low-priority fee level.
158    Low,
159    /// Standard fee level.
160    Medium,
161    /// High-priority fee level.
162    High,
163}
164
165// ── SCA core version ──────────────────────────────────────────────────────────
166
167/// Smart Contract Account core version.
168#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
169pub enum ScaCore {
170    /// Circle ERC-4337 v1 implementation.
171    #[serde(rename = "circle_4337_v1")]
172    Circle4337V1,
173    /// Circle ERC-6900 single-owner v1 implementation.
174    #[serde(rename = "circle_6900_singleowner_v1")]
175    Circle6900SingleownerV1,
176    /// Circle ERC-6900 single-owner v2 implementation.
177    #[serde(rename = "circle_6900_singleowner_v2")]
178    Circle6900SingleownerV2,
179    /// Circle ERC-6900 single-owner v3 implementation.
180    #[serde(rename = "circle_6900_singleowner_v3")]
181    Circle6900SingleownerV3,
182}
183
184// ── Transaction fee details ───────────────────────────────────────────────────
185
186/// Detailed fee break-down for a transaction.
187#[derive(Debug, Clone, Default, Deserialize, Serialize)]
188#[serde(rename_all = "camelCase")]
189pub struct TransactionFee {
190    /// Gas limit for the transaction.
191    pub gas_limit: Option<String>,
192    /// Gas price in wei.
193    pub gas_price: Option<String>,
194    /// EIP-1559 max fee per gas.
195    pub max_fee: Option<String>,
196    /// EIP-1559 max priority fee per gas.
197    pub priority_fee: Option<String>,
198    /// Base fee per gas at the time of estimation.
199    pub base_fee: Option<String>,
200    /// Total network fee amount.
201    pub network_fee: Option<String>,
202    /// Total network fee in raw units.
203    pub network_fee_raw: Option<String>,
204    /// Layer-1 data fee (for L2 networks).
205    pub l1_fee: Option<String>,
206}
207
208// ── Pagination ────────────────────────────────────────────────────────────────
209
210/// Query parameters for paginated list endpoints.
211#[derive(Debug, Clone, Default, Deserialize, Serialize)]
212pub struct PageParams {
213    /// Filter results from this date-time (ISO 8601).
214    pub from: Option<String>,
215    /// Filter results to this date-time (ISO 8601).
216    pub to: Option<String>,
217    /// Cursor for the previous page.
218    #[serde(rename = "pageBefore", skip_serializing_if = "Option::is_none")]
219    pub page_before: Option<String>,
220    /// Cursor for the next page.
221    #[serde(rename = "pageAfter", skip_serializing_if = "Option::is_none")]
222    pub page_after: Option<String>,
223    /// Number of items per page (max 50).
224    #[serde(rename = "pageSize", skip_serializing_if = "Option::is_none")]
225    pub page_size: Option<u32>,
226}
227
228// ── Tests ─────────────────────────────────────────────────────────────────────
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    #[test]
235    fn blockchain_eth_serializes_to_eth() -> Result<(), Box<dyn std::error::Error>> {
236        let s = serde_json::to_string(&Blockchain::Eth)?;
237        assert_eq!(s, "\"ETH\"");
238        Ok(())
239    }
240
241    #[test]
242    fn blockchain_eth_sepolia_serializes_with_hyphen() -> Result<(), Box<dyn std::error::Error>> {
243        let s = serde_json::to_string(&Blockchain::EthSepolia)?;
244        assert_eq!(s, "\"ETH-SEPOLIA\"");
245        Ok(())
246    }
247
248    #[test]
249    fn blockchain_avax_fuji_serializes_with_hyphen() -> Result<(), Box<dyn std::error::Error>> {
250        let s = serde_json::to_string(&Blockchain::AvaxFuji)?;
251        assert_eq!(s, "\"AVAX-FUJI\"");
252        Ok(())
253    }
254
255    #[test]
256    fn blockchain_round_trips() -> Result<(), Box<dyn std::error::Error>> {
257        let original = Blockchain::BaseSepolia;
258        let json = serde_json::to_string(&original)?;
259        assert_eq!(json, "\"BASE-SEPOLIA\"");
260        let decoded: Blockchain = serde_json::from_str(&json)?;
261        assert_eq!(decoded, original);
262        Ok(())
263    }
264
265    #[test]
266    fn sca_core_serializes_to_snake_case() -> Result<(), Box<dyn std::error::Error>> {
267        let s = serde_json::to_string(&ScaCore::Circle4337V1)?;
268        assert_eq!(s, "\"circle_4337_v1\"");
269        let s2 = serde_json::to_string(&ScaCore::Circle6900SingleownerV2)?;
270        assert_eq!(s2, "\"circle_6900_singleowner_v2\"");
271        Ok(())
272    }
273
274    #[test]
275    fn fee_level_screaming_snake_case() -> Result<(), Box<dyn std::error::Error>> {
276        assert_eq!(serde_json::to_string(&FeeLevel::Medium)?, "\"MEDIUM\"");
277        Ok(())
278    }
279
280    #[test]
281    fn page_params_camel_case_keys() -> Result<(), Box<dyn std::error::Error>> {
282        let p = PageParams {
283            page_before: Some("abc".to_string()),
284            page_size: Some(10),
285            ..Default::default()
286        };
287        let s = serde_json::to_string(&p)?;
288        assert!(s.contains("pageBefore"), "expected pageBefore in {s}");
289        assert!(s.contains("pageSize"), "expected pageSize in {s}");
290        Ok(())
291    }
292}