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}