Skip to main content

circles_rpc/methods/
group.rs

1use crate::client::RpcClient;
2use crate::error::Result;
3use crate::methods::QueryMethods;
4use crate::paged_query::{PagedFetch, PagedQuery};
5use circles_types::{
6    Address, Conjunction, CursorColumn, Filter, FilterPredicate, GroupMembershipRow,
7    GroupQueryParams, GroupRow, GroupTokenHolderRow, OrderBy, PagedQueryParams, SortOrder,
8};
9use std::pin::Pin;
10use std::sync::Arc;
11
12/// Methods for group membership lookups (`circles_getGroupMemberships`, `circles_getGroups`).
13#[derive(Clone, Debug)]
14pub struct GroupMethods {
15    client: RpcClient,
16}
17
18impl GroupMethods {
19    /// Create a new accessor for group-related RPCs.
20    pub fn new(client: RpcClient) -> Self {
21        Self { client }
22    }
23
24    fn paged_query<TRow>(&self, params: PagedQueryParams) -> PagedQuery<TRow>
25    where
26        TRow: serde::de::DeserializeOwned
27            + serde::Serialize
28            + Clone
29            + Send
30            + Sync
31            + std::fmt::Debug
32            + Unpin
33            + 'static,
34    {
35        let client = self.client.clone();
36        let fetch: PagedFetch<TRow> = Arc::new(move |params: PagedQueryParams| {
37            let client = client.clone();
38            Box::pin(async move { QueryMethods::new(client).paged_query::<TRow>(params).await })
39                as Pin<
40                    Box<
41                        dyn std::future::Future<Output = Result<circles_types::PagedResult<TRow>>>
42                            + Send,
43                    >,
44                >
45        });
46        PagedQuery::new(fetch, params)
47    }
48
49    /// circles_getGroupMemberships
50    pub async fn get_memberships(&self, avatar: Address) -> Result<Vec<GroupMembershipRow>> {
51        self.client
52            .call("circles_getGroupMemberships", (avatar,))
53            .await
54    }
55
56    /// circles_getGroups
57    pub async fn get_groups(&self, avatar: Address) -> Result<Vec<GroupRow>> {
58        self.client.call("circles_getGroups", (avatar,)).await
59    }
60
61    /// Paged `GroupMemberships` query filtered by member address.
62    pub fn get_group_memberships(
63        &self,
64        avatar: Address,
65        limit: u32,
66        sort_order: SortOrder,
67    ) -> PagedQuery<GroupMembershipRow> {
68        self.paged_query(PagedQueryParams {
69            namespace: "V_CrcV2".into(),
70            table: "GroupMemberships".into(),
71            sort_order,
72            columns: vec![
73                "blockNumber".into(),
74                "timestamp".into(),
75                "transactionIndex".into(),
76                "logIndex".into(),
77                "transactionHash".into(),
78                "group".into(),
79                "member".into(),
80                "expiryTime".into(),
81            ],
82            filter: Some(vec![
83                FilterPredicate::equals("member".into(), format!("{avatar:#x}")).into(),
84            ]),
85            cursor_columns: None,
86            order_columns: None,
87            limit,
88        })
89    }
90
91    /// Paged `GroupMemberships` query filtered by group address.
92    pub fn get_group_members(
93        &self,
94        group: Address,
95        limit: u32,
96        sort_order: SortOrder,
97    ) -> PagedQuery<GroupMembershipRow> {
98        self.paged_query(PagedQueryParams {
99            namespace: "V_CrcV2".into(),
100            table: "GroupMemberships".into(),
101            sort_order,
102            columns: vec![
103                "blockNumber".into(),
104                "timestamp".into(),
105                "transactionIndex".into(),
106                "logIndex".into(),
107                "transactionHash".into(),
108                "group".into(),
109                "member".into(),
110                "expiryTime".into(),
111            ],
112            filter: Some(vec![
113                FilterPredicate::equals("group".into(), format!("{group:#x}")).into(),
114            ]),
115            cursor_columns: None,
116            order_columns: None,
117            limit,
118        })
119    }
120
121    /// Paged `GroupTokenHoldersBalance` query matching the TS group holders helper.
122    pub fn get_group_holders(&self, group: Address, limit: u32) -> PagedQuery<GroupTokenHolderRow> {
123        self.paged_query(PagedQueryParams {
124            namespace: "V_CrcV2".into(),
125            table: "GroupTokenHoldersBalance".into(),
126            sort_order: SortOrder::DESC,
127            columns: vec![
128                "group".into(),
129                "holder".into(),
130                "totalBalance".into(),
131                "demurragedTotalBalance".into(),
132                "fractionOwnership".into(),
133            ],
134            filter: Some(vec![
135                FilterPredicate::equals("group".into(), format!("{group:#x}")).into(),
136            ]),
137            cursor_columns: Some(vec![CursorColumn::asc("holder".into())]),
138            order_columns: Some(vec![
139                OrderBy::desc("totalBalance".into()),
140                OrderBy::asc("holder".into()),
141            ]),
142            limit,
143        })
144    }
145
146    /// Paged `Groups` query with optional filters matching the TS helper.
147    pub fn get_groups_paged(
148        &self,
149        limit: u32,
150        params: Option<GroupQueryParams>,
151        sort_order: SortOrder,
152    ) -> PagedQuery<GroupRow> {
153        self.paged_query(PagedQueryParams {
154            namespace: "V_CrcV2".into(),
155            table: "Groups".into(),
156            sort_order,
157            columns: vec![
158                "blockNumber".into(),
159                "timestamp".into(),
160                "transactionIndex".into(),
161                "logIndex".into(),
162                "transactionHash".into(),
163                "group".into(),
164                "type".into(),
165                "owner".into(),
166                "mintPolicy".into(),
167                "mintHandler".into(),
168                "treasury".into(),
169                "service".into(),
170                "feeCollection".into(),
171                "memberCount".into(),
172                "name".into(),
173                "symbol".into(),
174                "cidV0Digest".into(),
175                "erc20WrapperDemurraged".into(),
176                "erc20WrapperStatic".into(),
177            ],
178            filter: build_group_filters(params),
179            cursor_columns: None,
180            order_columns: None,
181            limit,
182        })
183    }
184
185    /// Fetch groups across pages until `limit` rows are collected.
186    pub async fn find_groups(
187        &self,
188        limit: u32,
189        params: Option<GroupQueryParams>,
190    ) -> Result<Vec<GroupRow>> {
191        let mut query = self.get_groups_paged(limit, params, SortOrder::DESC);
192        let mut rows = Vec::new();
193
194        while let Some(page) = query.next_page().await? {
195            rows.extend(page.items);
196            if rows.len() as u32 >= limit || !page.has_more {
197                break;
198            }
199        }
200
201        rows.truncate(limit as usize);
202        Ok(rows)
203    }
204}
205
206fn build_group_filters(params: Option<GroupQueryParams>) -> Option<Vec<Filter>> {
207    let params = params?;
208
209    let mut filters = Vec::new();
210
211    if let Some(name_prefix) = params.name_starts_with {
212        filters.push(FilterPredicate::like("name".into(), format!("{name_prefix}%")).into());
213    }
214
215    if let Some(symbol_prefix) = params.symbol_starts_with {
216        filters.push(FilterPredicate::like("symbol".into(), format!("{symbol_prefix}%")).into());
217    }
218
219    if let Some(group_addresses) = params.group_address_in
220        && !group_addresses.is_empty()
221    {
222        let predicates: Vec<Filter> = group_addresses
223            .into_iter()
224            .map(|addr| FilterPredicate::equals("group".into(), format!("{addr:#x}")).into())
225            .collect();
226        filters.push(if predicates.len() == 1 {
227            predicates.into_iter().next().expect("one predicate")
228        } else {
229            Conjunction::or(predicates).into()
230        });
231    }
232
233    if let Some(group_types) = params.group_type_in
234        && !group_types.is_empty()
235    {
236        let predicates: Vec<Filter> = group_types
237            .into_iter()
238            .map(|group_type| FilterPredicate::equals("type".into(), group_type).into())
239            .collect();
240        filters.push(if predicates.len() == 1 {
241            predicates.into_iter().next().expect("one predicate")
242        } else {
243            Conjunction::or(predicates).into()
244        });
245    }
246
247    if let Some(owners) = params.owner_in
248        && !owners.is_empty()
249    {
250        let predicates: Vec<Filter> = owners
251            .into_iter()
252            .map(|addr| FilterPredicate::equals("owner".into(), format!("{addr:#x}")).into())
253            .collect();
254        filters.push(if predicates.len() == 1 {
255            predicates.into_iter().next().expect("one predicate")
256        } else {
257            Conjunction::or(predicates).into()
258        });
259    }
260
261    if let Some(mint_handler) = params.mint_handler_equals {
262        filters.push(
263            FilterPredicate::equals("mintHandler".into(), format!("{mint_handler:#x}")).into(),
264        );
265    }
266
267    if let Some(treasury) = params.treasury_equals {
268        filters.push(FilterPredicate::equals("treasury".into(), format!("{treasury:#x}")).into());
269    }
270
271    if filters.len() > 1 {
272        Some(vec![Conjunction::and(filters).into()])
273    } else if filters.is_empty() {
274        None
275    } else {
276        Some(filters)
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283
284    fn methods() -> GroupMethods {
285        let url = "https://rpc.example.com".parse().expect("valid url");
286        GroupMethods::new(RpcClient::http(url))
287    }
288
289    #[test]
290    fn group_memberships_query_uses_membership_table() {
291        let query =
292            methods().get_group_memberships(Address::repeat_byte(0x11), 50, SortOrder::DESC);
293
294        assert_eq!(query.params.namespace, "V_CrcV2");
295        assert_eq!(query.params.table, "GroupMemberships");
296        assert_eq!(query.params.limit, 50);
297    }
298
299    #[test]
300    fn group_filters_match_ts_shape() {
301        let query = methods().get_groups_paged(
302            25,
303            Some(GroupQueryParams {
304                name_starts_with: Some("Comm".into()),
305                symbol_starts_with: None,
306                group_address_in: Some(vec![
307                    Address::repeat_byte(0x22),
308                    Address::repeat_byte(0x33),
309                ]),
310                group_type_in: Some(vec!["Standard".into()]),
311                owner_in: Some(vec![Address::repeat_byte(0x44)]),
312                mint_handler_equals: Some(Address::repeat_byte(0x55)),
313                treasury_equals: None,
314            }),
315            SortOrder::ASC,
316        );
317
318        let filters = query.params.filter.expect("filters");
319        assert_eq!(filters.len(), 1);
320        match &filters[0] {
321            Filter::Conjunction(and) => {
322                assert_eq!(and.predicates.len(), 5);
323            }
324            other => panic!("expected conjunction filter, got {other:?}"),
325        }
326    }
327
328    #[test]
329    fn group_holders_query_matches_ts_cursor_and_order_shape() {
330        let query = methods().get_group_holders(Address::repeat_byte(0x66), 10);
331
332        assert_eq!(query.params.table, "GroupTokenHoldersBalance");
333        assert_eq!(query.params.sort_order, SortOrder::DESC);
334        assert_eq!(
335            query.params.cursor_columns,
336            Some(vec![CursorColumn::asc("holder".into())])
337        );
338        assert_eq!(
339            query.params.order_columns,
340            Some(vec![
341                OrderBy::desc("totalBalance".into()),
342                OrderBy::asc("holder".into()),
343            ])
344        );
345    }
346}