Skip to main content

quicknode_sdk/admin/
mod.rs

1pub mod billing;
2pub mod bulk;
3pub mod chains;
4pub mod endpoint_metrics;
5pub mod endpoint_rate_limits;
6pub mod endpoint_security;
7pub mod endpoint_urls;
8pub mod endpoints;
9pub mod logs;
10pub mod tags;
11pub mod teams;
12pub mod usage;
13
14pub use billing::{
15    Invoice, InvoiceLine, ListInvoicesData, ListInvoicesResponse, ListPaymentsData,
16    ListPaymentsResponse, Payment,
17};
18pub use bulk::{
19    BulkAddTagData, BulkAddTagRequest, BulkAddTagResponse, BulkOperationResult, BulkRemoveTagData,
20    BulkRemoveTagRequest, BulkRemoveTagResponse, BulkTag, BulkUpdateEndpointStatusData,
21    BulkUpdateEndpointStatusRequest, BulkUpdateEndpointStatusResponse,
22};
23pub use chains::{Chain, ChainNetwork, ListChainsResponse};
24pub use endpoint_metrics::{
25    EndpointMetric, GetAccountMetricsRequest, GetAccountMetricsResponse, GetEndpointMetricsRequest,
26    GetEndpointMetricsResponse,
27};
28pub use endpoint_rate_limits::{
29    CreateMethodRateLimitRequest, CreateMethodRateLimitResponse, GetMethodRateLimitsData,
30    GetMethodRateLimitsResponse, GetRateLimitsData, GetRateLimitsResponse, MethodRateLimiter,
31    RateLimitEntry, RateLimitSettings, UpdateMethodRateLimitRequest, UpdateMethodRateLimitResponse,
32    UpdateRateLimitsRequest,
33};
34pub use endpoint_security::{
35    CreateDomainMaskRequest, CreateIpRequest, CreateJwtRequest,
36    CreateOrUpdateIpCustomHeaderRequest, CreateOrUpdateIpCustomHeaderResponse,
37    CreateReferrerRequest, CreateRequestFilterData, CreateRequestFilterRequest,
38    CreateRequestFilterResponse, DeleteBoolResponse, GetSecurityOptionsResponse,
39    IpCustomHeaderData, SecurityOption, SecurityOptionsUpdate, UpdateRequestFilterRequest,
40    UpdateSecurityOptionsRequest, UpdateSecurityOptionsResponse,
41};
42pub use endpoint_urls::{EndpointUrl, GetEndpointUrlsData, GetEndpointUrlsResponse};
43pub use endpoints::{
44    CreateEndpointRequest, CreateEndpointResponse, CreateTagRequest, Endpoint, EndpointDomainMask,
45    EndpointIp, EndpointIpCustomHeaderOption, EndpointJwt, EndpointRateLimits, EndpointReferrer,
46    EndpointRequestFilter, EndpointSecurity, EndpointSecurityOptions, EndpointTag, EndpointToken,
47    GetEndpointSecurityResponse, GetEndpointsRequest, GetEndpointsResponse, Pagination,
48    ShowEndpointResponse, SingleEndpoint, UpdateEndpointRequest, UpdateEndpointStatusRequest,
49    UpdateEndpointStatusResponse,
50};
51pub use logs::{
52    EndpointLog, GetEndpointLogsRequest, GetEndpointLogsResponse, GetLogDetailsResponse, LogDetails,
53};
54pub use tags::{
55    AccountTag, DeleteAccountTagData, DeleteAccountTagResponse, ListTagsData, ListTagsResponse,
56    RenameTagRequest, RenameTagResponse,
57};
58pub use teams::{
59    CreateTeamData, CreateTeamRequest, CreateTeamResponse, DeleteTeamData, DeleteTeamResponse,
60    GetTeamResponse, InviteTeamMemberRequest, InviteTeamMemberResponse, ListTeamEndpointsResponse,
61    ListTeamsResponse, RemoveTeamMemberRequest, RemoveTeamMemberResponse, ResendTeamInviteResponse,
62    TeamDetail, TeamEndpoint, TeamMessageData, TeamSummary, TeamUser, UpdateTeamEndpointsData,
63    UpdateTeamEndpointsRequest, UpdateTeamEndpointsResponse,
64};
65
66pub use usage::{
67    ChainUsage, EndpointUsage, GetUsageByChainResponse, GetUsageByEndpointResponse,
68    GetUsageByMethodResponse, GetUsageByTagResponse, GetUsageRequest, GetUsageResponse,
69    MethodUsage, TagUsage, UsageByChainData, UsageByEndpointData, UsageByMethodData,
70    UsageByTagData, UsageData,
71};
72
73use crate::{config::AdminConfig, errors::SdkError, SdkConfig};
74
75const ADMIN_BASE_URL: &str = "https://api.quicknode.com/v0/";
76
77pub(crate) struct ResolvedAdminConfig {
78    pub(crate) base_url: reqwest::Url,
79}
80
81impl ResolvedAdminConfig {
82    pub(crate) fn from_config(config: Option<&AdminConfig>) -> Result<Self, SdkError> {
83        let url_str = config
84            .and_then(|a| a.base_url.as_deref())
85            .unwrap_or(ADMIN_BASE_URL);
86        let mut base_url = reqwest::Url::parse(url_str)?;
87        if !base_url.path().ends_with('/') {
88            base_url.set_path(&format!("{}/", base_url.path()));
89        }
90        Ok(Self { base_url })
91    }
92}
93
94/// Client for the Quicknode Admin API. Manage endpoints, tags, teams, billing,
95/// usage/metrics, security, and rate limits on the account.
96#[derive(Debug, Clone)]
97pub struct AdminApiClient {
98    config: SdkConfig,
99}
100
101impl AdminApiClient {
102    pub fn new(config: SdkConfig) -> Self {
103        Self { config }
104    }
105
106    /// Returns a paginated list of endpoints on the account. Supports searching
107    /// by subdomain or label, filtering by networks, statuses, labels, and
108    /// tags, and sorting. The response includes endpoint metadata (id, label,
109    /// status, chain/network, HTTP and WebSocket URLs, tags) plus
110    /// total/limit/offset pagination info.
111    pub async fn get_endpoints(
112        &self,
113        params: &GetEndpointsRequest,
114    ) -> Result<GetEndpointsResponse, SdkError> {
115        let url = self.config.admin().base_url.join("endpoints")?;
116        // Build query manually: serde_urlencoded (used by reqwest's .query())
117        // rejects Vec<T> fields, but the API expects array params like
118        // networks[]=mainnet for the filter/list query string.
119        let query = endpoints_query(params);
120        let resp = self
121            .config
122            .http_client()
123            .get(url)
124            .query(&query)
125            .send()
126            .await
127            .map_err(SdkError::Http)?;
128
129        let status = resp.status();
130        let body = resp.text().await.map_err(SdkError::Http)?;
131
132        if !status.is_success() {
133            return Err(SdkError::Api { status, body });
134        }
135        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
136    }
137
138    /// Creates a new endpoint for a given blockchain and network. Requires
139    /// `chain` and `network`; returns the new endpoint with its HTTP and
140    /// WebSocket URLs, default security configuration (tokens, JWTs, IPs,
141    /// domain masks, CORS), and rate limits.
142    pub async fn create_endpoint(
143        &self,
144        params: &CreateEndpointRequest,
145    ) -> Result<CreateEndpointResponse, SdkError> {
146        let url = self.config.admin().base_url.join("endpoints")?;
147        let resp = self
148            .config
149            .http_client()
150            .post(url)
151            .json(params)
152            .send()
153            .await
154            .map_err(SdkError::Http)?;
155
156        let status = resp.status();
157        let body = resp.text().await.map_err(SdkError::Http)?;
158
159        if !status.is_success() {
160            return Err(SdkError::Api { status, body });
161        }
162        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
163    }
164
165    /// Returns details for a specific endpoint by ID.
166    pub async fn show_endpoint(&self, id: &str) -> Result<ShowEndpointResponse, SdkError> {
167        let url = self
168            .config
169            .admin()
170            .base_url
171            .join(&format!("endpoints/{}", id))?;
172        let resp = self
173            .config
174            .http_client()
175            .get(url)
176            .send()
177            .await
178            .map_err(SdkError::Http)?;
179
180        let status = resp.status();
181        let body = resp.text().await.map_err(SdkError::Http)?;
182
183        if !status.is_success() {
184            return Err(SdkError::Api { status, body });
185        }
186        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
187    }
188
189    /// Updates editable fields on an endpoint (e.g. its label). Returns a
190    /// boolean indicating whether the update succeeded.
191    pub async fn update_endpoint(
192        &self,
193        id: &str,
194        params: &UpdateEndpointRequest,
195    ) -> Result<(), SdkError> {
196        let url = self
197            .config
198            .admin()
199            .base_url
200            .join(&format!("endpoints/{}", id))?;
201        let resp = self
202            .config
203            .http_client()
204            .patch(url)
205            .json(params)
206            .send()
207            .await
208            .map_err(SdkError::Http)?;
209
210        let status = resp.status();
211        let body = resp.text().await.map_err(SdkError::Http)?;
212
213        if !status.is_success() {
214            return Err(SdkError::Api { status, body });
215        }
216        Ok(())
217    }
218
219    /// Archives an endpoint. The API uses `DELETE` but the effect is archival
220    /// rather than permanent deletion.
221    pub async fn archive_endpoint(&self, id: &str) -> Result<(), SdkError> {
222        let url = self
223            .config
224            .admin()
225            .base_url
226            .join(&format!("endpoints/{}", id))?;
227        let resp = self
228            .config
229            .http_client()
230            .delete(url)
231            .send()
232            .await
233            .map_err(SdkError::Http)?;
234
235        let status = resp.status();
236        let body = resp.text().await.map_err(SdkError::Http)?;
237
238        if !status.is_success() {
239            return Err(SdkError::Api { status, body });
240        }
241        Ok(())
242    }
243
244    /// Pauses or unpauses an endpoint by setting its status to `active` or
245    /// `paused`.
246    pub async fn update_endpoint_status(
247        &self,
248        id: &str,
249        params: &UpdateEndpointStatusRequest,
250    ) -> Result<UpdateEndpointStatusResponse, SdkError> {
251        let url = self
252            .config
253            .admin()
254            .base_url
255            .join(&format!("endpoints/{}/status", id))?;
256        let resp = self
257            .config
258            .http_client()
259            .patch(url)
260            .json(params)
261            .send()
262            .await
263            .map_err(SdkError::Http)?;
264
265        let status = resp.status();
266        let body = resp.text().await.map_err(SdkError::Http)?;
267
268        if !status.is_success() {
269            return Err(SdkError::Api { status, body });
270        }
271        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
272    }
273
274    /// Creates a new tag on a specific endpoint from a label. Returns the new
275    /// tag with its id, account info, and timestamps.
276    pub async fn create_tag(&self, id: &str, params: &CreateTagRequest) -> Result<(), SdkError> {
277        let url = self
278            .config
279            .admin()
280            .base_url
281            .join(&format!("endpoints/{}/tags", id))?;
282        let resp = self
283            .config
284            .http_client()
285            .post(url)
286            .json(params)
287            .send()
288            .await
289            .map_err(SdkError::Http)?;
290
291        let status = resp.status();
292        let body = resp.text().await.map_err(SdkError::Http)?;
293
294        if !status.is_success() {
295            return Err(SdkError::Api { status, body });
296        }
297        Ok(())
298    }
299
300    /// Removes a tag from a specific endpoint by tag id.
301    pub async fn delete_tag(&self, id: &str, tag_id: &str) -> Result<(), SdkError> {
302        let url = self
303            .config
304            .admin()
305            .base_url
306            .join(&format!("endpoints/{}/tags/{}", id, tag_id))?;
307        let resp = self
308            .config
309            .http_client()
310            .delete(url)
311            .send()
312            .await
313            .map_err(SdkError::Http)?;
314
315        let status = resp.status();
316        let body = resp.text().await.map_err(SdkError::Http)?;
317
318        if !status.is_success() {
319            return Err(SdkError::Api { status, body });
320        }
321        Ok(())
322    }
323
324    /// Returns all teams on the account. Each team includes its id, name,
325    /// member count, and member details (roles, contact info, account status).
326    pub async fn list_teams(&self) -> Result<ListTeamsResponse, SdkError> {
327        let url = self.config.admin().base_url.join("teams")?;
328        let resp = self
329            .config
330            .http_client()
331            .get(url)
332            .send()
333            .await
334            .map_err(SdkError::Http)?;
335
336        let status = resp.status();
337        let body = resp.text().await.map_err(SdkError::Http)?;
338
339        if !status.is_success() {
340            return Err(SdkError::Api { status, body });
341        }
342        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
343    }
344
345    /// Creates a new team. Requires a `name`; returns the new team with its
346    /// id, name, default role, and member count.
347    pub async fn create_team(
348        &self,
349        params: &CreateTeamRequest,
350    ) -> Result<CreateTeamResponse, SdkError> {
351        let url = self.config.admin().base_url.join("teams")?;
352        let resp = self
353            .config
354            .http_client()
355            .post(url)
356            .json(params)
357            .send()
358            .await
359            .map_err(SdkError::Http)?;
360
361        let status = resp.status();
362        let body = resp.text().await.map_err(SdkError::Http)?;
363
364        if !status.is_success() {
365            return Err(SdkError::Api { status, body });
366        }
367        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
368    }
369
370    /// Returns a specific team by id, including active members with their
371    /// roles and contact info plus any pending invites.
372    pub async fn get_team(&self, id: i64) -> Result<GetTeamResponse, SdkError> {
373        let url = self
374            .config
375            .admin()
376            .base_url
377            .join(&format!("teams/{}", id))?;
378        let resp = self
379            .config
380            .http_client()
381            .get(url)
382            .send()
383            .await
384            .map_err(SdkError::Http)?;
385
386        let status = resp.status();
387        let body = resp.text().await.map_err(SdkError::Http)?;
388
389        if !status.is_success() {
390            return Err(SdkError::Api { status, body });
391        }
392        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
393    }
394
395    /// Deletes a team by id. The team must have no members before it can be
396    /// deleted.
397    pub async fn delete_team(&self, id: i64) -> Result<DeleteTeamResponse, SdkError> {
398        let url = self
399            .config
400            .admin()
401            .base_url
402            .join(&format!("teams/{}", id))?;
403        let resp = self
404            .config
405            .http_client()
406            .delete(url)
407            .send()
408            .await
409            .map_err(SdkError::Http)?;
410
411        let status = resp.status();
412        let body = resp.text().await.map_err(SdkError::Http)?;
413
414        if !status.is_success() {
415            return Err(SdkError::Api { status, body });
416        }
417        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
418    }
419
420    /// Returns the endpoints accessible to a given team. Each entry includes
421    /// the endpoint id, subdomain, chain, and network.
422    pub async fn list_team_endpoints(
423        &self,
424        id: i64,
425    ) -> Result<ListTeamEndpointsResponse, SdkError> {
426        let url = self
427            .config
428            .admin()
429            .base_url
430            .join(&format!("teams/{}/endpoints", id))?;
431        let resp = self
432            .config
433            .http_client()
434            .get(url)
435            .send()
436            .await
437            .map_err(SdkError::Http)?;
438
439        let status = resp.status();
440        let body = resp.text().await.map_err(SdkError::Http)?;
441
442        if !status.is_success() {
443            return Err(SdkError::Api { status, body });
444        }
445        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
446    }
447
448    /// Assigns or unassigns endpoints for a team. Pass an array of endpoint ids
449    /// to set the team's accessible endpoints; pass an empty array to remove
450    /// all associations.
451    pub async fn update_team_endpoints(
452        &self,
453        id: i64,
454        params: &UpdateTeamEndpointsRequest,
455    ) -> Result<UpdateTeamEndpointsResponse, SdkError> {
456        let url = self
457            .config
458            .admin()
459            .base_url
460            .join(&format!("teams/{}/endpoints", id))?;
461        let resp = self
462            .config
463            .http_client()
464            .patch(url)
465            .json(params)
466            .send()
467            .await
468            .map_err(SdkError::Http)?;
469
470        let status = resp.status();
471        let body = resp.text().await.map_err(SdkError::Http)?;
472
473        if !status.is_success() {
474            return Err(SdkError::Api { status, body });
475        }
476        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
477    }
478
479    /// Invites a user to a team by email. For new users, `full_name` and
480    /// `role` (`admin`, `viewer`, or `billing`) are also required. Returns the
481    /// invited user's profile and invitation status.
482    pub async fn invite_team_member(
483        &self,
484        id: i64,
485        params: &InviteTeamMemberRequest,
486    ) -> Result<InviteTeamMemberResponse, SdkError> {
487        let url = self
488            .config
489            .admin()
490            .base_url
491            .join(&format!("teams/{}/members", id))?;
492        let resp = self
493            .config
494            .http_client()
495            .post(url)
496            .json(params)
497            .send()
498            .await
499            .map_err(SdkError::Http)?;
500
501        let status = resp.status();
502        let body = resp.text().await.map_err(SdkError::Http)?;
503
504        if !status.is_success() {
505            return Err(SdkError::Api { status, body });
506        }
507        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
508    }
509
510    /// Removes a user from a team by team id and user id.
511    pub async fn remove_team_member(
512        &self,
513        id: i64,
514        user_id: i64,
515        params: &RemoveTeamMemberRequest,
516    ) -> Result<RemoveTeamMemberResponse, SdkError> {
517        let url = self
518            .config
519            .admin()
520            .base_url
521            .join(&format!("teams/{}/members/{}", id, user_id))?;
522        let resp = self
523            .config
524            .http_client()
525            .delete(url)
526            .json(params)
527            .send()
528            .await
529            .map_err(SdkError::Http)?;
530
531        let status = resp.status();
532        let body = resp.text().await.map_err(SdkError::Http)?;
533
534        if !status.is_success() {
535            return Err(SdkError::Api { status, body });
536        }
537        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
538    }
539
540    /// Resends the invitation email to a pending team member, identified by
541    /// team id and user id.
542    pub async fn resend_team_invite(
543        &self,
544        id: i64,
545        user_id: i64,
546    ) -> Result<ResendTeamInviteResponse, SdkError> {
547        let url = self
548            .config
549            .admin()
550            .base_url
551            .join(&format!("teams/{}/members/{}/resend_invite", id, user_id))?;
552        let resp = self
553            .config
554            .http_client()
555            .post(url)
556            .send()
557            .await
558            .map_err(SdkError::Http)?;
559
560        let status = resp.status();
561        let body = resp.text().await.map_err(SdkError::Http)?;
562
563        if !status.is_success() {
564            return Err(SdkError::Api { status, body });
565        }
566        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
567    }
568
569    /// Returns account RPC usage totals for an optional time range. The
570    /// response includes `credits_used`, `credits_remaining`, the account
571    /// `limit`, any `overages`, and the queried time window.
572    pub async fn get_usage(&self, params: &GetUsageRequest) -> Result<GetUsageResponse, SdkError> {
573        let url = self.config.admin().base_url.join("usage/rpc")?;
574        let resp = self
575            .config
576            .http_client()
577            .get(url)
578            .query(params)
579            .send()
580            .await
581            .map_err(SdkError::Http)?;
582
583        let status = resp.status();
584        let body = resp.text().await.map_err(SdkError::Http)?;
585
586        if !status.is_success() {
587            return Err(SdkError::Api { status, body });
588        }
589        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
590    }
591
592    /// Returns RPC usage broken down per endpoint over an optional time range.
593    /// Each entry includes endpoint metadata, aggregate `credits_used` and
594    /// `requests`, and a per-method credit breakdown.
595    pub async fn get_usage_by_endpoint(
596        &self,
597        params: &GetUsageRequest,
598    ) -> Result<GetUsageByEndpointResponse, SdkError> {
599        let url = self.config.admin().base_url.join("usage/rpc/by-endpoint")?;
600        let resp = self
601            .config
602            .http_client()
603            .get(url)
604            .query(params)
605            .send()
606            .await
607            .map_err(SdkError::Http)?;
608
609        let status = resp.status();
610        let body = resp.text().await.map_err(SdkError::Http)?;
611
612        if !status.is_success() {
613            return Err(SdkError::Api { status, body });
614        }
615        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
616    }
617
618    /// Returns RPC usage grouped by method over an optional time range. Each
619    /// entry includes the method name, credits consumed, and archival status.
620    /// Ranges longer than one week are rounded to midnight UTC.
621    pub async fn get_usage_by_method(
622        &self,
623        params: &GetUsageRequest,
624    ) -> Result<GetUsageByMethodResponse, SdkError> {
625        let url = self.config.admin().base_url.join("usage/rpc/by-method")?;
626        let resp = self
627            .config
628            .http_client()
629            .get(url)
630            .query(params)
631            .send()
632            .await
633            .map_err(SdkError::Http)?;
634
635        let status = resp.status();
636        let body = resp.text().await.map_err(SdkError::Http)?;
637
638        if !status.is_success() {
639            return Err(SdkError::Api { status, body });
640        }
641        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
642    }
643
644    /// Returns RPC usage grouped by chain over an optional time range. Each
645    /// entry includes the chain and its credit consumption.
646    pub async fn get_usage_by_chain(
647        &self,
648        params: &GetUsageRequest,
649    ) -> Result<GetUsageByChainResponse, SdkError> {
650        let url = self.config.admin().base_url.join("usage/rpc/by-chain")?;
651        let resp = self
652            .config
653            .http_client()
654            .get(url)
655            .query(params)
656            .send()
657            .await
658            .map_err(SdkError::Http)?;
659
660        let status = resp.status();
661        let body = resp.text().await.map_err(SdkError::Http)?;
662
663        if !status.is_success() {
664            return Err(SdkError::Api { status, body });
665        }
666        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
667    }
668
669    /// Returns activity logs for a specific endpoint. Supports filtering by
670    /// timestamp range and pagination. Each log entry includes timestamp,
671    /// HTTP method, network, status code, and error data; full request/response
672    /// bodies can be included when requested.
673    pub async fn get_endpoint_logs(
674        &self,
675        id: &str,
676        params: &GetEndpointLogsRequest,
677    ) -> Result<GetEndpointLogsResponse, SdkError> {
678        let url = self
679            .config
680            .admin()
681            .base_url
682            .join(&format!("endpoints/{}/logs", id))?;
683        let resp = self
684            .config
685            .http_client()
686            .get(url)
687            .query(params)
688            .send()
689            .await
690            .map_err(SdkError::Http)?;
691
692        let status = resp.status();
693        let body = resp.text().await.map_err(SdkError::Http)?;
694
695        if !status.is_success() {
696            return Err(SdkError::Api { status, body });
697        }
698        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
699    }
700
701    /// Returns the raw request and response payloads for a specific log entry
702    /// on an endpoint, identified by request UUID. Both payloads are
703    /// JSON-encoded strings and are truncated at 2KB.
704    pub async fn get_log_details(
705        &self,
706        id: &str,
707        request_id: &str,
708    ) -> Result<GetLogDetailsResponse, SdkError> {
709        let url = self
710            .config
711            .admin()
712            .base_url
713            .join(&format!("endpoints/{}/log_details", id))?;
714        let resp = self
715            .config
716            .http_client()
717            .get(url)
718            .query(&[("request_id", request_id)])
719            .send()
720            .await
721            .map_err(SdkError::Http)?;
722
723        let status = resp.status();
724        let body = resp.text().await.map_err(SdkError::Http)?;
725
726        if !status.is_success() {
727            return Err(SdkError::Api { status, body });
728        }
729        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
730    }
731
732    /// Returns the security options for an endpoint — an object of security
733    /// feature toggles with their current enabled/disabled status.
734    pub async fn get_security_options(
735        &self,
736        id: &str,
737    ) -> Result<GetSecurityOptionsResponse, SdkError> {
738        let url = self
739            .config
740            .admin()
741            .base_url
742            .join(&format!("endpoints/{}/security_options", id))?;
743        let resp = self
744            .config
745            .http_client()
746            .get(url)
747            .send()
748            .await
749            .map_err(SdkError::Http)?;
750
751        let status = resp.status();
752        let body = resp.text().await.map_err(SdkError::Http)?;
753
754        if !status.is_success() {
755            return Err(SdkError::Api { status, body });
756        }
757        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
758    }
759
760    /// Updates which security features are enabled on an endpoint. Each option
761    /// in the submitted object can be toggled `enabled` or `disabled` —
762    /// examples include token auth, JWT validation, IP restrictions, CORS,
763    /// HSTS, referrer validation, and domain masking.
764    pub async fn update_security_options(
765        &self,
766        id: &str,
767        params: &UpdateSecurityOptionsRequest,
768    ) -> Result<UpdateSecurityOptionsResponse, SdkError> {
769        let url = self
770            .config
771            .admin()
772            .base_url
773            .join(&format!("endpoints/{}/security_options", id))?;
774        let resp = self
775            .config
776            .http_client()
777            .patch(url)
778            .json(params)
779            .send()
780            .await
781            .map_err(SdkError::Http)?;
782
783        let status = resp.status();
784        let body = resp.text().await.map_err(SdkError::Http)?;
785
786        if !status.is_success() {
787            return Err(SdkError::Api { status, body });
788        }
789        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
790    }
791
792    /// Generates a new authentication token for an endpoint.
793    pub async fn create_token(&self, id: &str) -> Result<(), SdkError> {
794        let url = self
795            .config
796            .admin()
797            .base_url
798            .join(&format!("endpoints/{}/security/tokens", id))?;
799        let resp = self
800            .config
801            .http_client()
802            .post(url)
803            .send()
804            .await
805            .map_err(SdkError::Http)?;
806
807        let status = resp.status();
808        let body = resp.text().await.map_err(SdkError::Http)?;
809
810        if !status.is_success() {
811            return Err(SdkError::Api { status, body });
812        }
813        Ok(())
814    }
815
816    /// Revokes a token on an endpoint by token id.
817    pub async fn delete_token(
818        &self,
819        id: &str,
820        token_id: &str,
821    ) -> Result<DeleteBoolResponse, SdkError> {
822        let url = self
823            .config
824            .admin()
825            .base_url
826            .join(&format!("endpoints/{}/security/tokens/{}", id, token_id))?;
827        let resp = self
828            .config
829            .http_client()
830            .delete(url)
831            .send()
832            .await
833            .map_err(SdkError::Http)?;
834
835        let status = resp.status();
836        let body = resp.text().await.map_err(SdkError::Http)?;
837
838        if !status.is_success() {
839            return Err(SdkError::Api { status, body });
840        }
841        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
842    }
843
844    /// Adds a referrer to an endpoint's security settings, specifying which
845    /// external URL or domain is permitted to call the endpoint.
846    pub async fn create_referrer(
847        &self,
848        id: &str,
849        params: &CreateReferrerRequest,
850    ) -> Result<(), SdkError> {
851        let url = self
852            .config
853            .admin()
854            .base_url
855            .join(&format!("endpoints/{}/security/referrers", id))?;
856        let resp = self
857            .config
858            .http_client()
859            .post(url)
860            .json(params)
861            .send()
862            .await
863            .map_err(SdkError::Http)?;
864
865        let status = resp.status();
866        let body = resp.text().await.map_err(SdkError::Http)?;
867
868        if !status.is_success() {
869            return Err(SdkError::Api { status, body });
870        }
871        Ok(())
872    }
873
874    /// Removes a referrer from an endpoint's security settings by referrer id.
875    pub async fn delete_referrer(
876        &self,
877        id: &str,
878        referrer_id: &str,
879    ) -> Result<DeleteBoolResponse, SdkError> {
880        let url = self.config.admin().base_url.join(&format!(
881            "endpoints/{}/security/referrers/{}",
882            id, referrer_id
883        ))?;
884        let resp = self
885            .config
886            .http_client()
887            .delete(url)
888            .send()
889            .await
890            .map_err(SdkError::Http)?;
891
892        let status = resp.status();
893        let body = resp.text().await.map_err(SdkError::Http)?;
894
895        if !status.is_success() {
896            return Err(SdkError::Api { status, body });
897        }
898        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
899    }
900
901    /// Adds an IP address to an endpoint's security whitelist.
902    pub async fn create_ip(&self, id: &str, params: &CreateIpRequest) -> Result<(), SdkError> {
903        let url = self
904            .config
905            .admin()
906            .base_url
907            .join(&format!("endpoints/{}/security/ips", id))?;
908        let resp = self
909            .config
910            .http_client()
911            .post(url)
912            .json(params)
913            .send()
914            .await
915            .map_err(SdkError::Http)?;
916
917        let status = resp.status();
918        let body = resp.text().await.map_err(SdkError::Http)?;
919
920        if !status.is_success() {
921            return Err(SdkError::Api { status, body });
922        }
923        Ok(())
924    }
925
926    /// Removes an IP address from an endpoint's security whitelist by ip id.
927    pub async fn delete_ip(&self, id: &str, ip_id: &str) -> Result<DeleteBoolResponse, SdkError> {
928        let url = self
929            .config
930            .admin()
931            .base_url
932            .join(&format!("endpoints/{}/security/ips/{}", id, ip_id))?;
933        let resp = self
934            .config
935            .http_client()
936            .delete(url)
937            .send()
938            .await
939            .map_err(SdkError::Http)?;
940
941        let status = resp.status();
942        let body = resp.text().await.map_err(SdkError::Http)?;
943
944        if !status.is_success() {
945            return Err(SdkError::Api { status, body });
946        }
947        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
948    }
949
950    /// Adds a domain mask to an endpoint — a custom domain used to hide the
951    /// endpoint's Quicknode URL so requests can be routed through your own
952    /// domain.
953    pub async fn create_domain_mask(
954        &self,
955        id: &str,
956        params: &CreateDomainMaskRequest,
957    ) -> Result<(), SdkError> {
958        let url = self
959            .config
960            .admin()
961            .base_url
962            .join(&format!("endpoints/{}/security/domain_masks", id))?;
963        let resp = self
964            .config
965            .http_client()
966            .post(url)
967            .json(params)
968            .send()
969            .await
970            .map_err(SdkError::Http)?;
971
972        let status = resp.status();
973        let body = resp.text().await.map_err(SdkError::Http)?;
974
975        if !status.is_success() {
976            return Err(SdkError::Api { status, body });
977        }
978        Ok(())
979    }
980
981    /// Removes a domain mask from an endpoint by domain mask id.
982    pub async fn delete_domain_mask(
983        &self,
984        id: &str,
985        domain_mask_id: &str,
986    ) -> Result<DeleteBoolResponse, SdkError> {
987        let url = self.config.admin().base_url.join(&format!(
988            "endpoints/{}/security/domain_masks/{}",
989            id, domain_mask_id
990        ))?;
991        let resp = self
992            .config
993            .http_client()
994            .delete(url)
995            .send()
996            .await
997            .map_err(SdkError::Http)?;
998
999        let status = resp.status();
1000        let body = resp.text().await.map_err(SdkError::Http)?;
1001
1002        if !status.is_success() {
1003            return Err(SdkError::Api { status, body });
1004        }
1005        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
1006    }
1007
1008    /// Creates a new JWT for endpoint authentication. Accepts a public key,
1009    /// key id (`kid`), and token name.
1010    pub async fn create_jwt(&self, id: &str, params: &CreateJwtRequest) -> Result<(), SdkError> {
1011        let url = self
1012            .config
1013            .admin()
1014            .base_url
1015            .join(&format!("endpoints/{}/security/jwts", id))?;
1016        let resp = self
1017            .config
1018            .http_client()
1019            .post(url)
1020            .json(params)
1021            .send()
1022            .await
1023            .map_err(SdkError::Http)?;
1024
1025        let status = resp.status();
1026        let body = resp.text().await.map_err(SdkError::Http)?;
1027
1028        if !status.is_success() {
1029            return Err(SdkError::Api { status, body });
1030        }
1031        Ok(())
1032    }
1033
1034    /// Removes a JWT from an endpoint's security configuration by jwt id,
1035    /// revoking its access.
1036    pub async fn delete_jwt(&self, id: &str, jwt_id: &str) -> Result<(), SdkError> {
1037        let url = self
1038            .config
1039            .admin()
1040            .base_url
1041            .join(&format!("endpoints/{}/security/jwts/{}", id, jwt_id))?;
1042        let resp = self
1043            .config
1044            .http_client()
1045            .delete(url)
1046            .send()
1047            .await
1048            .map_err(SdkError::Http)?;
1049
1050        let status = resp.status();
1051        let body = resp.text().await.map_err(SdkError::Http)?;
1052
1053        if !status.is_success() {
1054            return Err(SdkError::Api { status, body });
1055        }
1056        Ok(())
1057    }
1058
1059    /// Creates a request filter on an endpoint — a method whitelist that
1060    /// restricts which RPC methods may be called. Accepts an array of method
1061    /// names; other methods are blocked.
1062    pub async fn create_request_filter(
1063        &self,
1064        id: &str,
1065        params: &CreateRequestFilterRequest,
1066    ) -> Result<CreateRequestFilterResponse, SdkError> {
1067        let url = self
1068            .config
1069            .admin()
1070            .base_url
1071            .join(&format!("endpoints/{}/security/request_filters", id))?;
1072        let resp = self
1073            .config
1074            .http_client()
1075            .post(url)
1076            .json(params)
1077            .send()
1078            .await
1079            .map_err(SdkError::Http)?;
1080
1081        let status = resp.status();
1082        let body = resp.text().await.map_err(SdkError::Http)?;
1083
1084        if !status.is_success() {
1085            return Err(SdkError::Api { status, body });
1086        }
1087        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
1088    }
1089
1090    /// Updates an existing request filter on an endpoint, replacing the
1091    /// whitelisted method list.
1092    pub async fn update_request_filter(
1093        &self,
1094        id: &str,
1095        request_filter_id: &str,
1096        params: &UpdateRequestFilterRequest,
1097    ) -> Result<(), SdkError> {
1098        let url = self.config.admin().base_url.join(&format!(
1099            "endpoints/{}/security/request_filters/{}",
1100            id, request_filter_id
1101        ))?;
1102        let resp = self
1103            .config
1104            .http_client()
1105            .put(url)
1106            .json(params)
1107            .send()
1108            .await
1109            .map_err(SdkError::Http)?;
1110
1111        let status = resp.status();
1112        let body = resp.text().await.map_err(SdkError::Http)?;
1113
1114        if !status.is_success() {
1115            return Err(SdkError::Api { status, body });
1116        }
1117        Ok(())
1118    }
1119
1120    /// Removes a request filter from an endpoint's security configuration by
1121    /// request filter id.
1122    pub async fn delete_request_filter(
1123        &self,
1124        id: &str,
1125        request_filter_id: &str,
1126    ) -> Result<(), SdkError> {
1127        let url = self.config.admin().base_url.join(&format!(
1128            "endpoints/{}/security/request_filters/{}",
1129            id, request_filter_id
1130        ))?;
1131        let resp = self
1132            .config
1133            .http_client()
1134            .delete(url)
1135            .send()
1136            .await
1137            .map_err(SdkError::Http)?;
1138
1139        let status = resp.status();
1140        let body = resp.text().await.map_err(SdkError::Http)?;
1141
1142        if !status.is_success() {
1143            return Err(SdkError::Api { status, body });
1144        }
1145        Ok(())
1146    }
1147
1148    /// Enables multichain functionality on an endpoint, allowing a single
1149    /// endpoint to serve multiple chains.
1150    pub async fn enable_multichain(&self, id: &str) -> Result<(), SdkError> {
1151        let url = self
1152            .config
1153            .admin()
1154            .base_url
1155            .join(&format!("endpoints/{}/enable_multichain", id))?;
1156        let resp = self
1157            .config
1158            .http_client()
1159            .post(url)
1160            .send()
1161            .await
1162            .map_err(SdkError::Http)?;
1163
1164        let status = resp.status();
1165        let body = resp.text().await.map_err(SdkError::Http)?;
1166
1167        if !status.is_success() {
1168            return Err(SdkError::Api { status, body });
1169        }
1170        Ok(())
1171    }
1172
1173    /// Disables multichain functionality on an endpoint.
1174    pub async fn disable_multichain(&self, id: &str) -> Result<(), SdkError> {
1175        let url = self
1176            .config
1177            .admin()
1178            .base_url
1179            .join(&format!("endpoints/{}/disable_multichain", id))?;
1180        let resp = self
1181            .config
1182            .http_client()
1183            .post(url)
1184            .send()
1185            .await
1186            .map_err(SdkError::Http)?;
1187
1188        let status = resp.status();
1189        let body = resp.text().await.map_err(SdkError::Http)?;
1190
1191        if !status.is_success() {
1192            return Err(SdkError::Api { status, body });
1193        }
1194        Ok(())
1195    }
1196
1197    /// Sets the custom HTTP header used to identify the client IP for an
1198    /// endpoint (for example, `X-Forwarded-For`). This header is used by
1199    /// IP-based security features to resolve the real client address when
1200    /// requests are proxied.
1201    pub async fn create_or_update_ip_custom_header(
1202        &self,
1203        id: &str,
1204        params: &CreateOrUpdateIpCustomHeaderRequest,
1205    ) -> Result<CreateOrUpdateIpCustomHeaderResponse, SdkError> {
1206        let url = self
1207            .config
1208            .admin()
1209            .base_url
1210            .join(&format!("endpoints/{}/ip_custom_header", id))?;
1211        let resp = self
1212            .config
1213            .http_client()
1214            .patch(url)
1215            .json(params)
1216            .send()
1217            .await
1218            .map_err(SdkError::Http)?;
1219
1220        let status = resp.status();
1221        let body = resp.text().await.map_err(SdkError::Http)?;
1222
1223        if !status.is_success() {
1224            return Err(SdkError::Api { status, body });
1225        }
1226        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
1227    }
1228
1229    /// Removes the custom IP header configuration from an endpoint.
1230    pub async fn delete_ip_custom_header(&self, id: &str) -> Result<DeleteBoolResponse, SdkError> {
1231        let url = self
1232            .config
1233            .admin()
1234            .base_url
1235            .join(&format!("endpoints/{}/ip_custom_header", id))?;
1236        let resp = self
1237            .config
1238            .http_client()
1239            .delete(url)
1240            .send()
1241            .await
1242            .map_err(SdkError::Http)?;
1243
1244        let status = resp.status();
1245        let body = resp.text().await.map_err(SdkError::Http)?;
1246
1247        if !status.is_success() {
1248            return Err(SdkError::Api { status, body });
1249        }
1250        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
1251    }
1252
1253    /// Returns the method rate limits configured on an endpoint, including
1254    /// each limiter's interval, methods, rate, and status.
1255    pub async fn get_method_rate_limits(
1256        &self,
1257        id: &str,
1258    ) -> Result<GetMethodRateLimitsResponse, SdkError> {
1259        let url = self
1260            .config
1261            .admin()
1262            .base_url
1263            .join(&format!("endpoints/{}/method-rate-limits", id))?;
1264        let resp = self
1265            .config
1266            .http_client()
1267            .get(url)
1268            .send()
1269            .await
1270            .map_err(SdkError::Http)?;
1271
1272        let status = resp.status();
1273        let body = resp.text().await.map_err(SdkError::Http)?;
1274
1275        if !status.is_success() {
1276            return Err(SdkError::Api { status, body });
1277        }
1278        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
1279    }
1280
1281    /// Creates a per-method rate limit on an endpoint. A method rate limit
1282    /// caps specific RPC methods rather than the endpoint as a whole, defined
1283    /// by an `interval` (e.g. `second`), the target `methods`, and a `rate`.
1284    pub async fn create_method_rate_limit(
1285        &self,
1286        id: &str,
1287        params: &CreateMethodRateLimitRequest,
1288    ) -> Result<CreateMethodRateLimitResponse, SdkError> {
1289        let url = self
1290            .config
1291            .admin()
1292            .base_url
1293            .join(&format!("endpoints/{}/method-rate-limits", id))?;
1294        let resp = self
1295            .config
1296            .http_client()
1297            .post(url)
1298            .json(params)
1299            .send()
1300            .await
1301            .map_err(SdkError::Http)?;
1302
1303        let status = resp.status();
1304        let body = resp.text().await.map_err(SdkError::Http)?;
1305
1306        if !status.is_success() {
1307            return Err(SdkError::Api { status, body });
1308        }
1309        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
1310    }
1311
1312    /// Updates an existing method rate limit on an endpoint. Accepts the
1313    /// methods to apply the limit to, the desired `status`, and the `rate`.
1314    pub async fn update_method_rate_limit(
1315        &self,
1316        id: &str,
1317        method_rate_limit_id: &str,
1318        params: &UpdateMethodRateLimitRequest,
1319    ) -> Result<UpdateMethodRateLimitResponse, SdkError> {
1320        let url = self.config.admin().base_url.join(&format!(
1321            "endpoints/{}/method-rate-limits/{}",
1322            id, method_rate_limit_id
1323        ))?;
1324        let resp = self
1325            .config
1326            .http_client()
1327            .patch(url)
1328            .json(params)
1329            .send()
1330            .await
1331            .map_err(SdkError::Http)?;
1332
1333        let status = resp.status();
1334        let body = resp.text().await.map_err(SdkError::Http)?;
1335
1336        if !status.is_success() {
1337            return Err(SdkError::Api { status, body });
1338        }
1339        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
1340    }
1341
1342    /// Removes a method rate limit from an endpoint by method rate limit id.
1343    pub async fn delete_method_rate_limit(
1344        &self,
1345        id: &str,
1346        method_rate_limit_id: &str,
1347    ) -> Result<(), SdkError> {
1348        let url = self.config.admin().base_url.join(&format!(
1349            "endpoints/{}/method-rate-limits/{}",
1350            id, method_rate_limit_id
1351        ))?;
1352        let resp = self
1353            .config
1354            .http_client()
1355            .delete(url)
1356            .send()
1357            .await
1358            .map_err(SdkError::Http)?;
1359
1360        let status = resp.status();
1361        let body = resp.text().await.map_err(SdkError::Http)?;
1362
1363        if !status.is_success() {
1364            return Err(SdkError::Api { status, body });
1365        }
1366        Ok(())
1367    }
1368
1369    /// Partial update of the endpoint-level rate-limit overrides. Accepts
1370    /// `rps` (requests per second), `rpm` (requests per minute), and `rpd`
1371    /// (requests per day). Only buckets included in the request body are
1372    /// modified — omitted buckets are left unchanged. Values are capped by the
1373    /// account's plan tier.
1374    pub async fn update_rate_limits(
1375        &self,
1376        id: &str,
1377        params: &UpdateRateLimitsRequest,
1378    ) -> Result<(), SdkError> {
1379        let url = self
1380            .config
1381            .admin()
1382            .base_url
1383            .join(&format!("endpoints/{}/rate-limits", id))?;
1384        let resp = self
1385            .config
1386            .http_client()
1387            .patch(url)
1388            .json(params)
1389            .send()
1390            .await
1391            .map_err(SdkError::Http)?;
1392
1393        let status = resp.status();
1394        let body = resp.text().await.map_err(SdkError::Http)?;
1395
1396        if !status.is_success() {
1397            return Err(SdkError::Api { status, body });
1398        }
1399        Ok(())
1400    }
1401
1402    /// Returns the endpoint-level rate limits currently enforced, with each
1403    /// row identifying its bucket (`rps`/`rpm`/`rpd`), value, and source
1404    /// (`plan_default` or `user_override`). User-set overrides expose an
1405    /// `override_id` that can be passed to `delete_rate_limit_override`.
1406    pub async fn get_rate_limits(&self, id: &str) -> Result<GetRateLimitsResponse, SdkError> {
1407        let url = self
1408            .config
1409            .admin()
1410            .base_url
1411            .join(&format!("endpoints/{}/rate-limits", id))?;
1412        let resp = self
1413            .config
1414            .http_client()
1415            .get(url)
1416            .send()
1417            .await
1418            .map_err(SdkError::Http)?;
1419
1420        let status = resp.status();
1421        let body = resp.text().await.map_err(SdkError::Http)?;
1422
1423        if !status.is_success() {
1424            return Err(SdkError::Api { status, body });
1425        }
1426        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
1427    }
1428
1429    /// Deletes a user-set rate-limit override by its UUID. Plan defaults are
1430    /// not deletable — passing a UUID that does not match a user-set override
1431    /// on the endpoint returns 404.
1432    pub async fn delete_rate_limit_override(
1433        &self,
1434        id: &str,
1435        override_id: &str,
1436    ) -> Result<(), SdkError> {
1437        let url = self
1438            .config
1439            .admin()
1440            .base_url
1441            .join(&format!("endpoints/{}/rate-limits/{}", id, override_id))?;
1442        let resp = self
1443            .config
1444            .http_client()
1445            .delete(url)
1446            .send()
1447            .await
1448            .map_err(SdkError::Http)?;
1449
1450        let status = resp.status();
1451        let body = resp.text().await.map_err(SdkError::Http)?;
1452
1453        if !status.is_success() {
1454            return Err(SdkError::Api { status, body });
1455        }
1456        Ok(())
1457    }
1458
1459    /// Returns the HTTP and WebSocket URLs for the endpoint without fetching
1460    /// the full endpoint record. For multichain endpoints, `multichain_urls`
1461    /// is a per-network map of additional URLs; for single-chain endpoints it
1462    /// is `None`.
1463    pub async fn get_endpoint_urls(&self, id: &str) -> Result<GetEndpointUrlsResponse, SdkError> {
1464        let url = self
1465            .config
1466            .admin()
1467            .base_url
1468            .join(&format!("endpoints/{}/urls", id))?;
1469        let resp = self
1470            .config
1471            .http_client()
1472            .get(url)
1473            .send()
1474            .await
1475            .map_err(SdkError::Http)?;
1476
1477        let status = resp.status();
1478        let body = resp.text().await.map_err(SdkError::Http)?;
1479
1480        if !status.is_success() {
1481            return Err(SdkError::Api { status, body });
1482        }
1483        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
1484    }
1485
1486    /// Returns time-series metrics for a specific endpoint. Requires a
1487    /// `period` (`hour`, `day`, `week`, or `month`) and a metric type such as
1488    /// `method_calls_over_time` or `response_status_breakdown`.
1489    pub async fn get_endpoint_metrics(
1490        &self,
1491        id: &str,
1492        params: &GetEndpointMetricsRequest,
1493    ) -> Result<GetEndpointMetricsResponse, SdkError> {
1494        let url = self
1495            .config
1496            .admin()
1497            .base_url
1498            .join(&format!("endpoints/{}/metrics", id))?;
1499        let resp = self
1500            .config
1501            .http_client()
1502            .get(url)
1503            .query(params)
1504            .send()
1505            .await
1506            .map_err(SdkError::Http)?;
1507
1508        let status = resp.status();
1509        let body = resp.text().await.map_err(SdkError::Http)?;
1510
1511        if !status.is_success() {
1512            return Err(SdkError::Api { status, body });
1513        }
1514        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
1515    }
1516
1517    /// Returns aggregated metrics across all endpoints on the account. Accepts
1518    /// a `period` (`hour`, `day`, `week`, or `month`) and a metric type such
1519    /// as `method_calls_over_time` or `credits_over_time`.
1520    pub async fn get_account_metrics(
1521        &self,
1522        params: &GetAccountMetricsRequest,
1523    ) -> Result<GetAccountMetricsResponse, SdkError> {
1524        let url = self.config.admin().base_url.join("metrics")?;
1525        let resp = self
1526            .config
1527            .http_client()
1528            .get(url)
1529            .query(params)
1530            .send()
1531            .await
1532            .map_err(SdkError::Http)?;
1533
1534        let status = resp.status();
1535        let body = resp.text().await.map_err(SdkError::Http)?;
1536
1537        if !status.is_success() {
1538            return Err(SdkError::Api { status, body });
1539        }
1540        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
1541    }
1542
1543    /// Returns all chains supported by Quicknode along with their networks.
1544    /// Each entry includes the chain slug and its network slugs and names.
1545    pub async fn list_chains(&self) -> Result<ListChainsResponse, SdkError> {
1546        let url = self.config.admin().base_url.join("chains")?;
1547        let resp = self
1548            .config
1549            .http_client()
1550            .get(url)
1551            .send()
1552            .await
1553            .map_err(SdkError::Http)?;
1554
1555        let status = resp.status();
1556        let body = resp.text().await.map_err(SdkError::Http)?;
1557
1558        if !status.is_success() {
1559            return Err(SdkError::Api { status, body });
1560        }
1561        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
1562    }
1563
1564    /// Returns the account's invoices, including id, status, billing reason,
1565    /// amounts due and paid, line items with descriptions and billing periods,
1566    /// and creation timestamps.
1567    pub async fn list_invoices(&self) -> Result<ListInvoicesResponse, SdkError> {
1568        let url = self.config.admin().base_url.join("billing/invoices")?;
1569        let resp = self
1570            .config
1571            .http_client()
1572            .get(url)
1573            .send()
1574            .await
1575            .map_err(SdkError::Http)?;
1576
1577        let status = resp.status();
1578        let body = resp.text().await.map_err(SdkError::Http)?;
1579
1580        if !status.is_success() {
1581            return Err(SdkError::Api { status, body });
1582        }
1583        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
1584    }
1585
1586    /// Returns all payments on the account, including amount, status, card
1587    /// last-four, timestamp, currency, and marketplace spending.
1588    pub async fn list_payments(&self) -> Result<ListPaymentsResponse, SdkError> {
1589        let url = self.config.admin().base_url.join("billing/payments")?;
1590        let resp = self
1591            .config
1592            .http_client()
1593            .get(url)
1594            .send()
1595            .await
1596            .map_err(SdkError::Http)?;
1597
1598        let status = resp.status();
1599        let body = resp.text().await.map_err(SdkError::Http)?;
1600
1601        if !status.is_success() {
1602            return Err(SdkError::Api { status, body });
1603        }
1604        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
1605    }
1606
1607    /// Pauses or unpauses multiple endpoints in a single call. Accepts an
1608    /// array of endpoint ids and a target status (`active` or `paused`);
1609    /// returns per-endpoint success/failure results plus totals.
1610    pub async fn bulk_update_endpoint_status(
1611        &self,
1612        params: &BulkUpdateEndpointStatusRequest,
1613    ) -> Result<BulkUpdateEndpointStatusResponse, SdkError> {
1614        if params.ids.is_empty() {
1615            return Err(SdkError::Config(
1616                "bulk_update_endpoint_status requires at least one id".into(),
1617            ));
1618        }
1619        let url = self.config.admin().base_url.join("endpoints/bulk/status")?;
1620        let resp = self
1621            .config
1622            .http_client()
1623            .post(url)
1624            .json(params)
1625            .send()
1626            .await
1627            .map_err(SdkError::Http)?;
1628
1629        let status = resp.status();
1630        let body = resp.text().await.map_err(SdkError::Http)?;
1631
1632        if !status.is_success() {
1633            return Err(SdkError::Api { status, body });
1634        }
1635        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
1636    }
1637
1638    /// Applies a single tag label to multiple endpoints in one call. Returns
1639    /// totals for affected endpoints, successes, and failures, plus the tag
1640    /// that was applied.
1641    pub async fn bulk_add_tag(
1642        &self,
1643        params: &BulkAddTagRequest,
1644    ) -> Result<BulkAddTagResponse, SdkError> {
1645        if params.ids.is_empty() {
1646            return Err(SdkError::Config(
1647                "bulk_add_tag requires at least one id".into(),
1648            ));
1649        }
1650        let url = self.config.admin().base_url.join("endpoints/bulk/tags")?;
1651        let resp = self
1652            .config
1653            .http_client()
1654            .post(url)
1655            .json(params)
1656            .send()
1657            .await
1658            .map_err(SdkError::Http)?;
1659
1660        let status = resp.status();
1661        let body = resp.text().await.map_err(SdkError::Http)?;
1662
1663        if !status.is_success() {
1664            return Err(SdkError::Api { status, body });
1665        }
1666        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
1667    }
1668
1669    /// Removes a tag from multiple endpoints in one call, identified by an
1670    /// array of endpoint ids and a tag id.
1671    pub async fn bulk_remove_tag(
1672        &self,
1673        params: &BulkRemoveTagRequest,
1674    ) -> Result<BulkRemoveTagResponse, SdkError> {
1675        // Empty ids on a DELETE-with-body is high blast radius: some proxies
1676        // strip DELETE bodies, and an empty batch could be misinterpreted by
1677        // the server. Fail fast client-side before firing the request.
1678        if params.ids.is_empty() {
1679            return Err(SdkError::Config(
1680                "bulk_remove_tag requires at least one id".into(),
1681            ));
1682        }
1683        let url = self.config.admin().base_url.join("endpoints/bulk/tags")?;
1684        let resp = self
1685            .config
1686            .http_client()
1687            .delete(url)
1688            .json(params)
1689            .send()
1690            .await
1691            .map_err(SdkError::Http)?;
1692
1693        let status = resp.status();
1694        let body = resp.text().await.map_err(SdkError::Http)?;
1695
1696        if !status.is_success() {
1697            return Err(SdkError::Api { status, body });
1698        }
1699        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
1700    }
1701
1702    /// Returns all account-level tags, including tags with zero associated
1703    /// endpoints. Each tag includes its id, label, and endpoint usage count.
1704    pub async fn list_tags(&self) -> Result<ListTagsResponse, SdkError> {
1705        let url = self.config.admin().base_url.join("endpoints/tags")?;
1706        let resp = self
1707            .config
1708            .http_client()
1709            .get(url)
1710            .send()
1711            .await
1712            .map_err(SdkError::Http)?;
1713
1714        let status = resp.status();
1715        let body = resp.text().await.map_err(SdkError::Http)?;
1716
1717        if !status.is_success() {
1718            return Err(SdkError::Api { status, body });
1719        }
1720        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
1721    }
1722
1723    /// Updates the label of an account tag. Because the tag is shared across
1724    /// endpoints, all associated endpoints reflect the new label immediately.
1725    pub async fn rename_tag(
1726        &self,
1727        id: i32,
1728        params: &RenameTagRequest,
1729    ) -> Result<RenameTagResponse, SdkError> {
1730        let url = self
1731            .config
1732            .admin()
1733            .base_url
1734            .join(&format!("endpoints/tags/{}", id))?;
1735        let resp = self
1736            .config
1737            .http_client()
1738            .patch(url)
1739            .json(params)
1740            .send()
1741            .await
1742            .map_err(SdkError::Http)?;
1743
1744        let status = resp.status();
1745        let body = resp.text().await.map_err(SdkError::Http)?;
1746
1747        if !status.is_success() {
1748            return Err(SdkError::Api { status, body });
1749        }
1750        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
1751    }
1752
1753    // Named delete_account_tag to avoid collision with the existing per-endpoint
1754    // delete_tag(id, tag_id). OpenAPI reuses the deleteTag operationId for both.
1755    /// Deletes an account-level tag. The tag must first be removed from all
1756    /// endpoints before it can be deleted.
1757    pub async fn delete_account_tag(&self, id: i32) -> Result<DeleteAccountTagResponse, SdkError> {
1758        let url = self
1759            .config
1760            .admin()
1761            .base_url
1762            .join(&format!("endpoints/tags/{}", id))?;
1763        let resp = self
1764            .config
1765            .http_client()
1766            .delete(url)
1767            .send()
1768            .await
1769            .map_err(SdkError::Http)?;
1770
1771        let status = resp.status();
1772        let body = resp.text().await.map_err(SdkError::Http)?;
1773
1774        if !status.is_success() {
1775            return Err(SdkError::Api { status, body });
1776        }
1777        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
1778    }
1779
1780    /// Returns RPC usage grouped by endpoint tag over an optional time range.
1781    /// Each entry includes the tag id, label, credits consumed, and request
1782    /// count.
1783    pub async fn get_usage_by_tag(
1784        &self,
1785        params: &GetUsageRequest,
1786    ) -> Result<GetUsageByTagResponse, SdkError> {
1787        let url = self.config.admin().base_url.join("usage/rpc/by-tag")?;
1788        let resp = self
1789            .config
1790            .http_client()
1791            .get(url)
1792            .query(params)
1793            .send()
1794            .await
1795            .map_err(SdkError::Http)?;
1796
1797        let status = resp.status();
1798        let body = resp.text().await.map_err(SdkError::Http)?;
1799
1800        if !status.is_success() {
1801            return Err(SdkError::Api { status, body });
1802        }
1803        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
1804    }
1805
1806    /// Returns the full security configuration for an endpoint in a single
1807    /// call, without loading the entire endpoint object. The response includes
1808    /// tokens, JWTs, referrers, domain masks, IPs, and a security options
1809    /// object describing which features are enabled.
1810    pub async fn get_endpoint_security(
1811        &self,
1812        id: &str,
1813    ) -> Result<GetEndpointSecurityResponse, SdkError> {
1814        let url = self
1815            .config
1816            .admin()
1817            .base_url
1818            .join(&format!("endpoints/{}/security", id))?;
1819        let resp = self
1820            .config
1821            .http_client()
1822            .get(url)
1823            .send()
1824            .await
1825            .map_err(SdkError::Http)?;
1826
1827        let status = resp.status();
1828        let body = resp.text().await.map_err(SdkError::Http)?;
1829
1830        if !status.is_success() {
1831            return Err(SdkError::Api { status, body });
1832        }
1833        serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
1834    }
1835}
1836
1837fn endpoints_query(params: &GetEndpointsRequest) -> Vec<(&'static str, String)> {
1838    let mut q: Vec<(&'static str, String)> = Vec::new();
1839    if let Some(v) = params.limit {
1840        q.push(("limit", v.to_string()));
1841    }
1842    if let Some(v) = params.offset {
1843        q.push(("offset", v.to_string()));
1844    }
1845    if let Some(ref v) = params.search {
1846        q.push(("search", v.clone()));
1847    }
1848    if let Some(ref v) = params.sort_by {
1849        q.push(("sort_by", v.clone()));
1850    }
1851    if let Some(ref v) = params.sort_direction {
1852        q.push(("sort_direction", v.clone()));
1853    }
1854    if let Some(ref list) = params.networks {
1855        for item in list {
1856            q.push(("networks[]", item.clone()));
1857        }
1858    }
1859    if let Some(ref list) = params.statuses {
1860        for item in list {
1861            q.push(("statuses[]", item.clone()));
1862        }
1863    }
1864    if let Some(ref list) = params.labels {
1865        for item in list {
1866            q.push(("labels[]", item.clone()));
1867        }
1868    }
1869    if let Some(v) = params.dedicated {
1870        q.push(("dedicated", v.to_string()));
1871    }
1872    if let Some(v) = params.is_flat_rate {
1873        q.push(("is_flat_rate", v.to_string()));
1874    }
1875    if let Some(ref list) = params.tag_ids {
1876        for item in list {
1877            q.push(("tag_ids[]", item.to_string()));
1878        }
1879    }
1880    if let Some(ref list) = params.tag_labels {
1881        for item in list {
1882            q.push(("tag_labels[]", item.clone()));
1883        }
1884    }
1885    q
1886}
1887
1888#[cfg(test)]
1889#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
1890mod tests {
1891    use super::*;
1892    use crate::{AdminConfig, QuicknodeSdk, SdkFullConfig};
1893    use wiremock::matchers::{method, path, query_param};
1894    use wiremock::{Mock, MockServer, ResponseTemplate};
1895
1896    fn make_sdk(base_url: String) -> QuicknodeSdk {
1897        QuicknodeSdk::new(&SdkFullConfig {
1898            api_key: "test-key".to_string(),
1899            http: None,
1900            admin: Some(AdminConfig {
1901                base_url: Some(base_url),
1902            }),
1903            streams: None,
1904            webhooks: None,
1905            kvstore: None,
1906            sql: None,
1907        })
1908        .unwrap()
1909    }
1910
1911    #[tokio::test]
1912    async fn get_endpoints_success() {
1913        let server = MockServer::start().await;
1914
1915        Mock::given(method("GET"))
1916            .and(path("/endpoints"))
1917            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1918                "data": [
1919                    {
1920                        "id": "abc123",
1921                        "name": "aged-intensive-patron",
1922                        "label": "My Endpoint",
1923                        "status": "active",
1924                        "chain": "ethereum",
1925                        "network": "mainnet",
1926                        "is_dedicated": false,
1927                        "is_flat_rate": true,
1928                        "http_url": "https://example.quicknode.pro/abc123",
1929                        "wss_url": null,
1930                        "tags": []
1931                    }
1932                ],
1933                "pagination": {
1934                    "total": 1,
1935                    "limit": 20,
1936                    "offset": 0
1937                },
1938                "error": null
1939            })))
1940            .mount(&server)
1941            .await;
1942
1943        let sdk = make_sdk(format!("{}/", server.uri()));
1944        let resp = sdk
1945            .admin
1946            .get_endpoints(&GetEndpointsRequest::default())
1947            .await
1948            .unwrap();
1949
1950        assert_eq!(resp.data.len(), 1);
1951        assert_eq!(resp.data[0].id, "abc123");
1952        assert_eq!(resp.data[0].name, "aged-intensive-patron");
1953        assert_eq!(resp.data[0].status, "active");
1954        assert_eq!(resp.data[0].chain, "ethereum");
1955        assert!(!resp.data[0].is_dedicated);
1956        assert!(resp.data[0].is_flat_rate);
1957        let pagination = resp.pagination.expect("pagination present");
1958        assert_eq!(pagination.total, 1);
1959        assert_eq!(pagination.limit, 20);
1960        assert_eq!(pagination.offset, 0);
1961    }
1962
1963    #[tokio::test]
1964    async fn get_endpoints_sends_search_and_filter_params() {
1965        let server = MockServer::start().await;
1966
1967        Mock::given(method("GET"))
1968            .and(path("/endpoints"))
1969            .and(query_param("search", "intensive"))
1970            .and(query_param("networks[]", "mainnet"))
1971            .and(query_param("statuses[]", "active"))
1972            .and(query_param("dedicated", "true"))
1973            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1974                "data": [],
1975                "error": null
1976            })))
1977            .mount(&server)
1978            .await;
1979
1980        let sdk = make_sdk(format!("{}/", server.uri()));
1981        let params = GetEndpointsRequest {
1982            search: Some("intensive".to_string()),
1983            networks: Some(vec!["mainnet".to_string()]),
1984            statuses: Some(vec!["active".to_string()]),
1985            dedicated: Some(true),
1986            ..Default::default()
1987        };
1988        let resp = sdk.admin.get_endpoints(&params).await.unwrap();
1989
1990        assert_eq!(resp.data.len(), 0);
1991    }
1992
1993    #[tokio::test]
1994    async fn get_endpoints_api_error() {
1995        let server = MockServer::start().await;
1996
1997        Mock::given(method("GET"))
1998            .and(path("/endpoints"))
1999            .respond_with(ResponseTemplate::new(401).set_body_string("Unauthorized"))
2000            .mount(&server)
2001            .await;
2002
2003        let sdk = make_sdk(format!("{}/", server.uri()));
2004        let err = sdk
2005            .admin
2006            .get_endpoints(&GetEndpointsRequest::default())
2007            .await
2008            .unwrap_err();
2009
2010        match err {
2011            SdkError::Api { status, .. } => assert_eq!(status.as_u16(), 401),
2012            other => panic!("expected SdkError::Api, got {:?}", other),
2013        }
2014    }
2015
2016    #[tokio::test]
2017    async fn get_endpoints_sends_query_params() {
2018        let server = MockServer::start().await;
2019
2020        Mock::given(method("GET"))
2021            .and(path("/endpoints"))
2022            .and(query_param("limit", "10"))
2023            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2024                "data": [],
2025                "error": null
2026            })))
2027            .mount(&server)
2028            .await;
2029
2030        let sdk = make_sdk(format!("{}/", server.uri()));
2031        let params = GetEndpointsRequest {
2032            limit: Some(10),
2033            ..Default::default()
2034        };
2035        let resp = sdk.admin.get_endpoints(&params).await.unwrap();
2036
2037        assert_eq!(resp.data.len(), 0);
2038    }
2039
2040    #[tokio::test]
2041    async fn get_endpoints_base_url_without_trailing_slash() {
2042        let server = MockServer::start().await;
2043
2044        Mock::given(method("GET"))
2045            .and(path("/endpoints"))
2046            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2047                "data": [],
2048                "error": null
2049            })))
2050            .mount(&server)
2051            .await;
2052
2053        let base_url_no_slash = server.uri();
2054        let sdk = make_sdk(base_url_no_slash);
2055        let resp = sdk
2056            .admin
2057            .get_endpoints(&GetEndpointsRequest::default())
2058            .await
2059            .unwrap();
2060
2061        assert_eq!(resp.data.len(), 0);
2062    }
2063
2064    #[tokio::test]
2065    async fn create_endpoint_success() {
2066        let server = MockServer::start().await;
2067
2068        Mock::given(method("POST"))
2069            .and(path("/endpoints"))
2070            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2071                "data": {
2072                    "id": "ep123",
2073                    "label": null,
2074                    "status": "active",
2075                    "chain": "ethereum",
2076                    "network": "mainnet",
2077                    "http_url": "https://example.quicknode.pro/ep123",
2078                    "wss_url": null,
2079                    "security": {
2080                        "options": { "tokens": true, "jwts": false, "domainMasks": false, "ips": false, "referrers": false, "requestFilters": false },
2081                        "tokens": [{"id": "tok1", "token": "abc123"}],
2082                        "jwts": null,
2083                        "referrers": null,
2084                        "domain_masks": null,
2085                        "ips": null,
2086                        "request_filters": null
2087                    },
2088                    "rate_limits": null,
2089                    "tags": []
2090                },
2091                "error": null
2092            })))
2093            .mount(&server)
2094            .await;
2095
2096        let sdk = make_sdk(format!("{}/", server.uri()));
2097        let resp = sdk
2098            .admin
2099            .create_endpoint(&CreateEndpointRequest::default())
2100            .await
2101            .unwrap();
2102
2103        assert_eq!(resp.data.id, "ep123");
2104        assert_eq!(resp.data.chain, "ethereum");
2105        assert_eq!(resp.data.network, "mainnet");
2106        let security = resp.data.security.unwrap();
2107        assert!(security.tokens.unwrap().len() == 1);
2108        assert!(security.jwts.is_none());
2109    }
2110
2111    #[tokio::test]
2112    async fn create_endpoint_api_error() {
2113        let server = MockServer::start().await;
2114
2115        Mock::given(method("POST"))
2116            .and(path("/endpoints"))
2117            .respond_with(ResponseTemplate::new(400).set_body_string("Bad Request"))
2118            .mount(&server)
2119            .await;
2120
2121        let sdk = make_sdk(format!("{}/", server.uri()));
2122        let err = sdk
2123            .admin
2124            .create_endpoint(&CreateEndpointRequest::default())
2125            .await
2126            .unwrap_err();
2127
2128        match err {
2129            SdkError::Api { status, .. } => assert_eq!(status.as_u16(), 400),
2130            other => panic!("expected SdkError::Api, got {:?}", other),
2131        }
2132    }
2133
2134    #[tokio::test]
2135    async fn create_endpoint_sends_body() {
2136        use wiremock::matchers::body_json;
2137
2138        let server = MockServer::start().await;
2139
2140        Mock::given(method("POST"))
2141            .and(path("/endpoints"))
2142            .and(body_json(serde_json::json!({
2143                "chain": "solana",
2144                "network": "mainnet-beta"
2145            })))
2146            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2147                "data": {
2148                    "id": "ep456",
2149                    "label": null,
2150                    "status": "active",
2151                    "chain": "solana",
2152                    "network": "mainnet-beta",
2153                    "http_url": "https://example.quicknode.pro/ep456",
2154                    "wss_url": null,
2155                    "security": null,
2156                    "rate_limits": null,
2157                    "tags": []
2158                },
2159                "error": null
2160            })))
2161            .mount(&server)
2162            .await;
2163
2164        let sdk = make_sdk(format!("{}/", server.uri()));
2165        let params = CreateEndpointRequest {
2166            chain: Some("solana".to_string()),
2167            network: Some("mainnet-beta".to_string()),
2168        };
2169        let resp = sdk.admin.create_endpoint(&params).await.unwrap();
2170
2171        assert_eq!(resp.data.id, "ep456");
2172        assert_eq!(resp.data.chain, "solana");
2173    }
2174
2175    #[tokio::test]
2176    async fn show_endpoint_success() {
2177        let server = MockServer::start().await;
2178
2179        Mock::given(method("GET"))
2180            .and(path("/endpoints/ep123"))
2181            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2182                "data": {
2183                    "id": "ep123",
2184                    "label": null,
2185                    "status": "active",
2186                    "chain": "ethereum",
2187                    "network": "mainnet",
2188                    "http_url": "https://example.quicknode.pro/ep123",
2189                    "wss_url": null,
2190                    "security": null,
2191                    "rate_limits": null,
2192                    "tags": []
2193                },
2194                "error": null
2195            })))
2196            .mount(&server)
2197            .await;
2198
2199        let sdk = make_sdk(format!("{}/", server.uri()));
2200        let resp = sdk.admin.show_endpoint("ep123").await.unwrap();
2201        assert_eq!(resp.data.unwrap().id, "ep123");
2202    }
2203
2204    #[tokio::test]
2205    async fn show_endpoint_api_error() {
2206        let server = MockServer::start().await;
2207
2208        Mock::given(method("GET"))
2209            .and(path("/endpoints/ep123"))
2210            .respond_with(ResponseTemplate::new(404).set_body_string("Not Found"))
2211            .mount(&server)
2212            .await;
2213
2214        let sdk = make_sdk(format!("{}/", server.uri()));
2215        let err = sdk.admin.show_endpoint("ep123").await.unwrap_err();
2216        match err {
2217            SdkError::Api { status, .. } => assert_eq!(status.as_u16(), 404),
2218            other => panic!("expected SdkError::Api, got {:?}", other),
2219        }
2220    }
2221
2222    #[tokio::test]
2223    async fn update_endpoint_success() {
2224        let server = MockServer::start().await;
2225
2226        Mock::given(method("PATCH"))
2227            .and(path("/endpoints/ep123"))
2228            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({})))
2229            .mount(&server)
2230            .await;
2231
2232        let sdk = make_sdk(format!("{}/", server.uri()));
2233        sdk.admin
2234            .update_endpoint(
2235                "ep123",
2236                &UpdateEndpointRequest {
2237                    label: Some("New Name".to_string()),
2238                },
2239            )
2240            .await
2241            .unwrap();
2242    }
2243
2244    #[tokio::test]
2245    async fn archive_endpoint_success() {
2246        let server = MockServer::start().await;
2247
2248        Mock::given(method("DELETE"))
2249            .and(path("/endpoints/ep123"))
2250            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({})))
2251            .mount(&server)
2252            .await;
2253
2254        let sdk = make_sdk(format!("{}/", server.uri()));
2255        sdk.admin.archive_endpoint("ep123").await.unwrap();
2256    }
2257
2258    #[tokio::test]
2259    async fn update_endpoint_status_success() {
2260        let server = MockServer::start().await;
2261
2262        Mock::given(method("PATCH"))
2263            .and(path("/endpoints/ep123/status"))
2264            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2265                "data": "paused",
2266                "error": null
2267            })))
2268            .mount(&server)
2269            .await;
2270
2271        let sdk = make_sdk(format!("{}/", server.uri()));
2272        let resp = sdk
2273            .admin
2274            .update_endpoint_status(
2275                "ep123",
2276                &UpdateEndpointStatusRequest {
2277                    status: "paused".to_string(),
2278                },
2279            )
2280            .await
2281            .unwrap();
2282        assert_eq!(resp.data.unwrap(), "paused");
2283    }
2284
2285    #[tokio::test]
2286    async fn create_tag_success() {
2287        let server = MockServer::start().await;
2288
2289        Mock::given(method("POST"))
2290            .and(path("/endpoints/ep123/tags"))
2291            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({})))
2292            .mount(&server)
2293            .await;
2294
2295        let sdk = make_sdk(format!("{}/", server.uri()));
2296        sdk.admin
2297            .create_tag(
2298                "ep123",
2299                &CreateTagRequest {
2300                    label: Some("my-tag".to_string()),
2301                },
2302            )
2303            .await
2304            .unwrap();
2305    }
2306
2307    #[tokio::test]
2308    async fn delete_tag_success() {
2309        let server = MockServer::start().await;
2310
2311        Mock::given(method("DELETE"))
2312            .and(path("/endpoints/ep123/tags/tag456"))
2313            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({})))
2314            .mount(&server)
2315            .await;
2316
2317        let sdk = make_sdk(format!("{}/", server.uri()));
2318        sdk.admin.delete_tag("ep123", "tag456").await.unwrap();
2319    }
2320
2321    #[tokio::test]
2322    async fn get_usage_success() {
2323        let server = MockServer::start().await;
2324
2325        Mock::given(method("GET"))
2326            .and(path("/usage/rpc"))
2327            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2328                "data": {
2329                    "credits_used": 5000,
2330                    "credits_remaining": 95000,
2331                    "limit": 100000,
2332                    "overages": null,
2333                    "start_time": 1700000000,
2334                    "end_time": 1702592000
2335                },
2336                "error": null
2337            })))
2338            .mount(&server)
2339            .await;
2340
2341        let sdk = make_sdk(format!("{}/", server.uri()));
2342        let resp = sdk
2343            .admin
2344            .get_usage(&GetUsageRequest::default())
2345            .await
2346            .unwrap();
2347        assert_eq!(resp.data.unwrap().credits_used, 5000);
2348    }
2349
2350    #[tokio::test]
2351    async fn get_usage_by_endpoint_success() {
2352        let server = MockServer::start().await;
2353
2354        Mock::given(method("GET"))
2355            .and(path("/usage/rpc/by-endpoint"))
2356            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2357                "data": {
2358                    "endpoints": [{"name": "ep1", "chain": "eth", "network": "mainnet", "status": "active", "credits_used": 100, "label": null, "methods_breakdown": [], "requests": 50}],
2359                    "start_time": 1700000000,
2360                    "end_time": 1702592000
2361                },
2362                "error": null
2363            })))
2364            .mount(&server)
2365            .await;
2366
2367        let sdk = make_sdk(format!("{}/", server.uri()));
2368        let resp = sdk
2369            .admin
2370            .get_usage_by_endpoint(&GetUsageRequest::default())
2371            .await
2372            .unwrap();
2373        assert_eq!(resp.data.unwrap().endpoints.len(), 1);
2374    }
2375
2376    #[tokio::test]
2377    async fn get_usage_by_chain_success() {
2378        let server = MockServer::start().await;
2379
2380        Mock::given(method("GET"))
2381            .and(path("/usage/rpc/by-chain"))
2382            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2383                "data": {
2384                    "chains": [{"name": "ethereum", "credits_used": 1000}],
2385                    "start_time": 1700000000,
2386                    "end_time": 1702592000
2387                },
2388                "error": null
2389            })))
2390            .mount(&server)
2391            .await;
2392
2393        let sdk = make_sdk(format!("{}/", server.uri()));
2394        let resp = sdk
2395            .admin
2396            .get_usage_by_chain(&GetUsageRequest::default())
2397            .await
2398            .unwrap();
2399        assert_eq!(resp.data.unwrap().chains[0].name, "ethereum");
2400    }
2401
2402    #[tokio::test]
2403    async fn get_endpoint_logs_success() {
2404        let server = MockServer::start().await;
2405
2406        Mock::given(method("GET"))
2407            .and(path("/endpoints/ep123/logs"))
2408            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2409                "data": [
2410                    {
2411                        "timestamp": "2025-04-29T12:39:25.543Z",
2412                        "method": "eth_call",
2413                        "network": "mainnet",
2414                        "http_method": "POST",
2415                        "status": 200,
2416                        "error_code": null,
2417                        "url": "/",
2418                        "request_id": "abc-123",
2419                        "details": null
2420                    }
2421                ],
2422                "next_at": null
2423            })))
2424            .mount(&server)
2425            .await;
2426
2427        let sdk = make_sdk(format!("{}/", server.uri()));
2428        let params = GetEndpointLogsRequest {
2429            from: "2025-04-29T00:00:00Z".to_string(),
2430            to: "2025-04-29T23:59:59Z".to_string(),
2431            ..Default::default()
2432        };
2433        let resp = sdk.admin.get_endpoint_logs("ep123", &params).await.unwrap();
2434        assert_eq!(resp.data.len(), 1);
2435        assert_eq!(resp.data[0].method.as_deref(), Some("eth_call"));
2436    }
2437
2438    #[tokio::test]
2439    async fn get_log_details_success() {
2440        let server = MockServer::start().await;
2441
2442        Mock::given(method("GET"))
2443            .and(path("/endpoints/ep123/log_details"))
2444            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2445                "data": {
2446                    "request": "{\"method\":\"eth_call\"}",
2447                    "response": "{\"result\":\"0x1\"}"
2448                }
2449            })))
2450            .mount(&server)
2451            .await;
2452
2453        let sdk = make_sdk(format!("{}/", server.uri()));
2454        let resp = sdk.admin.get_log_details("ep123", "abc-123").await.unwrap();
2455        assert!(resp.data.is_some());
2456    }
2457
2458    #[tokio::test]
2459    async fn get_security_options_success() {
2460        let server = MockServer::start().await;
2461
2462        Mock::given(method("GET"))
2463            .and(path("/endpoints/ep123/security_options"))
2464            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2465                "data": [{"option": "tokens", "status": "enabled", "value": null}],
2466                "error": null
2467            })))
2468            .mount(&server)
2469            .await;
2470
2471        let sdk = make_sdk(format!("{}/", server.uri()));
2472        let resp = sdk.admin.get_security_options("ep123").await.unwrap();
2473        assert_eq!(resp.data.len(), 1);
2474        assert_eq!(resp.data[0].option, "tokens");
2475    }
2476
2477    #[tokio::test]
2478    async fn update_security_options_success() {
2479        let server = MockServer::start().await;
2480
2481        Mock::given(method("PATCH"))
2482            .and(path("/endpoints/ep123/security_options"))
2483            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2484                "data": [{"option": "tokens", "status": "disabled", "value": null}],
2485                "error": null
2486            })))
2487            .mount(&server)
2488            .await;
2489
2490        let sdk = make_sdk(format!("{}/", server.uri()));
2491        let params = UpdateSecurityOptionsRequest {
2492            options: SecurityOptionsUpdate {
2493                tokens: Some("disabled".to_string()),
2494                ..Default::default()
2495            },
2496        };
2497        let resp = sdk
2498            .admin
2499            .update_security_options("ep123", &params)
2500            .await
2501            .unwrap();
2502        assert_eq!(resp.data[0].status, "disabled");
2503    }
2504
2505    #[tokio::test]
2506    async fn create_token_success() {
2507        let server = MockServer::start().await;
2508
2509        Mock::given(method("POST"))
2510            .and(path("/endpoints/ep123/security/tokens"))
2511            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({})))
2512            .mount(&server)
2513            .await;
2514
2515        let sdk = make_sdk(format!("{}/", server.uri()));
2516        sdk.admin.create_token("ep123").await.unwrap();
2517    }
2518
2519    #[tokio::test]
2520    async fn delete_token_success() {
2521        let server = MockServer::start().await;
2522
2523        Mock::given(method("DELETE"))
2524            .and(path("/endpoints/ep123/security/tokens/tok1"))
2525            .respond_with(
2526                ResponseTemplate::new(200)
2527                    .set_body_json(serde_json::json!({"data": true, "error": null})),
2528            )
2529            .mount(&server)
2530            .await;
2531
2532        let sdk = make_sdk(format!("{}/", server.uri()));
2533        let resp = sdk.admin.delete_token("ep123", "tok1").await.unwrap();
2534        assert_eq!(resp.data, Some(true));
2535    }
2536
2537    #[tokio::test]
2538    async fn create_referrer_success() {
2539        let server = MockServer::start().await;
2540
2541        Mock::given(method("POST"))
2542            .and(path("/endpoints/ep123/security/referrers"))
2543            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({})))
2544            .mount(&server)
2545            .await;
2546
2547        let sdk = make_sdk(format!("{}/", server.uri()));
2548        sdk.admin
2549            .create_referrer(
2550                "ep123",
2551                &CreateReferrerRequest {
2552                    referrer: "example.com".to_string(),
2553                },
2554            )
2555            .await
2556            .unwrap();
2557    }
2558
2559    #[tokio::test]
2560    async fn enable_disable_multichain_success() {
2561        let server = MockServer::start().await;
2562
2563        Mock::given(method("POST"))
2564            .and(path("/endpoints/ep123/enable_multichain"))
2565            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({})))
2566            .mount(&server)
2567            .await;
2568
2569        Mock::given(method("POST"))
2570            .and(path("/endpoints/ep123/disable_multichain"))
2571            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({})))
2572            .mount(&server)
2573            .await;
2574
2575        let sdk = make_sdk(format!("{}/", server.uri()));
2576        sdk.admin.enable_multichain("ep123").await.unwrap();
2577        sdk.admin.disable_multichain("ep123").await.unwrap();
2578    }
2579
2580    #[tokio::test]
2581    async fn create_or_update_ip_custom_header_success() {
2582        let server = MockServer::start().await;
2583
2584        Mock::given(method("PATCH"))
2585            .and(path("/endpoints/ep123/ip_custom_header"))
2586            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2587                "data": {"header_name": "CF-Connecting-IP"},
2588                "error": null
2589            })))
2590            .mount(&server)
2591            .await;
2592
2593        let sdk = make_sdk(format!("{}/", server.uri()));
2594        let params = CreateOrUpdateIpCustomHeaderRequest {
2595            header_name: "CF-Connecting-IP".to_string(),
2596        };
2597        let resp = sdk
2598            .admin
2599            .create_or_update_ip_custom_header("ep123", &params)
2600            .await
2601            .unwrap();
2602        assert_eq!(resp.data.unwrap().header_name, "CF-Connecting-IP");
2603    }
2604
2605    #[tokio::test]
2606    async fn get_method_rate_limits_success() {
2607        let server = MockServer::start().await;
2608
2609        Mock::given(method("GET"))
2610            .and(path("/endpoints/ep123/method-rate-limits"))
2611            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2612                "data": {
2613                    "rate_limiters": [
2614                        {"id": "rl1", "interval": "second", "methods": ["eth_call"], "rate": 10, "status": "enabled", "created": "2024-01-01T00:00:00Z"}
2615                    ]
2616                },
2617                "error": null
2618            })))
2619            .mount(&server)
2620            .await;
2621
2622        let sdk = make_sdk(format!("{}/", server.uri()));
2623        let resp = sdk.admin.get_method_rate_limits("ep123").await.unwrap();
2624        assert_eq!(resp.data.unwrap().rate_limiters.len(), 1);
2625    }
2626
2627    #[tokio::test]
2628    async fn create_method_rate_limit_success() {
2629        let server = MockServer::start().await;
2630
2631        Mock::given(method("POST"))
2632            .and(path("/endpoints/ep123/method-rate-limits"))
2633            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2634                "data": {"id": "rl1", "interval": "second", "methods": ["eth_call"], "rate": 10, "status": "enabled", "created": "2024-01-01T00:00:00Z"},
2635                "error": null
2636            })))
2637            .mount(&server)
2638            .await;
2639
2640        let sdk = make_sdk(format!("{}/", server.uri()));
2641        let params = CreateMethodRateLimitRequest {
2642            interval: "second".to_string(),
2643            methods: vec!["eth_call".to_string()],
2644            rate: 10,
2645        };
2646        let resp = sdk
2647            .admin
2648            .create_method_rate_limit("ep123", &params)
2649            .await
2650            .unwrap();
2651        assert_eq!(resp.data.unwrap().id, "rl1");
2652    }
2653
2654    #[tokio::test]
2655    async fn update_method_rate_limit_success() {
2656        let server = MockServer::start().await;
2657
2658        Mock::given(method("PATCH"))
2659            .and(path("/endpoints/ep123/method-rate-limits/rl1"))
2660            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2661                "data": {"id": "rl1", "interval": "day", "methods": ["eth_call"], "rate": 30, "status": "enabled", "created": "2024-01-01T00:00:00Z"},
2662                "error": null
2663            })))
2664            .mount(&server)
2665            .await;
2666
2667        let sdk = make_sdk(format!("{}/", server.uri()));
2668        let params = UpdateMethodRateLimitRequest {
2669            rate: Some(30),
2670            ..Default::default()
2671        };
2672        let resp = sdk
2673            .admin
2674            .update_method_rate_limit("ep123", "rl1", &params)
2675            .await
2676            .unwrap();
2677        assert_eq!(resp.data.unwrap().rate, 30);
2678    }
2679
2680    #[tokio::test]
2681    async fn delete_method_rate_limit_success() {
2682        let server = MockServer::start().await;
2683
2684        Mock::given(method("DELETE"))
2685            .and(path("/endpoints/ep123/method-rate-limits/rl1"))
2686            .respond_with(
2687                ResponseTemplate::new(200)
2688                    .set_body_json(serde_json::json!({"data": "deleted", "error": null})),
2689            )
2690            .mount(&server)
2691            .await;
2692
2693        let sdk = make_sdk(format!("{}/", server.uri()));
2694        sdk.admin
2695            .delete_method_rate_limit("ep123", "rl1")
2696            .await
2697            .unwrap();
2698    }
2699
2700    #[tokio::test]
2701    async fn update_rate_limits_success() {
2702        let server = MockServer::start().await;
2703
2704        Mock::given(method("PATCH"))
2705            .and(path("/endpoints/ep123/rate-limits"))
2706            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({})))
2707            .mount(&server)
2708            .await;
2709
2710        let sdk = make_sdk(format!("{}/", server.uri()));
2711        let params = UpdateRateLimitsRequest {
2712            rate_limits: RateLimitSettings {
2713                rps: Some(100),
2714                rpm: None,
2715                rpd: None,
2716            },
2717        };
2718        sdk.admin
2719            .update_rate_limits("ep123", &params)
2720            .await
2721            .unwrap();
2722    }
2723
2724    #[tokio::test]
2725    async fn get_rate_limits_success() {
2726        let server = MockServer::start().await;
2727
2728        Mock::given(method("GET"))
2729            .and(path("/endpoints/ep123/rate-limits"))
2730            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2731                "data": {
2732                    "rate_limits": [
2733                        {"bucket": "rps", "rate_limit": 100, "source": "plan_default"},
2734                        {"bucket": "rpm", "rate_limit": 6000, "source": "user_override", "id": "ovr-1"}
2735                    ]
2736                },
2737                "error": null
2738            })))
2739            .mount(&server)
2740            .await;
2741
2742        let sdk = make_sdk(format!("{}/", server.uri()));
2743        let resp = sdk.admin.get_rate_limits("ep123").await.unwrap();
2744        let rows = resp.data.unwrap().rate_limits;
2745        assert_eq!(rows.len(), 2);
2746        assert_eq!(rows[0].source, "plan_default");
2747        assert!(rows[0].id.is_none());
2748        assert_eq!(rows[1].source, "user_override");
2749        assert_eq!(rows[1].rate_limit, 6000);
2750        assert_eq!(rows[1].id.as_deref(), Some("ovr-1"));
2751    }
2752
2753    #[tokio::test]
2754    async fn get_rate_limits_api_error() {
2755        let server = MockServer::start().await;
2756
2757        Mock::given(method("GET"))
2758            .and(path("/endpoints/missing/rate-limits"))
2759            .respond_with(ResponseTemplate::new(404).set_body_string("not found"))
2760            .mount(&server)
2761            .await;
2762
2763        let sdk = make_sdk(format!("{}/", server.uri()));
2764        let err = sdk.admin.get_rate_limits("missing").await.unwrap_err();
2765        match err {
2766            SdkError::Api { status, .. } => assert_eq!(status.as_u16(), 404),
2767            other => panic!("expected SdkError::Api, got {:?}", other),
2768        }
2769    }
2770
2771    #[tokio::test]
2772    async fn delete_rate_limit_override_success() {
2773        let server = MockServer::start().await;
2774
2775        Mock::given(method("DELETE"))
2776            .and(path("/endpoints/ep123/rate-limits/ovr-1"))
2777            .respond_with(ResponseTemplate::new(200).set_body_string(""))
2778            .mount(&server)
2779            .await;
2780
2781        let sdk = make_sdk(format!("{}/", server.uri()));
2782        sdk.admin
2783            .delete_rate_limit_override("ep123", "ovr-1")
2784            .await
2785            .unwrap();
2786    }
2787
2788    #[tokio::test]
2789    async fn delete_rate_limit_override_not_found() {
2790        let server = MockServer::start().await;
2791
2792        Mock::given(method("DELETE"))
2793            .and(path("/endpoints/ep123/rate-limits/bogus"))
2794            .respond_with(ResponseTemplate::new(404).set_body_string("override not found"))
2795            .mount(&server)
2796            .await;
2797
2798        let sdk = make_sdk(format!("{}/", server.uri()));
2799        let err = sdk
2800            .admin
2801            .delete_rate_limit_override("ep123", "bogus")
2802            .await
2803            .unwrap_err();
2804        match err {
2805            SdkError::Api { status, .. } => assert_eq!(status.as_u16(), 404),
2806            other => panic!("expected SdkError::Api, got {:?}", other),
2807        }
2808    }
2809
2810    #[tokio::test]
2811    async fn get_endpoint_urls_success() {
2812        let server = MockServer::start().await;
2813
2814        Mock::given(method("GET"))
2815            .and(path("/endpoints/ep123/urls"))
2816            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2817                "data": {
2818                    "http_url": "https://example.quiknode.pro/abc/",
2819                    "wss_url": "wss://example.quiknode.pro/abc/",
2820                    "multichain_urls": null
2821                },
2822                "error": null
2823            })))
2824            .mount(&server)
2825            .await;
2826
2827        let sdk = make_sdk(format!("{}/", server.uri()));
2828        let resp = sdk.admin.get_endpoint_urls("ep123").await.unwrap();
2829        let data = resp.data.unwrap();
2830        assert_eq!(data.http_url, "https://example.quiknode.pro/abc/");
2831        assert!(data.multichain_urls.is_none());
2832    }
2833
2834    #[tokio::test]
2835    async fn get_endpoint_urls_multichain() {
2836        let server = MockServer::start().await;
2837
2838        Mock::given(method("GET"))
2839            .and(path("/endpoints/ep123/urls"))
2840            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2841                "data": {
2842                    "http_url": "https://example.quiknode.pro/abc/",
2843                    "wss_url": null,
2844                    "multichain_urls": {
2845                        "ethereum-mainnet": {
2846                            "http_url": "https://example.quiknode.pro/abc/eth/",
2847                            "wss_url": "wss://example.quiknode.pro/abc/eth/"
2848                        }
2849                    }
2850                },
2851                "error": null
2852            })))
2853            .mount(&server)
2854            .await;
2855
2856        let sdk = make_sdk(format!("{}/", server.uri()));
2857        let resp = sdk.admin.get_endpoint_urls("ep123").await.unwrap();
2858        let data = resp.data.unwrap();
2859        let mc = data.multichain_urls.unwrap();
2860        assert_eq!(mc.len(), 1);
2861        assert_eq!(
2862            mc.get("ethereum-mainnet").unwrap().http_url,
2863            "https://example.quiknode.pro/abc/eth/"
2864        );
2865    }
2866
2867    #[tokio::test]
2868    async fn get_endpoint_metrics_success() {
2869        let server = MockServer::start().await;
2870
2871        Mock::given(method("GET"))
2872            .and(path("/endpoints/ep123/metrics"))
2873            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2874                "data": [{"data": [[1700000000, 42]], "tag": ["network", "mainnet"]}],
2875                "error": null
2876            })))
2877            .mount(&server)
2878            .await;
2879
2880        let sdk = make_sdk(format!("{}/", server.uri()));
2881        let params = GetEndpointMetricsRequest {
2882            period: "day".to_string(),
2883            metric: "credits_over_time".to_string(),
2884        };
2885        let resp = sdk
2886            .admin
2887            .get_endpoint_metrics("ep123", &params)
2888            .await
2889            .unwrap();
2890        assert_eq!(resp.data.len(), 1);
2891        assert_eq!(
2892            resp.data[0].tag,
2893            vec!["network".to_string(), "mainnet".to_string()]
2894        );
2895    }
2896
2897    #[tokio::test]
2898    async fn get_account_metrics_success() {
2899        let server = MockServer::start().await;
2900
2901        Mock::given(method("GET"))
2902            .and(path("/metrics"))
2903            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2904                "data": [{"data": [[1700000000, 100]], "tag": "total"}],
2905                "error": null
2906            })))
2907            .mount(&server)
2908            .await;
2909
2910        let sdk = make_sdk(format!("{}/", server.uri()));
2911        let params = GetAccountMetricsRequest {
2912            period: "week".to_string(),
2913            metric: "credits_over_time".to_string(),
2914            percentile: None,
2915        };
2916        let resp = sdk.admin.get_account_metrics(&params).await.unwrap();
2917        assert_eq!(resp.data.len(), 1);
2918        assert_eq!(resp.data[0].tag, vec!["total".to_string()]);
2919    }
2920
2921    // Regression: the metrics endpoints return `tag` as either a plain string
2922    // (single-axis series) or a `[key, value]` tuple (multi-axis series).
2923    // Exercise both shapes so any future serde change that breaks either
2924    // branch fails loudly.
2925    #[tokio::test]
2926    async fn get_account_metrics_decodes_tuple_tag() {
2927        let server = MockServer::start().await;
2928
2929        Mock::given(method("GET"))
2930            .and(path("/metrics"))
2931            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2932                "data": [
2933                    {"tag": ["network", "arbitrum-mainnet"], "data": [[1779109200, 40]]},
2934                    {"tag": ["network", "mainnet"], "data": [[1779116400, 40]]},
2935                    {"tag": "p95", "data": [[1779116400, 12]]}
2936                ],
2937                "error": null
2938            })))
2939            .mount(&server)
2940            .await;
2941
2942        let sdk = make_sdk(format!("{}/", server.uri()));
2943        let params = GetAccountMetricsRequest {
2944            period: "day".to_string(),
2945            metric: "credits_over_time".to_string(),
2946            percentile: None,
2947        };
2948        let resp = sdk.admin.get_account_metrics(&params).await.unwrap();
2949        assert_eq!(resp.data.len(), 3);
2950        assert_eq!(
2951            resp.data[0].tag,
2952            vec!["network".to_string(), "arbitrum-mainnet".to_string()]
2953        );
2954        assert_eq!(
2955            resp.data[1].tag,
2956            vec!["network".to_string(), "mainnet".to_string()]
2957        );
2958        assert_eq!(resp.data[2].tag, vec!["p95".to_string()]);
2959    }
2960
2961    #[tokio::test]
2962    async fn list_chains_success() {
2963        let server = MockServer::start().await;
2964
2965        Mock::given(method("GET"))
2966            .and(path("/chains"))
2967            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2968                "data": [
2969                    {
2970                        "slug": "eth",
2971                        "networks": [{"slug": "mainnet", "name": "Ethereum Mainnet", "chain_id": 1}],
2972                        "is_select_chain": true
2973                    }
2974                ],
2975                "error": null
2976            })))
2977            .mount(&server)
2978            .await;
2979
2980        let sdk = make_sdk(format!("{}/", server.uri()));
2981        let resp = sdk.admin.list_chains().await.unwrap();
2982        assert_eq!(resp.data.len(), 1);
2983        assert_eq!(resp.data[0].slug, "eth");
2984        assert_eq!(resp.data[0].networks[0].chain_id, Some(1));
2985    }
2986
2987    #[tokio::test]
2988    async fn list_invoices_success() {
2989        let server = MockServer::start().await;
2990
2991        Mock::given(method("GET"))
2992            .and(path("/billing/invoices"))
2993            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2994                "data": {
2995                    "invoices": [
2996                        {
2997                            "id": "inv123",
2998                            "status": "paid",
2999                            "billing_reason": "subscription",
3000                            "lines": [{"description": "Pro plan", "amount": 4900}],
3001                            "amount_due": 4900,
3002                            "amount_paid": 4900,
3003                            "period_start": 1700000000,
3004                            "period_end": 1702592000,
3005                            "created": 1700000000,
3006                            "subtotal": 4900
3007                        }
3008                    ]
3009                },
3010                "error": null
3011            })))
3012            .mount(&server)
3013            .await;
3014
3015        let sdk = make_sdk(format!("{}/", server.uri()));
3016        let resp = sdk.admin.list_invoices().await.unwrap();
3017        let data = resp.data.unwrap();
3018        assert_eq!(data.invoices.len(), 1);
3019        assert_eq!(data.invoices[0].id, "inv123");
3020    }
3021
3022    #[tokio::test]
3023    async fn list_invoices_api_error() {
3024        let server = MockServer::start().await;
3025
3026        Mock::given(method("GET"))
3027            .and(path("/billing/invoices"))
3028            .respond_with(ResponseTemplate::new(401).set_body_string("Unauthorized"))
3029            .mount(&server)
3030            .await;
3031
3032        let sdk = make_sdk(format!("{}/", server.uri()));
3033        let err = sdk.admin.list_invoices().await.unwrap_err();
3034        match err {
3035            SdkError::Api { status, .. } => assert_eq!(status.as_u16(), 401),
3036            other => panic!("expected SdkError::Api, got {:?}", other),
3037        }
3038    }
3039
3040    #[tokio::test]
3041    async fn list_payments_success() {
3042        let server = MockServer::start().await;
3043
3044        Mock::given(method("GET"))
3045            .and(path("/billing/payments"))
3046            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
3047                "data": {
3048                    "payments": [
3049                        {
3050                            "amount": "49.00",
3051                            "card_last_4": "4242",
3052                            "created_at": "2024-01-01T00:00:00Z",
3053                            "currency": "usd",
3054                            "status": "succeeded",
3055                            "marketplace_amount": "9.0"
3056                        }
3057                    ]
3058                },
3059                "error": null
3060            })))
3061            .mount(&server)
3062            .await;
3063
3064        let sdk = make_sdk(format!("{}/", server.uri()));
3065        let resp = sdk.admin.list_payments().await.unwrap();
3066        let data = resp.data.unwrap();
3067        assert_eq!(data.payments.len(), 1);
3068        assert_eq!(data.payments[0].currency, "usd");
3069    }
3070
3071    #[tokio::test]
3072    async fn list_teams_success() {
3073        let server = MockServer::start().await;
3074
3075        Mock::given(method("GET"))
3076            .and(path("/teams"))
3077            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
3078                "data": [{"id": 1, "name": "Engineering", "members_count": 5, "users": []}],
3079                "error": null
3080            })))
3081            .mount(&server)
3082            .await;
3083
3084        let sdk = make_sdk(format!("{}/", server.uri()));
3085        let resp = sdk.admin.list_teams().await.unwrap();
3086        assert_eq!(resp.data.len(), 1);
3087        assert_eq!(resp.data[0].name, "Engineering");
3088    }
3089
3090    #[tokio::test]
3091    async fn list_teams_api_error() {
3092        let server = MockServer::start().await;
3093
3094        Mock::given(method("GET"))
3095            .and(path("/teams"))
3096            .respond_with(ResponseTemplate::new(401).set_body_string("Unauthorized"))
3097            .mount(&server)
3098            .await;
3099
3100        let sdk = make_sdk(format!("{}/", server.uri()));
3101        let err = sdk.admin.list_teams().await.unwrap_err();
3102        match err {
3103            SdkError::Api { status, .. } => assert_eq!(status.as_u16(), 401),
3104            other => panic!("expected SdkError::Api, got {:?}", other),
3105        }
3106    }
3107
3108    #[tokio::test]
3109    async fn create_team_success() {
3110        let server = MockServer::start().await;
3111
3112        Mock::given(method("POST"))
3113            .and(path("/teams"))
3114            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
3115                "data": {"id": 42, "name": "New Team", "default_role": null, "members_count": 0},
3116                "error": null
3117            })))
3118            .mount(&server)
3119            .await;
3120
3121        let sdk = make_sdk(format!("{}/", server.uri()));
3122        let params = CreateTeamRequest {
3123            name: "New Team".to_string(),
3124        };
3125        let resp = sdk.admin.create_team(&params).await.unwrap();
3126        assert_eq!(resp.data.unwrap().id, 42);
3127    }
3128
3129    #[tokio::test]
3130    async fn get_team_success() {
3131        let server = MockServer::start().await;
3132
3133        Mock::given(method("GET"))
3134            .and(path("/teams/1"))
3135            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
3136                "data": {
3137                    "id": 1,
3138                    "name": "Engineering",
3139                    "default_role": "member",
3140                    "members_count": 3,
3141                    "users": [],
3142                    "pending_invites": []
3143                },
3144                "error": null
3145            })))
3146            .mount(&server)
3147            .await;
3148
3149        let sdk = make_sdk(format!("{}/", server.uri()));
3150        let resp = sdk.admin.get_team(1).await.unwrap();
3151        assert_eq!(resp.data.unwrap().name, "Engineering");
3152    }
3153
3154    #[tokio::test]
3155    async fn delete_team_success() {
3156        let server = MockServer::start().await;
3157
3158        Mock::given(method("DELETE"))
3159            .and(path("/teams/1"))
3160            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
3161                "data": {"message": "Team deleted"},
3162                "error": null
3163            })))
3164            .mount(&server)
3165            .await;
3166
3167        let sdk = make_sdk(format!("{}/", server.uri()));
3168        let resp = sdk.admin.delete_team(1).await.unwrap();
3169        assert!(resp.data.is_some());
3170    }
3171
3172    #[tokio::test]
3173    async fn list_team_endpoints_success() {
3174        let server = MockServer::start().await;
3175
3176        Mock::given(method("GET"))
3177            .and(path("/teams/1/endpoints"))
3178            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
3179                "data": [{"id": 10, "subdomain": "abc123", "chain": "ethereum", "network": "mainnet"}],
3180                "error": null
3181            })))
3182            .mount(&server)
3183            .await;
3184
3185        let sdk = make_sdk(format!("{}/", server.uri()));
3186        let resp = sdk.admin.list_team_endpoints(1).await.unwrap();
3187        assert_eq!(resp.data.len(), 1);
3188        assert_eq!(resp.data[0].subdomain, "abc123");
3189    }
3190
3191    #[tokio::test]
3192    async fn update_team_endpoints_success() {
3193        let server = MockServer::start().await;
3194
3195        Mock::given(method("PATCH"))
3196            .and(path("/teams/1/endpoints"))
3197            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
3198                "data": {"success": true},
3199                "error": null
3200            })))
3201            .mount(&server)
3202            .await;
3203
3204        let sdk = make_sdk(format!("{}/", server.uri()));
3205        let params = UpdateTeamEndpointsRequest {
3206            endpoint_ids: vec!["ep1".to_string()],
3207        };
3208        let resp = sdk.admin.update_team_endpoints(1, &params).await.unwrap();
3209        assert!(resp.data.unwrap().success.unwrap());
3210    }
3211
3212    // Wire-inspection regression: confirm an empty endpoint_ids array reaches
3213    // the wire as `[]` (not omitted), so any future `skip_serializing_if`
3214    // change that drops the empty case fails loudly.
3215    #[tokio::test]
3216    async fn update_team_endpoints_empty_array_wire_body() {
3217        use wiremock::matchers::body_json;
3218        let server = MockServer::start().await;
3219        Mock::given(method("PATCH"))
3220            .and(path("/teams/1/endpoints"))
3221            .and(body_json(serde_json::json!({ "endpoint_ids": [] })))
3222            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
3223                "data": {"success": true},
3224                "error": null
3225            })))
3226            .mount(&server)
3227            .await;
3228        let sdk = make_sdk(format!("{}/", server.uri()));
3229        let params = UpdateTeamEndpointsRequest {
3230            endpoint_ids: vec![],
3231        };
3232        sdk.admin.update_team_endpoints(1, &params).await.unwrap();
3233    }
3234
3235    #[tokio::test]
3236    async fn invite_team_member_success() {
3237        let server = MockServer::start().await;
3238
3239        Mock::given(method("POST"))
3240            .and(path("/teams/1/members"))
3241            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
3242                "data": {
3243                    "id": 99,
3244                    "email": "user@example.com",
3245                    "full_name": null,
3246                    "role": "member",
3247                    "status": "pending",
3248                    "created_at": null,
3249                    "photo_url": null,
3250                    "account_primary_user": null
3251                },
3252                "error": null
3253            })))
3254            .mount(&server)
3255            .await;
3256
3257        let sdk = make_sdk(format!("{}/", server.uri()));
3258        let params = InviteTeamMemberRequest {
3259            email: "user@example.com".to_string(),
3260            full_name: None,
3261            role: None,
3262        };
3263        let resp = sdk.admin.invite_team_member(1, &params).await.unwrap();
3264        assert_eq!(resp.data.unwrap().email, "user@example.com");
3265    }
3266
3267    #[tokio::test]
3268    async fn remove_team_member_success() {
3269        let server = MockServer::start().await;
3270
3271        Mock::given(method("DELETE"))
3272            .and(path("/teams/1/members/99"))
3273            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
3274                "data": {"message": "Member removed"},
3275                "error": null
3276            })))
3277            .mount(&server)
3278            .await;
3279
3280        let sdk = make_sdk(format!("{}/", server.uri()));
3281        let params = RemoveTeamMemberRequest { destroy_user: None };
3282        let resp = sdk.admin.remove_team_member(1, 99, &params).await.unwrap();
3283        assert!(resp.data.is_some());
3284    }
3285
3286    #[tokio::test]
3287    async fn resend_team_invite_success() {
3288        let server = MockServer::start().await;
3289
3290        Mock::given(method("POST"))
3291            .and(path("/teams/1/members/99/resend_invite"))
3292            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
3293                "data": {"message": "Invite resent"},
3294                "error": null
3295            })))
3296            .mount(&server)
3297            .await;
3298
3299        let sdk = make_sdk(format!("{}/", server.uri()));
3300        let resp = sdk.admin.resend_team_invite(1, 99).await.unwrap();
3301        assert!(resp.data.is_some());
3302    }
3303
3304    #[tokio::test]
3305    async fn bulk_update_endpoint_status_success() {
3306        let server = MockServer::start().await;
3307        Mock::given(method("POST"))
3308            .and(path("/endpoints/bulk/status"))
3309            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
3310                "data": {
3311                    "total": 2,
3312                    "updated_count": 2,
3313                    "failed_count": 0,
3314                    "results": [
3315                        { "id": "a", "success": true },
3316                        { "id": "b", "success": true }
3317                    ]
3318                }
3319            })))
3320            .mount(&server)
3321            .await;
3322
3323        let sdk = make_sdk(format!("{}/", server.uri()));
3324        let params = BulkUpdateEndpointStatusRequest {
3325            ids: vec!["a".to_string(), "b".to_string()],
3326            status: "paused".to_string(),
3327        };
3328        let resp = sdk
3329            .admin
3330            .bulk_update_endpoint_status(&params)
3331            .await
3332            .unwrap();
3333        let data = resp.data.expect("data present");
3334        assert_eq!(data.total, 2);
3335        assert_eq!(data.updated_count, 2);
3336        assert_eq!(data.results.len(), 2);
3337    }
3338
3339    #[tokio::test]
3340    async fn bulk_update_endpoint_status_api_error() {
3341        let server = MockServer::start().await;
3342        Mock::given(method("POST"))
3343            .and(path("/endpoints/bulk/status"))
3344            .respond_with(ResponseTemplate::new(400).set_body_string("bad request"))
3345            .mount(&server)
3346            .await;
3347
3348        let sdk = make_sdk(format!("{}/", server.uri()));
3349        let params = BulkUpdateEndpointStatusRequest {
3350            ids: vec!["a".to_string()],
3351            status: "paused".to_string(),
3352        };
3353        let err = sdk
3354            .admin
3355            .bulk_update_endpoint_status(&params)
3356            .await
3357            .unwrap_err();
3358        match err {
3359            SdkError::Api { status, .. } => assert_eq!(status.as_u16(), 400),
3360            other => panic!("expected Api, got {:?}", other),
3361        }
3362    }
3363
3364    #[tokio::test]
3365    async fn bulk_add_tag_success() {
3366        let server = MockServer::start().await;
3367        Mock::given(method("POST"))
3368            .and(path("/endpoints/bulk/tags"))
3369            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
3370                "data": {
3371                    "total": 1,
3372                    "updated_count": 1,
3373                    "failed_count": 0,
3374                    "results": [{ "id": "a", "success": true }],
3375                    "tag": { "tag_id": 7, "label": "prod" }
3376                }
3377            })))
3378            .mount(&server)
3379            .await;
3380
3381        let sdk = make_sdk(format!("{}/", server.uri()));
3382        let params = BulkAddTagRequest {
3383            ids: vec!["a".to_string()],
3384            label: "prod".to_string(),
3385        };
3386        let resp = sdk.admin.bulk_add_tag(&params).await.unwrap();
3387        let data = resp.data.expect("data present");
3388        assert_eq!(data.tag.tag_id, 7);
3389        assert_eq!(data.tag.label, "prod");
3390    }
3391
3392    #[tokio::test]
3393    async fn bulk_remove_tag_success() {
3394        let server = MockServer::start().await;
3395        Mock::given(method("DELETE"))
3396            .and(path("/endpoints/bulk/tags"))
3397            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
3398                "data": {
3399                    "total": 2,
3400                    "updated_count": 2,
3401                    "failed_count": 0,
3402                    "results": [
3403                        { "id": "a", "success": true },
3404                        { "id": "b", "success": true }
3405                    ]
3406                }
3407            })))
3408            .mount(&server)
3409            .await;
3410
3411        let sdk = make_sdk(format!("{}/", server.uri()));
3412        let params = BulkRemoveTagRequest {
3413            ids: vec!["a".to_string(), "b".to_string()],
3414            tag_id: 42,
3415        };
3416        let resp = sdk.admin.bulk_remove_tag(&params).await.unwrap();
3417        assert_eq!(resp.data.expect("data").updated_count, 2);
3418    }
3419
3420    #[test]
3421    fn endpoint_token_debug_is_redacted() {
3422        let t = EndpointToken {
3423            id: "tok_1".to_string(),
3424            token: "super-secret".to_string(),
3425        };
3426        let dbg = format!("{t:?}");
3427        assert!(dbg.contains("tok_1"));
3428        assert!(!dbg.contains("super-secret"));
3429        assert!(dbg.contains("[redacted]"));
3430    }
3431
3432    #[test]
3433    fn endpoint_jwt_debug_redacts_public_key() {
3434        let j = EndpointJwt {
3435            id: "jwt_1".to_string(),
3436            public_key: "-----BEGIN PUBLIC KEY-----\nAAAA\n-----END PUBLIC KEY-----".to_string(),
3437            kid: "kid1".to_string(),
3438            name: "myjwt".to_string(),
3439        };
3440        let dbg = format!("{j:?}");
3441        assert!(dbg.contains("jwt_1"));
3442        assert!(dbg.contains("kid1"));
3443        assert!(!dbg.contains("BEGIN PUBLIC KEY"));
3444        assert!(dbg.contains("[redacted]"));
3445    }
3446
3447    #[tokio::test]
3448    async fn bulk_methods_reject_empty_ids() {
3449        // No MockServer: the guards must fail before any HTTP request fires.
3450        let sdk = make_sdk("http://127.0.0.1:1/".to_string());
3451
3452        let err = sdk
3453            .admin
3454            .bulk_update_endpoint_status(&BulkUpdateEndpointStatusRequest {
3455                ids: vec![],
3456                status: "paused".to_string(),
3457            })
3458            .await
3459            .unwrap_err();
3460        assert!(matches!(err, SdkError::Config(_)));
3461
3462        let err = sdk
3463            .admin
3464            .bulk_add_tag(&BulkAddTagRequest {
3465                ids: vec![],
3466                label: "x".to_string(),
3467            })
3468            .await
3469            .unwrap_err();
3470        assert!(matches!(err, SdkError::Config(_)));
3471
3472        let err = sdk
3473            .admin
3474            .bulk_remove_tag(&BulkRemoveTagRequest {
3475                ids: vec![],
3476                tag_id: 1,
3477            })
3478            .await
3479            .unwrap_err();
3480        assert!(matches!(err, SdkError::Config(_)));
3481    }
3482
3483    #[tokio::test]
3484    async fn list_tags_success() {
3485        let server = MockServer::start().await;
3486        Mock::given(method("GET"))
3487            .and(path("/endpoints/tags"))
3488            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
3489                "data": {
3490                    "tags": [
3491                        { "id": 1, "label": "prod", "usage_count": 3 },
3492                        { "id": 2, "label": "staging", "usage_count": 0 }
3493                    ]
3494                },
3495                "error": null
3496            })))
3497            .mount(&server)
3498            .await;
3499
3500        let sdk = make_sdk(format!("{}/", server.uri()));
3501        let resp = sdk.admin.list_tags().await.unwrap();
3502        let data = resp.data.expect("data present");
3503        assert_eq!(data.tags.len(), 2);
3504        assert_eq!(data.tags[0].label, "prod");
3505        assert_eq!(data.tags[1].usage_count, 0);
3506    }
3507
3508    #[tokio::test]
3509    async fn list_tags_api_error() {
3510        let server = MockServer::start().await;
3511        Mock::given(method("GET"))
3512            .and(path("/endpoints/tags"))
3513            .respond_with(ResponseTemplate::new(500).set_body_string("oops"))
3514            .mount(&server)
3515            .await;
3516
3517        let sdk = make_sdk(format!("{}/", server.uri()));
3518        let err = sdk.admin.list_tags().await.unwrap_err();
3519        match err {
3520            SdkError::Api { status, .. } => assert_eq!(status.as_u16(), 500),
3521            other => panic!("expected Api, got {:?}", other),
3522        }
3523    }
3524
3525    #[tokio::test]
3526    async fn rename_tag_success() {
3527        let server = MockServer::start().await;
3528        Mock::given(method("PATCH"))
3529            .and(path("/endpoints/tags/7"))
3530            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
3531                "data": { "id": 7, "label": "prod-v2", "usage_count": 3 },
3532                "error": null
3533            })))
3534            .mount(&server)
3535            .await;
3536
3537        let sdk = make_sdk(format!("{}/", server.uri()));
3538        let params = RenameTagRequest {
3539            label: "prod-v2".to_string(),
3540        };
3541        let resp = sdk.admin.rename_tag(7, &params).await.unwrap();
3542        let tag = resp.data.expect("tag present");
3543        assert_eq!(tag.id, 7);
3544        assert_eq!(tag.label, "prod-v2");
3545    }
3546
3547    #[tokio::test]
3548    async fn delete_account_tag_success() {
3549        let server = MockServer::start().await;
3550        Mock::given(method("DELETE"))
3551            .and(path("/endpoints/tags/7"))
3552            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
3553                "data": { "success": true },
3554                "error": null
3555            })))
3556            .mount(&server)
3557            .await;
3558
3559        let sdk = make_sdk(format!("{}/", server.uri()));
3560        let resp = sdk.admin.delete_account_tag(7).await.unwrap();
3561        assert!(resp.data.expect("data").success);
3562    }
3563
3564    #[tokio::test]
3565    async fn delete_account_tag_still_in_use() {
3566        let server = MockServer::start().await;
3567        Mock::given(method("DELETE"))
3568            .and(path("/endpoints/tags/7"))
3569            .respond_with(ResponseTemplate::new(400).set_body_string("tag still in use"))
3570            .mount(&server)
3571            .await;
3572
3573        let sdk = make_sdk(format!("{}/", server.uri()));
3574        let err = sdk.admin.delete_account_tag(7).await.unwrap_err();
3575        match err {
3576            SdkError::Api { status, .. } => assert_eq!(status.as_u16(), 400),
3577            other => panic!("expected Api, got {:?}", other),
3578        }
3579    }
3580
3581    #[tokio::test]
3582    async fn get_usage_by_tag_success() {
3583        let server = MockServer::start().await;
3584        Mock::given(method("GET"))
3585            .and(path("/usage/rpc/by-tag"))
3586            .and(query_param("start_time", "1700000000"))
3587            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
3588                "data": {
3589                    "tags": [
3590                        { "tag_id": 1, "label": "prod", "credits_used": 1234, "requests": 10 },
3591                        { "tag_id": null, "label": "untagged", "credits_used": 50, "requests": 2 }
3592                    ],
3593                    "start_time": 1700000000,
3594                    "end_time": 1700003600
3595                },
3596                "error": null
3597            })))
3598            .mount(&server)
3599            .await;
3600
3601        let sdk = make_sdk(format!("{}/", server.uri()));
3602        let params = GetUsageRequest {
3603            start_time: Some(1_700_000_000),
3604            ..Default::default()
3605        };
3606        let resp = sdk.admin.get_usage_by_tag(&params).await.unwrap();
3607        let data = resp.data.expect("data present");
3608        assert_eq!(data.tags.len(), 2);
3609        assert_eq!(data.tags[0].tag_id, Some(1));
3610        assert_eq!(data.tags[1].tag_id, None);
3611        assert_eq!(data.tags[1].label, "untagged");
3612    }
3613
3614    #[tokio::test]
3615    async fn get_endpoint_security_success() {
3616        let server = MockServer::start().await;
3617        Mock::given(method("GET"))
3618            .and(path("/endpoints/abc123/security"))
3619            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
3620                "data": {
3621                    "options": { "tokens": true, "ips": false },
3622                    "tokens": [{ "id": "tok_1", "token": "secret" }],
3623                    "jwts": [],
3624                    "referrers": [],
3625                    "domain_masks": [],
3626                    "ips": [],
3627                    "request_filters": []
3628                },
3629                "error": null
3630            })))
3631            .mount(&server)
3632            .await;
3633
3634        let sdk = make_sdk(format!("{}/", server.uri()));
3635        let resp = sdk.admin.get_endpoint_security("abc123").await.unwrap();
3636        let data = resp.data.expect("data present");
3637        let tokens = data.tokens.expect("tokens present");
3638        assert_eq!(tokens.len(), 1);
3639        assert_eq!(tokens[0].id, "tok_1");
3640    }
3641
3642    #[tokio::test]
3643    async fn get_endpoint_security_not_found() {
3644        let server = MockServer::start().await;
3645        Mock::given(method("GET"))
3646            .and(path("/endpoints/missing/security"))
3647            .respond_with(ResponseTemplate::new(404).set_body_string("not found"))
3648            .mount(&server)
3649            .await;
3650
3651        let sdk = make_sdk(format!("{}/", server.uri()));
3652        let err = sdk
3653            .admin
3654            .get_endpoint_security("missing")
3655            .await
3656            .unwrap_err();
3657        match err {
3658            SdkError::Api { status, .. } => assert_eq!(status.as_u16(), 404),
3659            other => panic!("expected Api, got {:?}", other),
3660        }
3661    }
3662
3663    #[test]
3664    fn negative_timeout_secs_returns_error() {
3665        use crate::{HttpConfig, SdkConfig, SdkFullConfig};
3666        let result = SdkConfig::new(&SdkFullConfig {
3667            api_key: "test-key".to_string(),
3668            http: Some(HttpConfig {
3669                timeout_secs: Some(-1),
3670                pool_max_idle_per_host: None,
3671                headers: None,
3672            }),
3673            admin: None,
3674            streams: None,
3675            webhooks: None,
3676            kvstore: None,
3677            sql: None,
3678        });
3679        assert!(matches!(result, Err(crate::errors::SdkError::Config(_))));
3680    }
3681}