Skip to main content

circle_user_controlled_wallets/models/
challenge.rs

1//! Challenge resource models for the Circle User-Controlled Wallets API.
2//!
3//! Contains request parameters and response types for PIN challenge and
4//! security question endpoints, as well as the challenge-ID response used
5//! by most write operations.
6
7use serde::{Deserialize, Serialize};
8
9use super::{
10    common::{AccountType, Blockchain},
11    wallet::WalletMetadata,
12};
13
14// ── Enums ─────────────────────────────────────────────────────────────────────
15
16/// Type of a PIN or device challenge.
17#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
18#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
19pub enum ChallengeType {
20    /// Initial user setup challenge.
21    Initialize,
22    /// Challenge for setting a PIN.
23    SetPin,
24    /// Challenge for changing a PIN.
25    ChangePin,
26    /// Challenge for setting security questions.
27    SetSecurityQuestions,
28    /// Challenge for creating a wallet.
29    CreateWallet,
30    /// Challenge for restoring a locked PIN.
31    RestorePin,
32    /// Challenge for creating a transaction.
33    CreateTransaction,
34    /// Challenge for accelerating a stuck transaction.
35    AccelerateTransaction,
36    /// Challenge for cancelling a transaction.
37    CancelTransaction,
38    /// Challenge for a smart-contract execution transaction.
39    ContractExecution,
40    /// Challenge for upgrading a wallet's SCA core.
41    WalletUpgrade,
42    /// Challenge for signing a message.
43    SignMessage,
44    /// Challenge for signing EIP-712 typed data.
45    SignTypeddata,
46    /// Challenge for signing a raw transaction.
47    SignTransaction,
48}
49
50/// Status of a challenge.
51#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
52#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
53pub enum ChallengeStatus {
54    /// Not yet started.
55    Pending,
56    /// Currently being processed by the mobile SDK.
57    InProgress,
58    /// Successfully completed.
59    Complete,
60    /// Finished with an error.
61    Failed,
62    /// Challenge has passed its expiry time.
63    Expired,
64}
65
66// ── Models ────────────────────────────────────────────────────────────────────
67
68/// A challenge record returned by the Circle API.
69#[derive(Debug, Clone, Deserialize, Serialize)]
70#[serde(rename_all = "camelCase")]
71pub struct Challenge {
72    /// Unique challenge identifier.
73    pub id: String,
74    /// Type of this challenge.
75    #[serde(rename = "type")]
76    pub challenge_type: ChallengeType,
77    /// Current status.
78    pub status: ChallengeStatus,
79    /// IDs of resources created or modified by this challenge outcome.
80    pub correlation_ids: Option<Vec<String>>,
81    /// Machine-readable error code (present on failure).
82    pub error_code: Option<i32>,
83    /// Human-readable error message (present on failure).
84    pub error_message: Option<String>,
85}
86
87/// `data` payload wrapping a list of challenges.
88#[derive(Debug, Clone, Deserialize, Serialize)]
89#[serde(rename_all = "camelCase")]
90pub struct ChallengesData {
91    /// List of challenges.
92    pub challenges: Vec<Challenge>,
93}
94
95/// Response envelope for list-challenges.
96#[derive(Debug, Clone, Deserialize, Serialize)]
97#[serde(rename_all = "camelCase")]
98pub struct Challenges {
99    /// Paginated challenges.
100    pub data: ChallengesData,
101}
102
103/// `data` payload wrapping a single challenge.
104#[derive(Debug, Clone, Deserialize, Serialize)]
105#[serde(rename_all = "camelCase")]
106pub struct ChallengeData {
107    /// The challenge record.
108    pub challenge: Challenge,
109}
110
111/// Response envelope for a single challenge lookup.
112#[derive(Debug, Clone, Deserialize, Serialize)]
113#[serde(rename_all = "camelCase")]
114pub struct ChallengeResponse {
115    /// Challenge data.
116    pub data: ChallengeData,
117}
118
119/// `data` payload returned by most write operations.
120#[derive(Debug, Clone, Deserialize, Serialize)]
121#[serde(rename_all = "camelCase")]
122pub struct ChallengeIdData {
123    /// UUID of the newly created challenge.
124    pub challenge_id: String,
125}
126
127/// Response envelope for operations that return a challenge ID.
128#[derive(Debug, Clone, Deserialize, Serialize)]
129#[serde(rename_all = "camelCase")]
130pub struct ChallengeIdResponse {
131    /// Challenge ID data.
132    pub data: ChallengeIdData,
133}
134
135// ── Request bodies ────────────────────────────────────────────────────────────
136
137/// Request body for `initializeUser` (sets PIN and optionally creates wallets).
138#[derive(Debug, Clone, Deserialize, Serialize)]
139#[serde(rename_all = "camelCase")]
140pub struct SetPinAndInitWalletRequest {
141    /// Client-generated idempotency key (UUID).
142    pub idempotency_key: String,
143    /// Account type for newly created wallets.
144    pub account_type: Option<AccountType>,
145    /// Blockchains on which to create wallets.
146    pub blockchains: Option<Vec<Blockchain>>,
147    /// Optional per-wallet metadata.
148    pub metadata: Option<Vec<WalletMetadata>>,
149}
150
151/// Request body for PIN set/change/restore operations.
152#[derive(Debug, Clone, Deserialize, Serialize)]
153#[serde(rename_all = "camelCase")]
154pub struct SetPinRequest {
155    /// Client-generated idempotency key (UUID).
156    pub idempotency_key: String,
157}
158
159// ── Tests ─────────────────────────────────────────────────────────────────────
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    #[test]
166    fn challenge_type_screaming() -> Result<(), Box<dyn std::error::Error>> {
167        assert_eq!(serde_json::to_string(&ChallengeType::CreateWallet)?, "\"CREATE_WALLET\"");
168        assert_eq!(serde_json::to_string(&ChallengeType::SignTypeddata)?, "\"SIGN_TYPEDDATA\"");
169        Ok(())
170    }
171
172    #[test]
173    fn challenge_status_screaming() -> Result<(), Box<dyn std::error::Error>> {
174        assert_eq!(serde_json::to_string(&ChallengeStatus::Complete)?, "\"COMPLETE\"");
175        assert_eq!(serde_json::to_string(&ChallengeStatus::InProgress)?, "\"IN_PROGRESS\"");
176        Ok(())
177    }
178
179    #[test]
180    fn challenge_id_response_round_trip() -> Result<(), Box<dyn std::error::Error>> {
181        let json = r#"{"data":{"challengeId":"abc-123"}}"#;
182        let resp: ChallengeIdResponse = serde_json::from_str(json)?;
183        assert_eq!(resp.data.challenge_id, "abc-123");
184        Ok(())
185    }
186
187    #[test]
188    fn challenge_type_field_renamed_to_type() -> Result<(), Box<dyn std::error::Error>> {
189        let c = Challenge {
190            id: "c1".to_string(),
191            challenge_type: ChallengeType::SetPin,
192            status: ChallengeStatus::Pending,
193            correlation_ids: None,
194            error_code: None,
195            error_message: None,
196        };
197        let s = serde_json::to_string(&c)?;
198        assert!(s.contains("\"type\""), "expected type key in {s}");
199        Ok(())
200    }
201}