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#[derive(Clone, Debug)]
14pub struct GroupMethods {
15 client: RpcClient,
16}
17
18impl GroupMethods {
19 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 pub async fn get_memberships(&self, avatar: Address) -> Result<Vec<GroupMembershipRow>> {
51 self.client
52 .call("circles_getGroupMemberships", (avatar,))
53 .await
54 }
55
56 pub async fn get_groups(&self, avatar: Address) -> Result<Vec<GroupRow>> {
58 self.client.call("circles_getGroups", (avatar,)).await
59 }
60
61 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 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 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 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 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}