Skip to main content

circles_rpc/methods/
transaction.rs

1use crate::client::RpcClient;
2use crate::error::{CirclesRpcError, Result};
3use crate::methods::QueryMethods;
4use crate::paged_query::{PagedFetch, PagedQuery};
5use circles_types::{
6    Address, Conjunction, FilterPredicate, PagedQueryParams, PagedResult, SortOrder,
7    TransactionHistoryRow, U256,
8};
9use circles_utils::converter::{
10    atto_circles_to_atto_crc, atto_circles_to_atto_static_circles, atto_circles_to_circles,
11};
12use serde::{Deserialize, Serialize};
13use std::pin::Pin;
14use std::str::FromStr;
15use std::sync::Arc;
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
18#[serde(rename_all = "camelCase")]
19struct RawTransactionHistoryRow {
20    block_number: u64,
21    timestamp: u64,
22    transaction_index: u32,
23    log_index: u32,
24    transaction_hash: alloy_primitives::TxHash,
25    version: u32,
26    from: Address,
27    to: Address,
28    id: String,
29    token_address: Address,
30    value: String,
31}
32
33fn enrich_transaction_row(row: RawTransactionHistoryRow) -> Result<TransactionHistoryRow> {
34    let atto_circles =
35        U256::from_str(&row.value).map_err(|e| CirclesRpcError::InvalidResponse {
36            message: format!("invalid transfer summary value: {e}"),
37        })?;
38    let circles = atto_circles_to_circles(atto_circles);
39    let atto_crc = atto_circles_to_atto_crc(atto_circles, row.timestamp);
40    let crc = atto_circles_to_circles(atto_crc);
41    let static_atto_circles =
42        atto_circles_to_atto_static_circles(atto_circles, Some(row.timestamp));
43    let static_circles = atto_circles_to_circles(static_atto_circles);
44
45    Ok(TransactionHistoryRow {
46        block_number: row.block_number,
47        timestamp: row.timestamp,
48        transaction_index: row.transaction_index,
49        log_index: row.log_index,
50        transaction_hash: row.transaction_hash,
51        version: row.version,
52        from: row.from,
53        to: row.to,
54        id: row.id,
55        token_address: row.token_address,
56        value: row.value,
57        circles: Some(circles),
58        atto_circles: Some(atto_circles),
59        static_circles: Some(static_circles),
60        static_atto_circles: Some(static_atto_circles),
61        crc: Some(crc),
62        atto_crc: Some(atto_crc),
63    })
64}
65
66/// Transaction history methods mirroring the TS RPC helper.
67#[derive(Clone, Debug)]
68pub struct TransactionMethods {
69    client: RpcClient,
70}
71
72impl TransactionMethods {
73    /// Create a new accessor for transaction-history RPCs.
74    pub fn new(client: RpcClient) -> Self {
75        Self { client }
76    }
77
78    /// Paged transaction history from the `V_Crc.TransferSummary` view.
79    pub fn get_transaction_history(
80        &self,
81        avatar: Address,
82        limit: u32,
83        sort_order: SortOrder,
84    ) -> PagedQuery<TransactionHistoryRow> {
85        let params = PagedQueryParams {
86            namespace: "V_Crc".into(),
87            table: "TransferSummary".into(),
88            sort_order,
89            columns: vec![
90                "blockNumber".into(),
91                "timestamp".into(),
92                "transactionIndex".into(),
93                "logIndex".into(),
94                "transactionHash".into(),
95                "version".into(),
96                "from".into(),
97                "to".into(),
98                "id".into(),
99                "tokenAddress".into(),
100                "value".into(),
101            ],
102            filter: Some(vec![
103                Conjunction::and(vec![
104                    FilterPredicate::equals("version".into(), 2).into(),
105                    Conjunction::or(vec![
106                        FilterPredicate::equals("from".into(), format!("{avatar:#x}")).into(),
107                        FilterPredicate::equals("to".into(), format!("{avatar:#x}")).into(),
108                    ])
109                    .into(),
110                ])
111                .into(),
112            ]),
113            cursor_columns: None,
114            order_columns: None,
115            limit,
116        };
117
118        let client = self.client.clone();
119        let fetch: PagedFetch<TransactionHistoryRow> = Arc::new(move |params: PagedQueryParams| {
120            let client = client.clone();
121            Box::pin(async move {
122                let raw = QueryMethods::new(client)
123                    .paged_query::<RawTransactionHistoryRow>(params)
124                    .await?;
125                let rows = raw
126                    .results
127                    .into_iter()
128                    .map(enrich_transaction_row)
129                    .collect::<Result<Vec<_>>>()?;
130                Ok(PagedResult {
131                    limit: raw.limit,
132                    size: rows.len() as u32,
133                    first_cursor: raw.first_cursor,
134                    last_cursor: raw.last_cursor,
135                    sort_order: raw.sort_order,
136                    has_more: raw.has_more,
137                    results: rows,
138                })
139            })
140                as Pin<
141                    Box<
142                        dyn std::future::Future<Output = Result<PagedResult<TransactionHistoryRow>>>
143                            + Send,
144                    >,
145                >
146        });
147
148        PagedQuery::new(fetch, params)
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    fn methods() -> TransactionMethods {
157        let url = "https://rpc.example.com".parse().expect("valid url");
158        TransactionMethods::new(RpcClient::http(url))
159    }
160
161    #[test]
162    fn transaction_history_query_uses_transfer_summary_view() {
163        let query =
164            methods().get_transaction_history(Address::repeat_byte(0x11), 50, SortOrder::DESC);
165
166        assert_eq!(query.params.namespace, "V_Crc");
167        assert_eq!(query.params.table, "TransferSummary");
168        assert_eq!(query.params.limit, 50);
169    }
170
171    #[test]
172    fn enrich_transaction_row_populates_amount_fields() {
173        let row = enrich_transaction_row(RawTransactionHistoryRow {
174            block_number: 1,
175            timestamp: 1_700_000_000,
176            transaction_index: 2,
177            log_index: 3,
178            transaction_hash: alloy_primitives::TxHash::ZERO,
179            version: 2,
180            from: Address::repeat_byte(0x11),
181            to: Address::repeat_byte(0x22),
182            id: "1".into(),
183            token_address: Address::repeat_byte(0x33),
184            value: "1000000000000000000".into(),
185        })
186        .expect("enrich");
187
188        assert_eq!(
189            row.atto_circles,
190            Some(U256::from(1_000_000_000_000_000_000u64))
191        );
192        assert!(row.circles.expect("circles") > 0.0);
193        assert!(row.static_circles.expect("static circles") > 0.0);
194        assert!(row.crc.expect("crc") > 0.0);
195        assert!(row.static_atto_circles.expect("static atto") > U256::ZERO);
196        assert!(row.atto_crc.expect("atto crc") > U256::ZERO);
197    }
198}