Skip to main content

rpc/
proxy.rs

1use alloy_primitives::U256;
2use reqwest::header::AUTHORIZATION;
3use reqwest::{Client, Url};
4use serde::Serialize;
5use serde::de::DeserializeOwned;
6
7use crate::{
8    ApiClientError,
9    common::{
10        AssetBalanceInfo, CollateralEventInfo, CreatePaymentTabRequest, CreatePaymentTabResult,
11        GuaranteeInfo, PendingRemunerationInfo, TabInfo, UpdateUserSuspensionRequest,
12        UserSuspensionStatus, UserTransactionInfo,
13    },
14    core::CorePublicParameters,
15    guarantee::PaymentGuaranteeRequest,
16};
17use crypto::bls::BLSCert;
18
19fn serialize_tab_id(val: U256) -> String {
20    format!("{:#x}", val)
21}
22
23#[derive(Debug, Clone)]
24pub struct RpcProxy {
25    client: Client,
26    base_url: Url,
27    bearer_token: Option<String>,
28}
29
30impl RpcProxy {
31    pub fn new(endpoint: &str) -> anyhow::Result<Self> {
32        let client = Client::builder().build()?;
33        let mut base_url = Url::parse(endpoint)?;
34        if base_url.path().is_empty() {
35            base_url.set_path("/");
36        }
37        Ok(Self {
38            client,
39            base_url,
40            bearer_token: None,
41        })
42    }
43
44    pub fn with_bearer_token(mut self, token: impl Into<String>) -> Self {
45        self.bearer_token = Some(token.into());
46        self
47    }
48
49    fn url(&self, path: &str) -> Result<Url, ApiClientError> {
50        self.base_url.join(path).map_err(ApiClientError::InvalidUrl)
51    }
52
53    fn apply_auth_header(&self, builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
54        if let Some(token) = &self.bearer_token {
55            builder.header(AUTHORIZATION, format!("Bearer {token}"))
56        } else {
57            builder
58        }
59    }
60
61    async fn get<T>(&self, url: Url) -> Result<T, ApiClientError>
62    where
63        T: DeserializeOwned,
64    {
65        let builder = self.client.get(url);
66        let builder = self.apply_auth_header(builder);
67        let response = builder.send().await?;
68        Self::decode_response(response).await
69    }
70
71    async fn post<Req, Resp>(&self, url: Url, body: &Req) -> Result<Resp, ApiClientError>
72    where
73        Req: Serialize + ?Sized,
74        Resp: DeserializeOwned,
75    {
76        let builder = self.client.post(url).json(body);
77        let builder = self.apply_auth_header(builder);
78        let response = builder.send().await?;
79        Self::decode_response(response).await
80    }
81
82    async fn decode_response<T>(response: reqwest::Response) -> Result<T, ApiClientError>
83    where
84        T: DeserializeOwned,
85    {
86        let status = response.status();
87        let bytes = response.bytes().await?;
88        if status.is_success() {
89            let value = serde_json::from_slice(&bytes)?;
90            Ok(value)
91        } else {
92            let message = parse_error_message(&bytes);
93            Err(ApiClientError::Api { status, message })
94        }
95    }
96
97    pub async fn get_public_params(&self) -> Result<CorePublicParameters, ApiClientError> {
98        let url = self.url("/core/public-params")?;
99        self.get(url).await
100    }
101
102    pub async fn issue_guarantee(
103        &self,
104        req: PaymentGuaranteeRequest,
105    ) -> Result<BLSCert, ApiClientError> {
106        let url = self.url("/core/guarantees")?;
107        self.post(url, &req).await
108    }
109
110    pub async fn create_payment_tab(
111        &self,
112        req: CreatePaymentTabRequest,
113    ) -> Result<CreatePaymentTabResult, ApiClientError> {
114        let url = self.url("/core/payment-tabs")?;
115        self.post(url, &req).await
116    }
117
118    pub async fn list_settled_tabs(
119        &self,
120        recipient_address: String,
121    ) -> Result<Vec<TabInfo>, ApiClientError> {
122        let path = format!("/core/recipients/{recipient_address}/settled-tabs");
123        let url = self.url(&path)?;
124        self.get(url).await
125    }
126
127    pub async fn list_pending_remunerations(
128        &self,
129        recipient_address: String,
130    ) -> Result<Vec<PendingRemunerationInfo>, ApiClientError> {
131        let path = format!("/core/recipients/{recipient_address}/pending-remunerations");
132        let url = self.url(&path)?;
133        self.get(url).await
134    }
135
136    pub async fn get_tab(&self, tab_id: U256) -> Result<Option<TabInfo>, ApiClientError> {
137        let path = format!("/core/tabs/{}", serialize_tab_id(tab_id));
138        let url = self.url(&path)?;
139        self.get(url).await
140    }
141
142    pub async fn list_recipient_tabs(
143        &self,
144        recipient_address: String,
145        settlement_statuses: Option<Vec<String>>,
146    ) -> Result<Vec<TabInfo>, ApiClientError> {
147        let path = format!("/core/recipients/{recipient_address}/tabs");
148        let mut url = self.url(&path)?;
149        if let Some(statuses) = settlement_statuses {
150            {
151                let mut pairs = url.query_pairs_mut();
152                for status in statuses {
153                    pairs.append_pair("settlement_status", &status);
154                }
155            }
156        }
157        self.get(url).await
158    }
159
160    pub async fn get_tab_guarantees(
161        &self,
162        tab_id: U256,
163    ) -> Result<Vec<GuaranteeInfo>, ApiClientError> {
164        let path = format!("/core/tabs/{}/guarantees", serialize_tab_id(tab_id));
165        let url = self.url(&path)?;
166        self.get(url).await
167    }
168
169    pub async fn get_latest_guarantee(
170        &self,
171        tab_id: U256,
172    ) -> Result<Option<GuaranteeInfo>, ApiClientError> {
173        let path = format!("/core/tabs/{}/guarantees/latest", serialize_tab_id(tab_id));
174        let url = self.url(&path)?;
175        self.get(url).await
176    }
177
178    pub async fn get_guarantee(
179        &self,
180        tab_id: U256,
181        req_id: U256,
182    ) -> Result<Option<GuaranteeInfo>, ApiClientError> {
183        let path = format!(
184            "/core/tabs/{}/guarantees/{}",
185            serialize_tab_id(tab_id),
186            req_id
187        );
188        let url = self.url(&path)?;
189        self.get(url).await
190    }
191
192    pub async fn list_recipient_payments(
193        &self,
194        recipient_address: String,
195    ) -> Result<Vec<UserTransactionInfo>, ApiClientError> {
196        let path = format!("/core/recipients/{recipient_address}/payments");
197        let url = self.url(&path)?;
198        self.get(url).await
199    }
200
201    pub async fn get_collateral_events_for_tab(
202        &self,
203        tab_id: U256,
204    ) -> Result<Vec<CollateralEventInfo>, ApiClientError> {
205        let path = format!("/core/tabs/{}/collateral-events", serialize_tab_id(tab_id));
206        let url = self.url(&path)?;
207        self.get(url).await
208    }
209
210    pub async fn get_user_asset_balance(
211        &self,
212        user_address: String,
213        asset_address: String,
214    ) -> Result<Option<AssetBalanceInfo>, ApiClientError> {
215        let path = format!("/core/users/{user_address}/assets/{asset_address}");
216        let url = self.url(&path)?;
217        self.get(url).await
218    }
219
220    pub async fn update_user_suspension(
221        &self,
222        user_address: String,
223        suspended: bool,
224    ) -> Result<UserSuspensionStatus, ApiClientError> {
225        let path = format!("/core/users/{user_address}/suspension");
226        let url = self.url(&path)?;
227        let body = UpdateUserSuspensionRequest { suspended };
228        self.post(url, &body).await
229    }
230}
231
232fn parse_error_message(bytes: &[u8]) -> String {
233    if bytes.is_empty() {
234        return "unknown error".to_string();
235    }
236
237    if let Ok(value) = serde_json::from_slice::<serde_json::Value>(bytes) {
238        if let Some(msg) = value.get("error").and_then(|v| v.as_str()) {
239            return msg.to_string();
240        }
241        if let Some(msg) = value.get("message").and_then(|v| v.as_str()) {
242            return msg.to_string();
243        }
244    }
245
246    match std::str::from_utf8(bytes) {
247        Ok(text) if !text.trim().is_empty() => text.trim().to_string(),
248        _ => "unknown error".to_string(),
249    }
250}