Skip to main content

circle_developer_controlled_wallets/models/
wallet.rs

1//! Wallet resource models for the Circle Developer-Controlled Wallets API.
2//!
3//! Contains request parameters and response types for wallet management
4//! endpoints including balances and NFTs.
5
6use super::common::{AccountType, Blockchain, CustodyType, PageParams, TokenStandard, WalletState};
7
8/// NFT token standard.
9#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
10pub enum NftStandard {
11    /// ERC-721 non-fungible token.
12    #[serde(rename = "ERC721")]
13    Erc721,
14    /// ERC-1155 multi-token.
15    #[serde(rename = "ERC1155")]
16    Erc1155,
17}
18
19/// Fungible token standard (for wallet balance queries).
20#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
21pub enum FtStandard {
22    /// Native coin (no ERC standard).
23    #[serde(rename = "")]
24    Native,
25    /// ERC-20 fungible token.
26    #[serde(rename = "ERC20")]
27    Erc20,
28}
29
30/// Smart Contract Account core implementation variant.
31#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
32pub enum ScaCore {
33    /// Circle ERC-4337 v1 implementation.
34    #[serde(rename = "circle_4337_v1")]
35    Circle4337V1,
36    /// Circle ERC-6900 single-owner v1 implementation.
37    #[serde(rename = "circle_6900_singleowner_v1")]
38    Circle6900SingleownerV1,
39    /// Circle ERC-6900 single-owner v2 implementation.
40    #[serde(rename = "circle_6900_singleowner_v2")]
41    Circle6900SingleownerV2,
42    /// Circle ERC-6900 single-owner v3 implementation.
43    #[serde(rename = "circle_6900_singleowner_v3")]
44    Circle6900SingleownerV3,
45}
46
47/// A blockchain token definition.
48#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
49#[serde(rename_all = "camelCase")]
50pub struct Token {
51    /// Unique token ID.
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub id: Option<String>,
54    /// Blockchain network the token lives on.
55    pub blockchain: Blockchain,
56    /// Whether this token is the native coin of its chain.
57    pub is_native: bool,
58    /// Token name (e.g. `"USD Coin"`).
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub name: Option<String>,
61    /// Token standard.
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub standard: Option<TokenStandard>,
64    /// Number of decimal places.
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub decimals: Option<i32>,
67    /// Chain symbol (e.g. `"USDC"`).
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub symbol: Option<String>,
70    /// Contract address (absent for native coins).
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub token_address: Option<String>,
73    /// ISO-8601 last-update timestamp.
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub update_date: Option<String>,
76    /// ISO-8601 creation timestamp.
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub create_date: Option<String>,
79}
80
81/// A single fungible token balance entry.
82#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
83#[serde(rename_all = "camelCase")]
84pub struct Balance {
85    /// Token amount as a decimal string.
86    pub amount: String,
87    /// Token definition.
88    pub token: Token,
89    /// ISO-8601 last-update timestamp.
90    pub update_date: String,
91}
92
93/// Wallet metadata for creation requests.
94#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
95#[serde(rename_all = "camelCase")]
96pub struct WalletMetadata {
97    /// Human-readable name for the wallet.
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub name: Option<String>,
100    /// External reference ID.
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub ref_id: Option<String>,
103}
104
105/// A developer-controlled wallet resource.
106#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
107#[serde(rename_all = "camelCase")]
108pub struct Wallet {
109    /// Unique wallet ID (UUID).
110    pub id: String,
111    /// On-chain wallet address.
112    pub address: String,
113    /// Blockchain network this wallet is on.
114    pub blockchain: Blockchain,
115    /// ISO-8601 creation timestamp.
116    pub create_date: String,
117    /// ISO-8601 last-update timestamp.
118    pub update_date: String,
119    /// Custody type.
120    pub custody_type: CustodyType,
121    /// Human-readable name.
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub name: Option<String>,
124    /// External reference ID.
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub ref_id: Option<String>,
127    /// Wallet lifecycle state.
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub state: Option<WalletState>,
130    /// User ID (user-controlled wallets).
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub user_id: Option<String>,
133    /// Wallet set ID the wallet belongs to.
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub wallet_set_id: Option<String>,
136    /// Initial public key.
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub initial_public_key: Option<String>,
139    /// Account type (SCA or EOA).
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub account_type: Option<AccountType>,
142    /// SCA core implementation.
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub sca_core: Option<ScaCore>,
145    /// Token balances (populated when requested via `include_all` or balance endpoints).
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub token_balances: Option<Vec<Balance>>,
148}
149
150/// Inner data of a list-wallets response.
151#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
152#[serde(rename_all = "camelCase")]
153pub struct WalletsData {
154    /// Wallets matching the query.
155    pub wallets: Vec<Wallet>,
156}
157
158/// Response wrapper for the list-wallets and create-wallets endpoints.
159#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
160pub struct Wallets {
161    /// Response data.
162    pub data: WalletsData,
163}
164
165/// Inner data of a single-wallet response.
166#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
167#[serde(rename_all = "camelCase")]
168pub struct WalletData {
169    /// The wallet.
170    pub wallet: Wallet,
171}
172
173/// Response wrapper for get/update wallet endpoints.
174#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
175pub struct WalletResponse {
176    /// Response data.
177    pub data: WalletData,
178}
179
180/// Inner data of the list-wallet-balances (developer-level) response.
181#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
182#[serde(rename_all = "camelCase")]
183pub struct WalletsWithBalancesData {
184    /// Wallets with their token balance details.
185    pub wallets: Vec<Wallet>,
186}
187
188/// Response wrapper for the list-developer-wallet-balances endpoint.
189#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
190pub struct WalletsWithBalances {
191    /// Response data.
192    pub data: WalletsWithBalancesData,
193}
194
195/// Inner data of single-wallet token-balances response.
196#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
197#[serde(rename_all = "camelCase")]
198pub struct BalancesData {
199    /// Token balances for the wallet.
200    pub token_balances: Vec<Balance>,
201}
202
203/// Response wrapper for the per-wallet balances endpoint.
204#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
205pub struct Balances {
206    /// Response data.
207    pub data: BalancesData,
208}
209
210/// A single NFT holding.
211#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
212#[serde(rename_all = "camelCase")]
213pub struct Nft {
214    /// NFT amount (ERC-1155 can have quantity > 1).
215    pub amount: String,
216    /// Token definition.
217    pub token: Token,
218    /// ISO-8601 last-update timestamp.
219    pub update_date: String,
220    /// On-chain token ID.
221    #[serde(skip_serializing_if = "Option::is_none")]
222    pub nft_token_id: Option<String>,
223    /// IPFS or HTTP URI of the NFT metadata.
224    #[serde(skip_serializing_if = "Option::is_none")]
225    pub metadata: Option<String>,
226}
227
228/// Inner data of a list-wallet-NFTs response.
229#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
230pub struct NftsData {
231    /// NFTs matching the query.
232    pub nfts: Vec<Nft>,
233}
234
235/// Response wrapper for wallet NFT endpoints.
236#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
237pub struct Nfts {
238    /// Response data.
239    pub data: NftsData,
240}
241
242/// Request body for creating one or more developer-controlled wallets.
243#[derive(Debug, Clone, serde::Serialize)]
244#[serde(rename_all = "camelCase")]
245pub struct CreateWalletsRequest {
246    /// Idempotency key (UUID) to deduplicate requests.
247    pub idempotency_key: String,
248    /// Encrypted entity secret ciphertext.
249    pub entity_secret_ciphertext: String,
250    /// Wallet set ID the wallets should belong to.
251    pub wallet_set_id: String,
252    /// Blockchain networks to create wallets on.
253    pub blockchains: Vec<Blockchain>,
254    /// Account type (SCA or EOA); defaults to EOA.
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub account_type: Option<AccountType>,
257    /// Number of wallets to create (default 1).
258    #[serde(skip_serializing_if = "Option::is_none")]
259    pub count: Option<u32>,
260    /// Per-wallet metadata overrides.
261    #[serde(skip_serializing_if = "Option::is_none")]
262    pub metadata: Option<Vec<WalletMetadata>>,
263}
264
265/// Request body for updating a wallet.
266#[derive(Debug, Clone, Default, serde::Serialize)]
267#[serde(rename_all = "camelCase")]
268pub struct UpdateWalletRequest {
269    /// New human-readable name.
270    #[serde(skip_serializing_if = "Option::is_none")]
271    pub name: Option<String>,
272    /// New external reference ID.
273    #[serde(skip_serializing_if = "Option::is_none")]
274    pub ref_id: Option<String>,
275}
276
277/// Query parameters for the list-wallets endpoint.
278#[derive(Debug, Default, Clone, serde::Serialize)]
279#[serde(rename_all = "camelCase")]
280pub struct ListWalletsParams {
281    /// Filter by blockchain.
282    #[serde(skip_serializing_if = "Option::is_none")]
283    pub blockchain: Option<Blockchain>,
284    /// Filter by wallet address.
285    #[serde(skip_serializing_if = "Option::is_none")]
286    pub address: Option<String>,
287    /// Filter by wallet set ID.
288    #[serde(skip_serializing_if = "Option::is_none")]
289    pub wallet_set_id: Option<String>,
290    /// Filter by external reference ID.
291    #[serde(skip_serializing_if = "Option::is_none")]
292    pub ref_id: Option<String>,
293    /// Filter by wallet state.
294    #[serde(skip_serializing_if = "Option::is_none")]
295    pub state: Option<WalletState>,
296    /// Filter by custody type.
297    #[serde(skip_serializing_if = "Option::is_none")]
298    pub custody_type: Option<CustodyType>,
299    /// Pagination parameters.
300    #[serde(flatten)]
301    pub page: PageParams,
302}
303
304/// Query parameters for the developer list-wallet-balances endpoint.
305#[derive(Debug, Default, Clone, serde::Serialize)]
306#[serde(rename_all = "camelCase")]
307pub struct ListWalletBalancesParams {
308    /// Include all wallets, not just those with balances.
309    #[serde(skip_serializing_if = "Option::is_none")]
310    pub include_all: Option<bool>,
311    /// Filter by token name.
312    #[serde(skip_serializing_if = "Option::is_none")]
313    pub name: Option<String>,
314    /// Filter by token contract address.
315    #[serde(skip_serializing_if = "Option::is_none")]
316    pub token_address: Option<String>,
317    /// Filter by blockchain.
318    #[serde(skip_serializing_if = "Option::is_none")]
319    pub blockchain: Option<Blockchain>,
320    /// Filter by wallet set ID.
321    #[serde(skip_serializing_if = "Option::is_none")]
322    pub wallet_set_id: Option<String>,
323    /// Filter by specific wallet IDs (comma-separated string).
324    #[serde(skip_serializing_if = "Option::is_none")]
325    pub wallet_ids: Option<String>,
326    /// Filter by custody type.
327    #[serde(skip_serializing_if = "Option::is_none")]
328    pub custody_type: Option<CustodyType>,
329    /// Filter by wallet address.
330    #[serde(skip_serializing_if = "Option::is_none")]
331    pub address: Option<String>,
332    /// Pagination parameters.
333    #[serde(flatten)]
334    pub page: PageParams,
335}
336
337/// Query parameters for per-wallet balance and NFT endpoints.
338#[derive(Debug, Default, Clone, serde::Serialize)]
339#[serde(rename_all = "camelCase")]
340pub struct WalletNftsParams {
341    /// Pagination parameters.
342    #[serde(flatten)]
343    pub page: PageParams,
344}
345
346/// Query parameters for the list-wallet-NFTs endpoint.
347#[derive(Debug, Default, Clone, serde::Serialize)]
348#[serde(rename_all = "camelCase")]
349pub struct ListWalletNftsParams {
350    /// Filter by NFT standard.
351    #[serde(skip_serializing_if = "Option::is_none")]
352    pub standard: Option<NftStandard>,
353    /// Filter by token name.
354    #[serde(skip_serializing_if = "Option::is_none")]
355    pub name: Option<String>,
356    /// Filter by token contract address.
357    #[serde(skip_serializing_if = "Option::is_none")]
358    pub token_address: Option<String>,
359    /// Pagination parameters.
360    #[serde(flatten)]
361    pub page: PageParams,
362}
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367
368    #[test]
369    fn wallet_response_deserializes() -> Result<(), Box<dyn std::error::Error>> {
370        let json = r#"{
371            "data": {
372                "wallet": {
373                    "id": "wallet-id-1",
374                    "address": "0x1234",
375                    "blockchain": "ETH",
376                    "createDate": "2024-01-01T00:00:00Z",
377                    "updateDate": "2024-01-01T00:00:00Z",
378                    "custodyType": "DEVELOPER",
379                    "state": "LIVE"
380                }
381            }
382        }"#;
383        let resp: WalletResponse = serde_json::from_str(json)?;
384        assert_eq!(resp.data.wallet.id, "wallet-id-1");
385        assert_eq!(resp.data.wallet.blockchain, Blockchain::Eth);
386        assert_eq!(resp.data.wallet.state, Some(WalletState::Live));
387        Ok(())
388    }
389
390    #[test]
391    fn wallets_response_deserializes() -> Result<(), Box<dyn std::error::Error>> {
392        let json = r#"{
393            "data": {
394                "wallets": [
395                    {
396                        "id": "w1",
397                        "address": "0xabc",
398                        "blockchain": "MATIC",
399                        "createDate": "2024-01-01T00:00:00Z",
400                        "updateDate": "2024-01-01T00:00:00Z",
401                        "custodyType": "DEVELOPER"
402                    }
403                ]
404            }
405        }"#;
406        let resp: Wallets = serde_json::from_str(json)?;
407        assert_eq!(resp.data.wallets.len(), 1);
408        assert_eq!(resp.data.wallets[0].blockchain, Blockchain::Matic);
409        Ok(())
410    }
411
412    #[test]
413    fn nfts_response_deserializes() -> Result<(), Box<dyn std::error::Error>> {
414        let json = r#"{
415            "data": {
416                "nfts": [
417                    {
418                        "amount": "1",
419                        "token": {
420                            "blockchain": "BASE",
421                            "isNative": false
422                        },
423                        "updateDate": "2024-01-01T00:00:00Z",
424                        "nftTokenId": "42"
425                    }
426                ]
427            }
428        }"#;
429        let resp: Nfts = serde_json::from_str(json)?;
430        assert_eq!(resp.data.nfts.len(), 1);
431        assert_eq!(resp.data.nfts[0].nft_token_id.as_deref(), Some("42"));
432        Ok(())
433    }
434
435    #[test]
436    fn sca_core_serializes() -> Result<(), Box<dyn std::error::Error>> {
437        let json = serde_json::to_string(&ScaCore::Circle4337V1)?;
438        assert_eq!(json, "\"circle_4337_v1\"");
439        let back: ScaCore = serde_json::from_str(&json)?;
440        assert_eq!(back, ScaCore::Circle4337V1);
441        Ok(())
442    }
443
444    #[test]
445    fn create_wallets_request_serializes() -> Result<(), Box<dyn std::error::Error>> {
446        let req = CreateWalletsRequest {
447            idempotency_key: "key".to_string(),
448            entity_secret_ciphertext: "cipher".to_string(),
449            wallet_set_id: "set-id".to_string(),
450            blockchains: vec![Blockchain::Eth],
451            account_type: None,
452            count: Some(2),
453            metadata: None,
454        };
455        let json = serde_json::to_string(&req)?;
456        assert!(json.contains("walletSetId"));
457        assert!(json.contains("\"ETH\""));
458        Ok(())
459    }
460}