1use crate::{
4 error::Error,
5 models::{
6 auth::{
7 DeviceTokenEmailRequest, DeviceTokenEmailResponse, DeviceTokenSocialRequest,
8 DeviceTokenSocialResponse, RefreshUserTokenRequest, RefreshUserTokenResponse,
9 ResendOtpRequest, ResendOtpResponse,
10 },
11 challenge::{
12 ChallengeIdResponse, ChallengeResponse, Challenges, SetPinAndInitWalletRequest,
13 SetPinRequest,
14 },
15 common::ApiErrorBody,
16 signing::{SignMessageRequest, SignTransactionRequest, SignTypedDataRequest},
17 transaction::{
18 AccelerateTxRequest, CancelTxRequest, CreateContractExecutionTxRequest,
19 CreateTransferTxRequest, CreateWalletUpgradeTxRequest, EstimateContractExecFeeRequest,
20 EstimateTransactionFee, EstimateTransferFeeRequest, GetLowestNonceTransactionResponse,
21 GetLowestNonceTxParams, ListTransactionsParams, TransactionResponse, Transactions,
22 ValidateAddressRequest, ValidateAddressResponse,
23 },
24 user::{
25 CreateUserRequest, GetUserByIdResponse, GetUserTokenRequest, ListUsersParams,
26 UserResponse, UserTokenResponse, Users,
27 },
28 wallet::{
29 Balances, CreateEndUserWalletRequest, ListWalletBalancesParams, ListWalletNftsParams,
30 ListWalletsParams, Nfts, TokenResponse, UpdateWalletRequest, WalletResponse, Wallets,
31 },
32 },
33};
34
35pub struct UserWalletsClient {
37 base_url: String,
39 api_key: String,
41 http: hpx::Client,
43}
44
45impl std::fmt::Debug for UserWalletsClient {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 f.debug_struct("UserWalletsClient")
48 .field("base_url", &self.base_url)
49 .field("api_key", &"<redacted>")
50 .finish_non_exhaustive()
51 }
52}
53
54impl UserWalletsClient {
55 pub fn new(api_key: impl Into<String>) -> Self {
57 Self::with_base_url(api_key, "https://api.circle.com")
58 }
59
60 pub fn with_base_url(api_key: impl Into<String>, base_url: impl Into<String>) -> Self {
62 Self { base_url: base_url.into(), api_key: api_key.into(), http: hpx::Client::new() }
63 }
64
65 async fn get<T, P>(&self, path: &str, params: &P) -> Result<T, Error>
69 where
70 T: serde::de::DeserializeOwned,
71 P: serde::Serialize + ?Sized,
72 {
73 let url = format!("{}{}", self.base_url, path);
74 let resp = self
75 .http
76 .get(&url)
77 .header("Authorization", format!("Bearer {}", self.api_key))
78 .header("X-Request-Id", uuid::Uuid::new_v4().to_string())
79 .query(params)
80 .send()
81 .await
82 .map_err(|e| Error::Http(e.to_string()))?;
83
84 Self::decode(resp).await
85 }
86
87 async fn post<T, B>(&self, path: &str, body: &B) -> Result<T, Error>
89 where
90 T: serde::de::DeserializeOwned,
91 B: serde::Serialize + ?Sized,
92 {
93 let url = format!("{}{}", self.base_url, path);
94 let resp = self
95 .http
96 .post(&url)
97 .header("Authorization", format!("Bearer {}", self.api_key))
98 .header("X-Request-Id", uuid::Uuid::new_v4().to_string())
99 .json(body)
100 .send()
101 .await
102 .map_err(|e| Error::Http(e.to_string()))?;
103
104 Self::decode(resp).await
105 }
106
107 #[expect(dead_code)]
109 async fn put<T, B>(&self, path: &str, body: &B) -> Result<T, Error>
110 where
111 T: serde::de::DeserializeOwned,
112 B: serde::Serialize + ?Sized,
113 {
114 let url = format!("{}{}", self.base_url, path);
115 let resp = self
116 .http
117 .put(&url)
118 .header("Authorization", format!("Bearer {}", self.api_key))
119 .header("X-Request-Id", uuid::Uuid::new_v4().to_string())
120 .json(body)
121 .send()
122 .await
123 .map_err(|e| Error::Http(e.to_string()))?;
124
125 Self::decode(resp).await
126 }
127
128 async fn get_with_user_token<T, P>(
130 &self,
131 path: &str,
132 params: &P,
133 user_token: &str,
134 ) -> Result<T, Error>
135 where
136 T: serde::de::DeserializeOwned,
137 P: serde::Serialize + ?Sized,
138 {
139 let url = format!("{}{}", self.base_url, path);
140 let resp = self
141 .http
142 .get(&url)
143 .header("Authorization", format!("Bearer {}", self.api_key))
144 .header("X-User-Token", user_token)
145 .header("X-Request-Id", uuid::Uuid::new_v4().to_string())
146 .query(params)
147 .send()
148 .await
149 .map_err(|e| Error::Http(e.to_string()))?;
150
151 Self::decode(resp).await
152 }
153
154 async fn post_with_user_token<T, B>(
156 &self,
157 path: &str,
158 body: &B,
159 user_token: &str,
160 ) -> Result<T, Error>
161 where
162 T: serde::de::DeserializeOwned,
163 B: serde::Serialize + ?Sized,
164 {
165 let url = format!("{}{}", self.base_url, path);
166 let resp = self
167 .http
168 .post(&url)
169 .header("Authorization", format!("Bearer {}", self.api_key))
170 .header("X-User-Token", user_token)
171 .header("X-Request-Id", uuid::Uuid::new_v4().to_string())
172 .json(body)
173 .send()
174 .await
175 .map_err(|e| Error::Http(e.to_string()))?;
176
177 Self::decode(resp).await
178 }
179
180 async fn put_with_user_token<T, B>(
182 &self,
183 path: &str,
184 body: &B,
185 user_token: &str,
186 ) -> Result<T, Error>
187 where
188 T: serde::de::DeserializeOwned,
189 B: serde::Serialize + ?Sized,
190 {
191 let url = format!("{}{}", self.base_url, path);
192 let resp = self
193 .http
194 .put(&url)
195 .header("Authorization", format!("Bearer {}", self.api_key))
196 .header("X-User-Token", user_token)
197 .header("X-Request-Id", uuid::Uuid::new_v4().to_string())
198 .json(body)
199 .send()
200 .await
201 .map_err(|e| Error::Http(e.to_string()))?;
202
203 Self::decode(resp).await
204 }
205
206 async fn decode<T: serde::de::DeserializeOwned>(resp: hpx::Response) -> Result<T, Error> {
208 if resp.status().is_success() {
209 resp.json::<T>().await.map_err(|e| Error::Http(e.to_string()))
210 } else {
211 let err: ApiErrorBody = resp.json().await.map_err(|e| Error::Http(e.to_string()))?;
212 Err(Error::Api { code: err.code, message: err.message })
213 }
214 }
215
216 pub async fn create_user(&self, req: &CreateUserRequest) -> Result<UserResponse, Error> {
222 self.post("/v1/w3s/users", req).await
223 }
224
225 pub async fn list_users(&self, params: &ListUsersParams) -> Result<Users, Error> {
229 self.get("/v1/w3s/users", params).await
230 }
231
232 pub async fn get_user(&self, id: &str) -> Result<GetUserByIdResponse, Error> {
236 let path = format!("/v1/w3s/users/{id}");
237 self.get(&path, &[("", "")][..0]).await
238 }
239
240 pub async fn get_user_token(
244 &self,
245 req: &GetUserTokenRequest,
246 ) -> Result<UserTokenResponse, Error> {
247 self.post("/v1/w3s/users/token", req).await
248 }
249
250 pub async fn get_device_token_social(
256 &self,
257 req: &DeviceTokenSocialRequest,
258 ) -> Result<DeviceTokenSocialResponse, Error> {
259 self.post("/v1/w3s/users/social/token", req).await
260 }
261
262 pub async fn get_device_token_email(
266 &self,
267 req: &DeviceTokenEmailRequest,
268 ) -> Result<DeviceTokenEmailResponse, Error> {
269 self.post("/v1/w3s/users/email/token", req).await
270 }
271
272 pub async fn refresh_user_token(
276 &self,
277 user_token: &str,
278 req: &RefreshUserTokenRequest,
279 ) -> Result<RefreshUserTokenResponse, Error> {
280 self.post_with_user_token("/v1/w3s/users/token/refresh", req, user_token).await
281 }
282
283 pub async fn resend_otp(
287 &self,
288 user_token: &str,
289 req: &ResendOtpRequest,
290 ) -> Result<ResendOtpResponse, Error> {
291 self.post_with_user_token("/v1/w3s/users/email/resendOTP", req, user_token).await
292 }
293
294 pub async fn get_user_by_token(&self, user_token: &str) -> Result<UserResponse, Error> {
300 self.get_with_user_token("/v1/w3s/user", &[("", "")][..0], user_token).await
301 }
302
303 pub async fn initialize_user(
309 &self,
310 user_token: &str,
311 req: &SetPinAndInitWalletRequest,
312 ) -> Result<ChallengeIdResponse, Error> {
313 self.post_with_user_token("/v1/w3s/user/initialize", req, user_token).await
314 }
315
316 pub async fn create_pin_challenge(
320 &self,
321 user_token: &str,
322 req: &SetPinRequest,
323 ) -> Result<ChallengeIdResponse, Error> {
324 self.post_with_user_token("/v1/w3s/user/pin", req, user_token).await
325 }
326
327 pub async fn update_pin_challenge(
331 &self,
332 user_token: &str,
333 req: &SetPinRequest,
334 ) -> Result<ChallengeIdResponse, Error> {
335 self.put_with_user_token("/v1/w3s/user/pin", req, user_token).await
336 }
337
338 pub async fn restore_pin_challenge(
342 &self,
343 user_token: &str,
344 req: &SetPinRequest,
345 ) -> Result<ChallengeIdResponse, Error> {
346 self.post_with_user_token("/v1/w3s/user/pin/restore", req, user_token).await
347 }
348
349 pub async fn list_challenges(&self, user_token: &str) -> Result<Challenges, Error> {
353 self.get_with_user_token("/v1/w3s/user/challenges", &[("", "")][..0], user_token).await
354 }
355
356 pub async fn get_challenge(
360 &self,
361 user_token: &str,
362 id: &str,
363 ) -> Result<ChallengeResponse, Error> {
364 let path = format!("/v1/w3s/user/challenges/{id}");
365 self.get_with_user_token(&path, &[("", "")][..0], user_token).await
366 }
367
368 pub async fn create_wallet(
377 &self,
378 user_token: &str,
379 req: &CreateEndUserWalletRequest,
380 ) -> Result<ChallengeIdResponse, Error> {
381 self.post_with_user_token("/v1/w3s/user/wallets", req, user_token).await
382 }
383
384 pub async fn list_wallets(
388 &self,
389 user_token: &str,
390 params: &ListWalletsParams,
391 ) -> Result<Wallets, Error> {
392 self.get_with_user_token("/v1/w3s/wallets", params, user_token).await
393 }
394
395 pub async fn get_wallet(&self, user_token: &str, id: &str) -> Result<WalletResponse, Error> {
399 let path = format!("/v1/w3s/wallets/{id}");
400 self.get_with_user_token(&path, &[("", "")][..0], user_token).await
401 }
402
403 pub async fn update_wallet(
407 &self,
408 user_token: &str,
409 id: &str,
410 req: &UpdateWalletRequest,
411 ) -> Result<WalletResponse, Error> {
412 let path = format!("/v1/w3s/wallets/{id}");
413 self.put_with_user_token(&path, req, user_token).await
414 }
415
416 pub async fn list_wallet_balances(
420 &self,
421 user_token: &str,
422 wallet_id: &str,
423 params: &ListWalletBalancesParams,
424 ) -> Result<Balances, Error> {
425 let path = format!("/v1/w3s/wallets/{wallet_id}/balances");
426 self.get_with_user_token(&path, params, user_token).await
427 }
428
429 pub async fn list_wallet_nfts(
433 &self,
434 user_token: &str,
435 wallet_id: &str,
436 params: &ListWalletNftsParams,
437 ) -> Result<Nfts, Error> {
438 let path = format!("/v1/w3s/wallets/{wallet_id}/nfts");
439 self.get_with_user_token(&path, params, user_token).await
440 }
441
442 pub async fn create_transfer_transaction(
448 &self,
449 user_token: &str,
450 req: &CreateTransferTxRequest,
451 ) -> Result<ChallengeIdResponse, Error> {
452 self.post_with_user_token("/v1/w3s/user/transactions/transfer", req, user_token).await
453 }
454
455 pub async fn accelerate_transaction(
459 &self,
460 user_token: &str,
461 id: &str,
462 req: &AccelerateTxRequest,
463 ) -> Result<ChallengeIdResponse, Error> {
464 let path = format!("/v1/w3s/user/transactions/{id}/accelerate");
465 self.post_with_user_token(&path, req, user_token).await
466 }
467
468 pub async fn cancel_transaction(
472 &self,
473 user_token: &str,
474 id: &str,
475 req: &CancelTxRequest,
476 ) -> Result<ChallengeIdResponse, Error> {
477 let path = format!("/v1/w3s/user/transactions/{id}/cancel");
478 self.post_with_user_token(&path, req, user_token).await
479 }
480
481 pub async fn create_contract_execution_transaction(
485 &self,
486 user_token: &str,
487 req: &CreateContractExecutionTxRequest,
488 ) -> Result<ChallengeIdResponse, Error> {
489 self.post_with_user_token("/v1/w3s/user/transactions/contractExecution", req, user_token)
490 .await
491 }
492
493 pub async fn create_wallet_upgrade_transaction(
497 &self,
498 user_token: &str,
499 req: &CreateWalletUpgradeTxRequest,
500 ) -> Result<ChallengeIdResponse, Error> {
501 self.post_with_user_token("/v1/w3s/user/transactions/walletUpgrade", req, user_token).await
502 }
503
504 pub async fn list_transactions(
508 &self,
509 user_token: &str,
510 params: &ListTransactionsParams,
511 ) -> Result<Transactions, Error> {
512 self.get_with_user_token("/v1/w3s/transactions", params, user_token).await
513 }
514
515 pub async fn get_transaction(
519 &self,
520 user_token: &str,
521 id: &str,
522 ) -> Result<TransactionResponse, Error> {
523 let path = format!("/v1/w3s/transactions/{id}");
524 self.get_with_user_token(&path, &[("", "")][..0], user_token).await
525 }
526
527 pub async fn get_lowest_nonce_transaction(
531 &self,
532 params: &GetLowestNonceTxParams,
533 ) -> Result<GetLowestNonceTransactionResponse, Error> {
534 self.get("/v1/w3s/transactions/lowestNonceTransaction", params).await
535 }
536
537 pub async fn estimate_transfer_fee(
541 &self,
542 user_token: &str,
543 req: &EstimateTransferFeeRequest,
544 ) -> Result<EstimateTransactionFee, Error> {
545 self.post_with_user_token("/v1/w3s/transactions/transfer/estimateFee", req, user_token)
546 .await
547 }
548
549 pub async fn estimate_contract_execution_fee(
553 &self,
554 user_token: &str,
555 req: &EstimateContractExecFeeRequest,
556 ) -> Result<EstimateTransactionFee, Error> {
557 self.post_with_user_token(
558 "/v1/w3s/transactions/contractExecution/estimateFee",
559 req,
560 user_token,
561 )
562 .await
563 }
564
565 pub async fn validate_address(
569 &self,
570 req: &ValidateAddressRequest,
571 ) -> Result<ValidateAddressResponse, Error> {
572 self.post("/v1/w3s/transactions/validateAddress", req).await
573 }
574
575 pub async fn get_token(&self, id: &str) -> Result<TokenResponse, Error> {
581 let path = format!("/v1/w3s/tokens/{id}");
582 self.get(&path, &[("", "")][..0]).await
583 }
584
585 pub async fn sign_message(
591 &self,
592 user_token: &str,
593 req: &SignMessageRequest,
594 ) -> Result<ChallengeIdResponse, Error> {
595 self.post_with_user_token("/v1/w3s/user/sign/message", req, user_token).await
596 }
597
598 pub async fn sign_typed_data(
602 &self,
603 user_token: &str,
604 req: &SignTypedDataRequest,
605 ) -> Result<ChallengeIdResponse, Error> {
606 self.post_with_user_token("/v1/w3s/user/sign/typedData", req, user_token).await
607 }
608
609 pub async fn sign_transaction(
613 &self,
614 user_token: &str,
615 req: &SignTransactionRequest,
616 ) -> Result<ChallengeIdResponse, Error> {
617 self.post_with_user_token("/v1/w3s/user/sign/transaction", req, user_token).await
618 }
619}