Skip to main content

circles_types/
rpc.rs

1use crate::AvatarInfo;
2use alloy_primitives::{Address, TxHash, U256};
3use serde::{Deserialize, Deserializer, Serialize};
4
5/// JSON-RPC request structure
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct JsonRpcRequest<T = serde_json::Value> {
8    pub jsonrpc: String,
9    pub id: serde_json::Value, // Can be number or string
10    pub method: String,
11    pub params: T,
12}
13
14impl<T> JsonRpcRequest<T> {
15    pub fn new(id: impl Into<serde_json::Value>, method: String, params: T) -> Self {
16        Self {
17            jsonrpc: "2.0".to_string(),
18            id: id.into(),
19            method,
20            params,
21        }
22    }
23}
24
25/// JSON-RPC error object
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct JsonRpcError {
28    pub code: i32,
29    pub message: String,
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub data: Option<serde_json::Value>,
32}
33
34/// JSON-RPC response structure
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct JsonRpcResponse<T = serde_json::Value> {
37    pub jsonrpc: String,
38    pub id: serde_json::Value,
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub result: Option<T>,
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub error: Option<JsonRpcError>,
43}
44
45impl<T> JsonRpcResponse<T> {
46    pub fn success(id: impl Into<serde_json::Value>, result: T) -> Self {
47        Self {
48            jsonrpc: "2.0".to_string(),
49            id: id.into(),
50            result: Some(result),
51            error: None,
52        }
53    }
54
55    pub fn error(id: impl Into<serde_json::Value>, error: JsonRpcError) -> Self {
56        Self {
57            jsonrpc: "2.0".to_string(),
58            id: id.into(),
59            result: None,
60            error: Some(error),
61        }
62    }
63}
64
65/// Circles query response format
66/// Used for circles_query RPC method results
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct CirclesQueryResponse {
69    pub columns: Vec<String>,
70    pub rows: Vec<Vec<serde_json::Value>>,
71}
72
73/// Generic query response wrapper
74/// Used for internal query transformations and type-safe responses
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct QueryResponse<T = serde_json::Value> {
77    pub result: T,
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub error: Option<serde_json::Value>,
80}
81
82impl<T> QueryResponse<T> {
83    pub fn success(result: T) -> Self {
84        Self {
85            result,
86            error: None,
87        }
88    }
89
90    pub fn error(error: serde_json::Value) -> Self {
91        Self {
92            result: unsafe { std::mem::zeroed() }, // This is a hack, in practice we'd use Option<T>
93            error: Some(error),
94        }
95    }
96}
97
98/// Better version of QueryResponse that's more idiomatic
99#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct SafeQueryResponse<T> {
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub result: Option<T>,
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub error: Option<serde_json::Value>,
105}
106
107impl<T> SafeQueryResponse<T> {
108    pub fn success(result: T) -> Self {
109        Self {
110            result: Some(result),
111            error: None,
112        }
113    }
114
115    pub fn error(error: serde_json::Value) -> Self {
116        Self {
117            result: None,
118            error: Some(error),
119        }
120    }
121}
122
123/// Unified invitation-origin response from `circles_getInvitationOrigin`.
124#[derive(Debug, Clone, Serialize, Deserialize)]
125#[serde(rename_all = "camelCase")]
126pub struct InvitationOriginResponse {
127    pub address: Address,
128    pub invitation_type: String,
129    pub inviter: Option<Address>,
130    pub proxy_inviter: Option<Address>,
131    pub escrow_amount: Option<String>,
132    pub block_number: u64,
133    pub timestamp: u64,
134    pub transaction_hash: TxHash,
135    pub version: u32,
136}
137
138/// Trust-based invitation information returned by `circles_getAllInvitations`.
139#[derive(Debug, Clone, Serialize, Deserialize)]
140#[serde(rename_all = "camelCase")]
141pub struct TrustInvitation {
142    pub address: Address,
143    pub source: String,
144    pub balance: String,
145    pub avatar_info: Option<AvatarInfo>,
146}
147
148/// Escrow-based invitation information returned by `circles_getAllInvitations`.
149#[derive(Debug, Clone, Serialize, Deserialize)]
150#[serde(rename_all = "camelCase")]
151pub struct EscrowInvitation {
152    pub address: Address,
153    pub source: String,
154    pub escrowed_amount: String,
155    pub escrow_days: u32,
156    pub block_number: u64,
157    pub timestamp: u64,
158    pub avatar_info: Option<AvatarInfo>,
159}
160
161/// At-scale invitation information returned by `circles_getAllInvitations`.
162#[derive(Debug, Clone, Serialize, Deserialize)]
163#[serde(rename_all = "camelCase")]
164pub struct AtScaleInvitation {
165    pub address: Address,
166    pub source: String,
167    pub block_number: u64,
168    pub timestamp: u64,
169    pub origin_inviter: Option<Address>,
170}
171
172/// Combined invitation response from `circles_getAllInvitations`.
173#[derive(Debug, Clone, Serialize, Deserialize)]
174#[serde(rename_all = "camelCase")]
175pub struct AllInvitationsResponse {
176    pub address: Address,
177    pub trust_invitations: Vec<TrustInvitation>,
178    pub escrow_invitations: Vec<EscrowInvitation>,
179    pub at_scale_invitations: Vec<AtScaleInvitation>,
180}
181
182/// Account information returned by `circles_getInvitationsFrom`.
183#[derive(Debug, Clone, Serialize, Deserialize)]
184#[serde(rename_all = "camelCase")]
185pub struct InvitedAccountInfo {
186    pub address: Address,
187    pub status: String,
188    pub block_number: u64,
189    pub timestamp: u64,
190    pub avatar_info: Option<AvatarInfo>,
191}
192
193/// Response returned by `circles_getInvitationsFrom`.
194#[derive(Debug, Clone, Serialize, Deserialize)]
195#[serde(rename_all = "camelCase")]
196pub struct InvitationsFromResponse {
197    pub address: Address,
198    pub accepted: bool,
199    pub results: Vec<InvitedAccountInfo>,
200}
201
202/// Balance type that can be either raw U256 or formatted as TimeCircles floating point
203#[derive(Debug, Clone, Serialize, Deserialize)]
204#[serde(untagged)]
205pub enum Balance {
206    Raw(U256),
207    TimeCircles(f64),
208}
209
210/// Token balance response from circles_getTokenBalances
211#[derive(Debug, Clone, Serialize)]
212pub struct TokenBalanceResponse {
213    #[serde(rename = "tokenAddress")]
214    pub token_address: Address,
215    #[serde(rename = "tokenId")]
216    pub token_id: Address,
217    pub balance: Balance,
218    /// Static atto-circles (inflationary wrappers) when provided by the backend.
219    #[serde(default, rename = "staticAttoCircles")]
220    pub static_atto_circles: Option<U256>,
221    #[serde(default, rename = "staticCircles")]
222    pub static_circles: Option<f64>,
223    #[serde(default, rename = "tokenType", skip_serializing_if = "Option::is_none")]
224    pub token_type: Option<String>,
225    #[serde(default, skip_serializing_if = "Option::is_none")]
226    pub version: Option<u32>,
227    #[serde(
228        default,
229        rename = "attoCircles",
230        skip_serializing_if = "Option::is_none"
231    )]
232    pub atto_circles: Option<U256>,
233    #[serde(default, skip_serializing_if = "Option::is_none")]
234    pub circles: Option<f64>,
235    #[serde(default, rename = "attoCrc", skip_serializing_if = "Option::is_none")]
236    pub atto_crc: Option<U256>,
237    #[serde(default, skip_serializing_if = "Option::is_none")]
238    pub crc: Option<f64>,
239    #[serde(default, rename = "isErc20")]
240    pub is_erc20: bool,
241    #[serde(default, rename = "isErc1155")]
242    pub is_erc1155: bool,
243    #[serde(default, rename = "isWrapped")]
244    pub is_wrapped: bool,
245    #[serde(default, rename = "isInflationary")]
246    pub is_inflationary: bool,
247    #[serde(default, rename = "isGroup")]
248    pub is_group: bool,
249    #[serde(rename = "tokenOwner")]
250    pub token_owner: Address,
251}
252
253#[derive(Debug, Clone, Deserialize)]
254struct TokenBalanceResponseWire {
255    #[serde(default, rename = "tokenAddress", alias = "token_address")]
256    token_address: Option<Address>,
257    #[serde(rename = "tokenId", alias = "token_id")]
258    token_id: Address,
259    #[serde(default)]
260    balance: Option<Balance>,
261    #[serde(default, rename = "staticAttoCircles")]
262    static_atto_circles: Option<U256>,
263    #[serde(default, rename = "staticCircles")]
264    static_circles: Option<f64>,
265    #[serde(default, rename = "tokenType", alias = "token_type")]
266    token_type: Option<String>,
267    #[serde(default)]
268    version: Option<u32>,
269    #[serde(default, rename = "attoCircles")]
270    atto_circles: Option<U256>,
271    #[serde(default)]
272    circles: Option<f64>,
273    #[serde(default, rename = "attoCrc")]
274    atto_crc: Option<U256>,
275    #[serde(default)]
276    crc: Option<f64>,
277    #[serde(default, rename = "isErc20", alias = "is_erc20")]
278    is_erc20: bool,
279    #[serde(default, rename = "isErc1155", alias = "is_erc1155")]
280    is_erc1155: bool,
281    #[serde(default, rename = "isWrapped", alias = "is_wrapped")]
282    is_wrapped: bool,
283    #[serde(default, rename = "isInflationary", alias = "is_inflationary")]
284    is_inflationary: bool,
285    #[serde(default, rename = "isGroup", alias = "is_group")]
286    is_group: bool,
287    #[serde(rename = "tokenOwner", alias = "token_owner")]
288    token_owner: Address,
289}
290
291impl<'de> Deserialize<'de> for TokenBalanceResponse {
292    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
293    where
294        D: Deserializer<'de>,
295    {
296        let wire = TokenBalanceResponseWire::deserialize(deserializer)?;
297        let balance = wire
298            .balance
299            .or_else(|| wire.atto_circles.map(Balance::Raw))
300            .or_else(|| wire.circles.map(Balance::TimeCircles))
301            .ok_or_else(|| serde::de::Error::missing_field("balance / attoCircles / circles"))?;
302
303        Ok(Self {
304            token_address: wire.token_address.unwrap_or(wire.token_id),
305            token_id: wire.token_id,
306            balance,
307            static_atto_circles: wire.static_atto_circles,
308            static_circles: wire.static_circles,
309            token_type: wire.token_type,
310            version: wire.version,
311            atto_circles: wire.atto_circles,
312            circles: wire.circles,
313            atto_crc: wire.atto_crc,
314            crc: wire.crc,
315            is_erc20: wire.is_erc20,
316            is_erc1155: wire.is_erc1155,
317            is_wrapped: wire.is_wrapped,
318            is_inflationary: wire.is_inflationary,
319            is_group: wire.is_group,
320            token_owner: wire.token_owner,
321        })
322    }
323}
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328    use serde_json::json;
329
330    #[test]
331    fn invitation_origin_response_deserializes_plugin_shape() {
332        let value = json!({
333            "address": "0xde374ece6fa50e781e81aac78e811b33d16912c7",
334            "invitationType": "v2_at_scale",
335            "inviter": "0x1234567890abcdef1234567890abcdef12345678",
336            "proxyInviter": "0xabcdef1234567890abcdef1234567890abcdef12",
337            "escrowAmount": null,
338            "blockNumber": 36500000,
339            "timestamp": 1704240000,
340            "transactionHash": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
341            "version": 2
342        });
343
344        let response: InvitationOriginResponse =
345            serde_json::from_value(value).expect("deserialize invitation origin");
346
347        assert_eq!(response.invitation_type, "v2_at_scale");
348        assert_eq!(response.version, 2);
349        assert_eq!(response.block_number, 36_500_000);
350        assert!(response.inviter.is_some());
351        assert!(response.proxy_inviter.is_some());
352    }
353
354    #[test]
355    fn all_invitations_response_deserializes_plugin_shape() {
356        let value = json!({
357            "address": "0xde374ece6fa50e781e81aac78e811b33d16912c7",
358            "trustInvitations": [{
359                "address": "0x1234567890abcdef1234567890abcdef12345678",
360                "source": "trust",
361                "balance": "150.5",
362                "avatarInfo": null
363            }],
364            "escrowInvitations": [{
365                "address": "0xabcdef1234567890abcdef1234567890abcdef12",
366                "source": "escrow",
367                "escrowedAmount": "100000000000000000000",
368                "escrowDays": 7,
369                "blockNumber": 43645581,
370                "timestamp": 1765725505,
371                "avatarInfo": null
372            }],
373            "atScaleInvitations": [{
374                "address": "0xde374ece6fa50e781e81aac78e811b33d16912c7",
375                "source": "atScale",
376                "blockNumber": 43260668,
377                "timestamp": 1763742205,
378                "originInviter": null
379            }]
380        });
381
382        let response: AllInvitationsResponse =
383            serde_json::from_value(value).expect("deserialize all invitations");
384
385        assert_eq!(response.trust_invitations.len(), 1);
386        assert_eq!(response.escrow_invitations.len(), 1);
387        assert_eq!(response.at_scale_invitations.len(), 1);
388        assert_eq!(response.trust_invitations[0].balance, "150.5");
389        assert_eq!(response.escrow_invitations[0].escrow_days, 7);
390    }
391}
392
393/// Transaction history row matching the TS RPC helper shape.
394#[derive(Debug, Clone, Serialize, Deserialize)]
395#[serde(rename_all = "camelCase")]
396pub struct TransactionHistoryRow {
397    pub block_number: u64,
398    pub timestamp: u64,
399    pub transaction_index: u32,
400    pub log_index: u32,
401    pub transaction_hash: TxHash,
402    pub version: u32,
403    pub from: Address,
404    pub to: Address,
405    pub id: String,
406    pub token_address: Address,
407    pub value: String,
408    #[serde(default)]
409    pub circles: Option<f64>,
410    #[serde(default)]
411    pub atto_circles: Option<U256>,
412    #[serde(default)]
413    pub static_circles: Option<f64>,
414    #[serde(default)]
415    pub static_atto_circles: Option<U256>,
416    #[serde(default)]
417    pub crc: Option<f64>,
418    #[serde(default)]
419    pub atto_crc: Option<U256>,
420}