Skip to main content

circle_compliance/models/
screening.rs

1//! Screening resource models for the Circle Compliance Engine API.
2
3/// Supported blockchain networks for address screening.
4#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
5#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
6pub enum Chain {
7    /// Ethereum mainnet.
8    Eth,
9    /// Ethereum Sepolia testnet.
10    #[serde(rename = "ETH-SEPOLIA")]
11    EthSepolia,
12    /// Avalanche C-Chain mainnet.
13    Avax,
14    /// Avalanche Fuji testnet.
15    #[serde(rename = "AVAX-FUJI")]
16    AvaxFuji,
17    /// Polygon PoS mainnet.
18    Matic,
19    /// Polygon Amoy testnet.
20    #[serde(rename = "MATIC-AMOY")]
21    MaticAmoy,
22    /// Algorand mainnet.
23    Algo,
24    /// Cosmos Hub mainnet.
25    Atom,
26    /// Arbitrum One mainnet.
27    Arb,
28    /// Arbitrum Sepolia testnet.
29    #[serde(rename = "ARB-SEPOLIA")]
30    ArbSepolia,
31    /// Hedera mainnet.
32    Hbar,
33    /// Solana mainnet.
34    Sol,
35    /// Solana devnet.
36    #[serde(rename = "SOL-DEVNET")]
37    SolDevnet,
38    /// Unichain mainnet.
39    Uni,
40    /// Unichain Sepolia testnet.
41    #[serde(rename = "UNI-SEPOLIA")]
42    UniSepolia,
43    /// TRON mainnet.
44    Trx,
45    /// Stellar mainnet.
46    Xlm,
47    /// Bitcoin Cash mainnet.
48    Bch,
49    /// Bitcoin mainnet.
50    Btc,
51    /// Bitcoin SV mainnet.
52    Bsv,
53    /// Ethereum Classic mainnet.
54    Etc,
55    /// Litecoin mainnet.
56    Ltc,
57    /// Monero mainnet.
58    Xmr,
59    /// XRP Ledger mainnet.
60    Xrp,
61    /// 0x / ZRX.
62    Zrx,
63    /// Optimism mainnet.
64    Op,
65    /// Polkadot mainnet.
66    Dot,
67}
68
69/// Request body for the `screenAddress` endpoint.
70#[derive(Debug, Clone, serde::Serialize)]
71#[serde(rename_all = "camelCase")]
72pub struct ScreenAddressRequest {
73    /// UUID v4 idempotency key.
74    pub idempotency_key: String,
75    /// Blockchain address to screen.
76    pub address: String,
77    /// Blockchain network.
78    pub chain: Chain,
79}
80
81/// Action to take based on a screening decision.
82#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
83#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
84pub enum RiskAction {
85    /// Address is safe to proceed.
86    Approve,
87    /// Address requires manual review.
88    Review,
89    /// The wallet associated with the address should be frozen.
90    FreezeWallet,
91    /// Transaction/interaction should be denied.
92    Deny,
93}
94
95/// Risk severity score.
96#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
97#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
98pub enum RiskScore {
99    /// Risk cannot be determined.
100    Unknown,
101    /// Low risk.
102    Low,
103    /// Medium risk.
104    Medium,
105    /// High risk.
106    High,
107    /// Severe risk.
108    Severe,
109    /// Address is on a blocklist.
110    Blocklist,
111}
112
113/// Risk category of a signal.
114#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
115#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
116pub enum RiskCategory {
117    /// Government/international sanctions.
118    Sanctions,
119    /// Child sexual abuse material.
120    Csam,
121    /// General illicit behavior.
122    IllicitBehavior,
123    /// Gambling-related.
124    Gambling,
125    /// Terrorist financing.
126    TerroristFinancing,
127    /// Unsupported category.
128    Unsupported,
129    /// Frozen address.
130    Frozen,
131    /// Other risk.
132    Other,
133    /// Industry considered high-risk.
134    HighRiskIndustry,
135    /// Politically exposed person.
136    Pep,
137    /// Trusted entity.
138    Trusted,
139    /// Hacking-related.
140    Hacking,
141    /// Human trafficking.
142    HumanTrafficking,
143    /// Subject to special regulatory measures.
144    SpecialMeasures,
145}
146
147/// Relationship type of a risk signal to the screened address.
148#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
149#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
150pub enum RiskType {
151    /// Direct ownership risk.
152    Ownership,
153    /// Risk from a counterparty.
154    Counterparty,
155    /// Indirect exposure.
156    Indirect,
157}
158
159/// Risk signal source identifier and location.
160#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
161#[serde(rename_all = "camelCase")]
162pub struct SignalSource {
163    /// UUID of the vendor response row.
164    pub row_id: String,
165    /// JSON path of the signal in the vendor response.
166    pub pointer: String,
167}
168
169/// A risk signal associated with the screened address.
170#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
171#[serde(rename_all = "camelCase")]
172pub struct RiskSignal {
173    /// Signal data source (`ADDRESS`, `BLOCKCHAIN`, or `ASSET`).
174    pub source: String,
175    /// Value of the source (e.g. a blockchain address).
176    pub source_value: String,
177    /// Risk severity.
178    pub risk_score: RiskScore,
179    /// Risk categories.
180    pub risk_categories: Vec<RiskCategory>,
181    /// Relationship type.
182    #[serde(rename = "type")]
183    pub risk_type: RiskType,
184    /// Pointer back to the raw vendor response.
185    pub signal_source: Option<SignalSource>,
186}
187
188/// Screening decision for a blockchain address.
189#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
190#[serde(rename_all = "camelCase")]
191pub struct AddressScreeningDecision {
192    /// ISO-8601 date the screening was run.
193    pub screening_date: String,
194    /// Matched rule name (if any).
195    pub rule_name: Option<String>,
196    /// Actions to take.
197    pub actions: Option<Vec<RiskAction>>,
198    /// Risk signals driving the decision.
199    pub reasons: Option<Vec<RiskSignal>>,
200}
201
202/// Raw vendor response detail.
203#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
204#[serde(rename_all = "camelCase")]
205pub struct ScreeningVendorDetail {
206    /// UUID of this vendor response record.
207    pub id: String,
208    /// Vendor name.
209    pub vendor: String,
210    /// Free-form vendor response payload.
211    pub response: serde_json::Value,
212    /// Creation timestamp (ISO-8601).
213    pub create_date: String,
214}
215
216/// Outer `{ "data": … }` envelope returned by the compliance API.
217///
218/// The Circle compliance endpoint wraps its payload in the same `data` field
219/// used by every other Circle API; this struct handles that deserialization.
220#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
221pub struct ScreenAddressEnvelope {
222    /// Inner screening result.
223    pub data: BlockchainAddressScreeningResponse,
224}
225
226/// Response from the `screenAddress` endpoint.
227#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
228#[serde(rename_all = "camelCase")]
229pub struct BlockchainAddressScreeningResponse {
230    /// Summary result of the screening evaluation.
231    pub result: ScreeningResult,
232    /// Detailed screening decision.
233    pub decision: AddressScreeningDecision,
234    /// UUID matching the idempotency key from the request.
235    pub id: String,
236    /// Screened blockchain address.
237    pub address: String,
238    /// Blockchain network.
239    pub chain: Chain,
240    /// Raw vendor response details.
241    pub details: Vec<ScreeningVendorDetail>,
242    /// UUID of any generated compliance alert.
243    pub alert_id: Option<String>,
244}
245
246/// Top-level outcome of a screening request.
247#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
248#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
249pub enum ScreeningResult {
250    /// Address is approved.
251    Approved,
252    /// Address is denied.
253    Denied,
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    #[test]
261    fn chain_roundtrip() -> Result<(), Box<dyn std::error::Error>> {
262        let s = serde_json::to_string(&Chain::EthSepolia)?;
263        assert_eq!(s, "\"ETH-SEPOLIA\"");
264        let parsed: Chain = serde_json::from_str("\"MATIC\"")?;
265        assert_eq!(parsed, Chain::Matic);
266        Ok(())
267    }
268
269    #[test]
270    fn risk_action_deserializes() -> Result<(), Box<dyn std::error::Error>> {
271        let a: RiskAction = serde_json::from_str("\"APPROVE\"")?;
272        assert_eq!(a, RiskAction::Approve);
273        Ok(())
274    }
275}