1use crate::{
4 error::Error,
5 models::{
6 common::ApiErrorBody,
7 signing::{
8 SignMessageRequest, SignTransactionRequest, SignTransactionResponse,
9 SignTypedDataRequest, SignatureResponse,
10 },
11 token::TokenResponse,
12 transaction::{
13 AccelerateTxRequest, CancelTxRequest, CreateContractExecutionTxRequest,
14 CreateTransferTxRequest, EstimateFeeResponse, EstimateTransferFeeRequest,
15 ListTransactionsParams, TransactionResponse, Transactions, ValidateAddressRequest,
16 ValidateAddressResponse,
17 },
18 wallet::{
19 Balances, CreateWalletsRequest, ListWalletBalancesParams, ListWalletNftsParams,
20 ListWalletsParams, Nfts, UpdateWalletRequest, WalletNftsParams, WalletResponse,
21 Wallets, WalletsWithBalances,
22 },
23 wallet_set::{
24 CreateWalletSetRequest, ListWalletSetsParams, UpdateWalletSetRequest,
25 WalletSetResponse, WalletSets,
26 },
27 },
28};
29
30pub struct DeveloperWalletsClient {
32 base_url: String,
33 api_key: String,
34 http: hpx::Client,
35}
36
37impl std::fmt::Debug for DeveloperWalletsClient {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 f.debug_struct("DeveloperWalletsClient")
40 .field("base_url", &self.base_url)
41 .field("api_key", &"<redacted>")
42 .finish_non_exhaustive()
43 }
44}
45
46impl DeveloperWalletsClient {
47 pub fn new(api_key: impl Into<String>) -> Self {
49 Self::with_base_url(api_key, "https://api.circle.com")
50 }
51
52 pub fn with_base_url(api_key: impl Into<String>, base_url: impl Into<String>) -> Self {
54 Self { base_url: base_url.into(), api_key: api_key.into(), http: hpx::Client::new() }
55 }
56
57 async fn get<T, P>(&self, path: &str, params: &P) -> Result<T, Error>
59 where
60 T: serde::de::DeserializeOwned,
61 P: serde::Serialize + ?Sized,
62 {
63 let url = format!("{}{}", self.base_url, path);
64 let resp = self
65 .http
66 .get(&url)
67 .header("Authorization", format!("Bearer {}", self.api_key))
68 .header("X-Request-Id", uuid::Uuid::new_v4().to_string())
69 .query(params)
70 .send()
71 .await
72 .map_err(|e| Error::Http(e.to_string()))?;
73
74 if resp.status().is_success() {
75 resp.json::<T>().await.map_err(|e| Error::Http(e.to_string()))
76 } else {
77 let err: ApiErrorBody = resp.json().await.map_err(|e| Error::Http(e.to_string()))?;
78 Err(Error::Api { code: err.code, message: err.message })
79 }
80 }
81
82 async fn post<T, B>(&self, path: &str, body: &B) -> Result<T, Error>
84 where
85 T: serde::de::DeserializeOwned,
86 B: serde::Serialize + ?Sized,
87 {
88 let url = format!("{}{}", self.base_url, path);
89 let resp = self
90 .http
91 .post(&url)
92 .header("Authorization", format!("Bearer {}", self.api_key))
93 .header("X-Request-Id", uuid::Uuid::new_v4().to_string())
94 .json(body)
95 .send()
96 .await
97 .map_err(|e| Error::Http(e.to_string()))?;
98
99 if resp.status().is_success() {
100 resp.json::<T>().await.map_err(|e| Error::Http(e.to_string()))
101 } else {
102 let err: ApiErrorBody = resp.json().await.map_err(|e| Error::Http(e.to_string()))?;
103 Err(Error::Api { code: err.code, message: err.message })
104 }
105 }
106
107 async fn put<T, B>(&self, path: &str, body: &B) -> Result<T, Error>
109 where
110 T: serde::de::DeserializeOwned,
111 B: serde::Serialize + ?Sized,
112 {
113 let url = format!("{}{}", self.base_url, path);
114 let resp = self
115 .http
116 .put(&url)
117 .header("Authorization", format!("Bearer {}", self.api_key))
118 .header("X-Request-Id", uuid::Uuid::new_v4().to_string())
119 .json(body)
120 .send()
121 .await
122 .map_err(|e| Error::Http(e.to_string()))?;
123
124 if resp.status().is_success() {
125 resp.json::<T>().await.map_err(|e| Error::Http(e.to_string()))
126 } else {
127 let err: ApiErrorBody = resp.json().await.map_err(|e| Error::Http(e.to_string()))?;
128 Err(Error::Api { code: err.code, message: err.message })
129 }
130 }
131
132 pub async fn create_wallet_set(
136 &self,
137 req: &CreateWalletSetRequest,
138 ) -> Result<WalletSetResponse, Error> {
139 self.post("/v1/w3s/developer/walletSets", req).await
140 }
141
142 pub async fn get_wallet_set(&self, id: &str) -> Result<WalletSetResponse, Error> {
144 let path = format!("/v1/w3s/developer/walletSets/{}", id);
145 self.get(&path, &[("", "")][..0]).await
146 }
147
148 pub async fn update_wallet_set(
150 &self,
151 id: &str,
152 req: &UpdateWalletSetRequest,
153 ) -> Result<WalletSetResponse, Error> {
154 let path = format!("/v1/w3s/developer/walletSets/{}", id);
155 self.put(&path, req).await
156 }
157
158 pub async fn list_wallet_sets(
160 &self,
161 params: &ListWalletSetsParams,
162 ) -> Result<WalletSets, Error> {
163 self.get("/v1/w3s/walletSets", params).await
164 }
165
166 pub async fn create_wallets(&self, req: &CreateWalletsRequest) -> Result<Wallets, Error> {
170 self.post("/v1/w3s/developer/wallets", req).await
171 }
172
173 pub async fn list_wallets(&self, params: &ListWalletsParams) -> Result<Wallets, Error> {
175 self.get("/v1/w3s/wallets", params).await
176 }
177
178 pub async fn get_wallet(&self, id: &str) -> Result<WalletResponse, Error> {
180 let path = format!("/v1/w3s/wallets/{}", id);
181 self.get(&path, &[("", "")][..0]).await
182 }
183
184 pub async fn update_wallet(
186 &self,
187 id: &str,
188 req: &UpdateWalletRequest,
189 ) -> Result<WalletResponse, Error> {
190 let path = format!("/v1/w3s/wallets/{}", id);
191 self.put(&path, req).await
192 }
193
194 pub async fn list_wallet_balances(
196 &self,
197 params: &ListWalletBalancesParams,
198 ) -> Result<WalletsWithBalances, Error> {
199 self.get("/v1/w3s/developer/wallets/balances", params).await
200 }
201
202 pub async fn list_wallet_token_balances(
204 &self,
205 wallet_id: &str,
206 params: &WalletNftsParams,
207 ) -> Result<Balances, Error> {
208 let path = format!("/v1/w3s/wallets/{}/balances", wallet_id);
209 self.get(&path, params).await
210 }
211
212 pub async fn list_wallet_nfts(
214 &self,
215 wallet_id: &str,
216 params: &ListWalletNftsParams,
217 ) -> Result<Nfts, Error> {
218 let path = format!("/v1/w3s/wallets/{}/nfts", wallet_id);
219 self.get(&path, params).await
220 }
221
222 pub async fn sign_message(&self, req: &SignMessageRequest) -> Result<SignatureResponse, Error> {
226 self.post("/v1/w3s/developer/sign/message", req).await
227 }
228
229 pub async fn sign_typed_data(
231 &self,
232 req: &SignTypedDataRequest,
233 ) -> Result<SignatureResponse, Error> {
234 self.post("/v1/w3s/developer/sign/typedData", req).await
235 }
236
237 pub async fn sign_transaction(
239 &self,
240 req: &SignTransactionRequest,
241 ) -> Result<SignTransactionResponse, Error> {
242 self.post("/v1/w3s/developer/sign/transaction", req).await
243 }
244
245 pub async fn list_transactions(
249 &self,
250 params: &ListTransactionsParams,
251 ) -> Result<Transactions, Error> {
252 self.get("/v1/w3s/transactions", params).await
253 }
254
255 pub async fn get_transaction(&self, id: &str) -> Result<TransactionResponse, Error> {
257 let path = format!("/v1/w3s/transactions/{}", id);
258 self.get(&path, &[("", "")][..0]).await
259 }
260
261 pub async fn create_transfer_transaction(
263 &self,
264 req: &CreateTransferTxRequest,
265 ) -> Result<TransactionResponse, Error> {
266 self.post("/v1/w3s/developer/transactions/transfer", req).await
267 }
268
269 pub async fn get_fee_parameters(
271 &self,
272 req: &CreateTransferTxRequest,
273 ) -> Result<EstimateFeeResponse, Error> {
274 self.post("/v1/w3s/developer/transactions/feeParameters", req).await
275 }
276
277 pub async fn create_contract_execution_transaction(
279 &self,
280 req: &CreateContractExecutionTxRequest,
281 ) -> Result<TransactionResponse, Error> {
282 self.post("/v1/w3s/developer/transactions/contractExecution", req).await
283 }
284
285 pub async fn cancel_transaction(
287 &self,
288 id: &str,
289 req: &CancelTxRequest,
290 ) -> Result<TransactionResponse, Error> {
291 let path = format!("/v1/w3s/developer/transactions/{}/cancel", id);
292 self.post(&path, req).await
293 }
294
295 pub async fn accelerate_transaction(
297 &self,
298 id: &str,
299 req: &AccelerateTxRequest,
300 ) -> Result<TransactionResponse, Error> {
301 let path = format!("/v1/w3s/developer/transactions/{}/accelerate", id);
302 self.post(&path, req).await
303 }
304
305 pub async fn get_token(&self, id: &str) -> Result<TokenResponse, Error> {
309 let path = format!("/v1/w3s/tokens/{}", id);
310 self.get(&path, &[("", "")][..0]).await
311 }
312
313 pub async fn estimate_transfer_fee(
317 &self,
318 req: &EstimateTransferFeeRequest,
319 ) -> Result<EstimateFeeResponse, Error> {
320 self.post("/v1/w3s/transactions/transfer/estimateFee", req).await
321 }
322
323 pub async fn validate_address(
325 &self,
326 req: &ValidateAddressRequest,
327 ) -> Result<ValidateAddressResponse, Error> {
328 self.post("/v1/w3s/transactions/validateAddress", req).await
329 }
330}
331
332#[cfg(test)]
333mod tests {
334 use crate::models::transaction::TransactionState;
335
336 #[test]
338 fn transaction_state_serde_roundtrip() -> Result<(), Box<dyn std::error::Error>> {
339 let s = serde_json::to_string(&TransactionState::Complete)?;
340 assert_eq!(s, "\"COMPLETE\"");
341 Ok(())
342 }
343
344 #[test]
345 fn cancel_tx_request_serializes() -> Result<(), Box<dyn std::error::Error>> {
346 let req = crate::models::transaction::CancelTxRequest {
347 entity_secret_ciphertext: "cipher".to_string(),
348 };
349 let json = serde_json::to_string(&req)?;
350 assert!(json.contains("entitySecretCiphertext"));
351 Ok(())
352 }
353
354 #[test]
355 fn accelerate_tx_request_serializes() -> Result<(), Box<dyn std::error::Error>> {
356 let req = crate::models::transaction::AccelerateTxRequest {
357 entity_secret_ciphertext: "cipher".to_string(),
358 };
359 let json = serde_json::to_string(&req)?;
360 assert!(json.contains("entitySecretCiphertext"));
361 Ok(())
362 }
363}