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        })
1907        .unwrap()
1908    }
1909
1910    #[tokio::test]
1911    async fn get_endpoints_success() {
1912        let server = MockServer::start().await;
1913
1914        Mock::given(method("GET"))
1915            .and(path("/endpoints"))
1916            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1917                "data": [
1918                    {
1919                        "id": "abc123",
1920                        "name": "aged-intensive-patron",
1921                        "label": "My Endpoint",
1922                        "status": "active",
1923                        "chain": "ethereum",
1924                        "network": "mainnet",
1925                        "is_dedicated": false,
1926                        "is_flat_rate": true,
1927                        "http_url": "https://example.quicknode.pro/abc123",
1928                        "wss_url": null,
1929                        "tags": []
1930                    }
1931                ],
1932                "pagination": {
1933                    "total": 1,
1934                    "limit": 20,
1935                    "offset": 0
1936                },
1937                "error": null
1938            })))
1939            .mount(&server)
1940            .await;
1941
1942        let sdk = make_sdk(format!("{}/", server.uri()));
1943        let resp = sdk
1944            .admin
1945            .get_endpoints(&GetEndpointsRequest::default())
1946            .await
1947            .unwrap();
1948
1949        assert_eq!(resp.data.len(), 1);
1950        assert_eq!(resp.data[0].id, "abc123");
1951        assert_eq!(resp.data[0].name, "aged-intensive-patron");
1952        assert_eq!(resp.data[0].status, "active");
1953        assert_eq!(resp.data[0].chain, "ethereum");
1954        assert!(!resp.data[0].is_dedicated);
1955        assert!(resp.data[0].is_flat_rate);
1956        let pagination = resp.pagination.expect("pagination present");
1957        assert_eq!(pagination.total, 1);
1958        assert_eq!(pagination.limit, 20);
1959        assert_eq!(pagination.offset, 0);
1960    }
1961
1962    #[tokio::test]
1963    async fn get_endpoints_sends_search_and_filter_params() {
1964        let server = MockServer::start().await;
1965
1966        Mock::given(method("GET"))
1967            .and(path("/endpoints"))
1968            .and(query_param("search", "intensive"))
1969            .and(query_param("networks[]", "mainnet"))
1970            .and(query_param("statuses[]", "active"))
1971            .and(query_param("dedicated", "true"))
1972            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1973                "data": [],
1974                "error": null
1975            })))
1976            .mount(&server)
1977            .await;
1978
1979        let sdk = make_sdk(format!("{}/", server.uri()));
1980        let params = GetEndpointsRequest {
1981            search: Some("intensive".to_string()),
1982            networks: Some(vec!["mainnet".to_string()]),
1983            statuses: Some(vec!["active".to_string()]),
1984            dedicated: Some(true),
1985            ..Default::default()
1986        };
1987        let resp = sdk.admin.get_endpoints(&params).await.unwrap();
1988
1989        assert_eq!(resp.data.len(), 0);
1990    }
1991
1992    #[tokio::test]
1993    async fn get_endpoints_api_error() {
1994        let server = MockServer::start().await;
1995
1996        Mock::given(method("GET"))
1997            .and(path("/endpoints"))
1998            .respond_with(ResponseTemplate::new(401).set_body_string("Unauthorized"))
1999            .mount(&server)
2000            .await;
2001
2002        let sdk = make_sdk(format!("{}/", server.uri()));
2003        let err = sdk
2004            .admin
2005            .get_endpoints(&GetEndpointsRequest::default())
2006            .await
2007            .unwrap_err();
2008
2009        match err {
2010            SdkError::Api { status, .. } => assert_eq!(status.as_u16(), 401),
2011            other => panic!("expected SdkError::Api, got {:?}", other),
2012        }
2013    }
2014
2015    #[tokio::test]
2016    async fn get_endpoints_sends_query_params() {
2017        let server = MockServer::start().await;
2018
2019        Mock::given(method("GET"))
2020            .and(path("/endpoints"))
2021            .and(query_param("limit", "10"))
2022            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2023                "data": [],
2024                "error": null
2025            })))
2026            .mount(&server)
2027            .await;
2028
2029        let sdk = make_sdk(format!("{}/", server.uri()));
2030        let params = GetEndpointsRequest {
2031            limit: Some(10),
2032            ..Default::default()
2033        };
2034        let resp = sdk.admin.get_endpoints(&params).await.unwrap();
2035
2036        assert_eq!(resp.data.len(), 0);
2037    }
2038
2039    #[tokio::test]
2040    async fn get_endpoints_base_url_without_trailing_slash() {
2041        let server = MockServer::start().await;
2042
2043        Mock::given(method("GET"))
2044            .and(path("/endpoints"))
2045            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2046                "data": [],
2047                "error": null
2048            })))
2049            .mount(&server)
2050            .await;
2051
2052        let base_url_no_slash = server.uri();
2053        let sdk = make_sdk(base_url_no_slash);
2054        let resp = sdk
2055            .admin
2056            .get_endpoints(&GetEndpointsRequest::default())
2057            .await
2058            .unwrap();
2059
2060        assert_eq!(resp.data.len(), 0);
2061    }
2062
2063    #[tokio::test]
2064    async fn create_endpoint_success() {
2065        let server = MockServer::start().await;
2066
2067        Mock::given(method("POST"))
2068            .and(path("/endpoints"))
2069            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2070                "data": {
2071                    "id": "ep123",
2072                    "label": null,
2073                    "status": "active",
2074                    "chain": "ethereum",
2075                    "network": "mainnet",
2076                    "http_url": "https://example.quicknode.pro/ep123",
2077                    "wss_url": null,
2078                    "security": {
2079                        "options": { "tokens": true, "jwts": false, "domainMasks": false, "ips": false, "referrers": false, "requestFilters": false },
2080                        "tokens": [{"id": "tok1", "token": "abc123"}],
2081                        "jwts": null,
2082                        "referrers": null,
2083                        "domain_masks": null,
2084                        "ips": null,
2085                        "request_filters": null
2086                    },
2087                    "rate_limits": null,
2088                    "tags": []
2089                },
2090                "error": null
2091            })))
2092            .mount(&server)
2093            .await;
2094
2095        let sdk = make_sdk(format!("{}/", server.uri()));
2096        let resp = sdk
2097            .admin
2098            .create_endpoint(&CreateEndpointRequest::default())
2099            .await
2100            .unwrap();
2101
2102        assert_eq!(resp.data.id, "ep123");
2103        assert_eq!(resp.data.chain, "ethereum");
2104        assert_eq!(resp.data.network, "mainnet");
2105        let security = resp.data.security.unwrap();
2106        assert!(security.tokens.unwrap().len() == 1);
2107        assert!(security.jwts.is_none());
2108    }
2109
2110    #[tokio::test]
2111    async fn create_endpoint_api_error() {
2112        let server = MockServer::start().await;
2113
2114        Mock::given(method("POST"))
2115            .and(path("/endpoints"))
2116            .respond_with(ResponseTemplate::new(400).set_body_string("Bad Request"))
2117            .mount(&server)
2118            .await;
2119
2120        let sdk = make_sdk(format!("{}/", server.uri()));
2121        let err = sdk
2122            .admin
2123            .create_endpoint(&CreateEndpointRequest::default())
2124            .await
2125            .unwrap_err();
2126
2127        match err {
2128            SdkError::Api { status, .. } => assert_eq!(status.as_u16(), 400),
2129            other => panic!("expected SdkError::Api, got {:?}", other),
2130        }
2131    }
2132
2133    #[tokio::test]
2134    async fn create_endpoint_sends_body() {
2135        use wiremock::matchers::body_json;
2136
2137        let server = MockServer::start().await;
2138
2139        Mock::given(method("POST"))
2140            .and(path("/endpoints"))
2141            .and(body_json(serde_json::json!({
2142                "chain": "solana",
2143                "network": "mainnet-beta"
2144            })))
2145            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2146                "data": {
2147                    "id": "ep456",
2148                    "label": null,
2149                    "status": "active",
2150                    "chain": "solana",
2151                    "network": "mainnet-beta",
2152                    "http_url": "https://example.quicknode.pro/ep456",
2153                    "wss_url": null,
2154                    "security": null,
2155                    "rate_limits": null,
2156                    "tags": []
2157                },
2158                "error": null
2159            })))
2160            .mount(&server)
2161            .await;
2162
2163        let sdk = make_sdk(format!("{}/", server.uri()));
2164        let params = CreateEndpointRequest {
2165            chain: Some("solana".to_string()),
2166            network: Some("mainnet-beta".to_string()),
2167        };
2168        let resp = sdk.admin.create_endpoint(&params).await.unwrap();
2169
2170        assert_eq!(resp.data.id, "ep456");
2171        assert_eq!(resp.data.chain, "solana");
2172    }
2173
2174    #[tokio::test]
2175    async fn show_endpoint_success() {
2176        let server = MockServer::start().await;
2177
2178        Mock::given(method("GET"))
2179            .and(path("/endpoints/ep123"))
2180            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2181                "data": {
2182                    "id": "ep123",
2183                    "label": null,
2184                    "status": "active",
2185                    "chain": "ethereum",
2186                    "network": "mainnet",
2187                    "http_url": "https://example.quicknode.pro/ep123",
2188                    "wss_url": null,
2189                    "security": null,
2190                    "rate_limits": null,
2191                    "tags": []
2192                },
2193                "error": null
2194            })))
2195            .mount(&server)
2196            .await;
2197
2198        let sdk = make_sdk(format!("{}/", server.uri()));
2199        let resp = sdk.admin.show_endpoint("ep123").await.unwrap();
2200        assert_eq!(resp.data.unwrap().id, "ep123");
2201    }
2202
2203    #[tokio::test]
2204    async fn show_endpoint_api_error() {
2205        let server = MockServer::start().await;
2206
2207        Mock::given(method("GET"))
2208            .and(path("/endpoints/ep123"))
2209            .respond_with(ResponseTemplate::new(404).set_body_string("Not Found"))
2210            .mount(&server)
2211            .await;
2212
2213        let sdk = make_sdk(format!("{}/", server.uri()));
2214        let err = sdk.admin.show_endpoint("ep123").await.unwrap_err();
2215        match err {
2216            SdkError::Api { status, .. } => assert_eq!(status.as_u16(), 404),
2217            other => panic!("expected SdkError::Api, got {:?}", other),
2218        }
2219    }
2220
2221    #[tokio::test]
2222    async fn update_endpoint_success() {
2223        let server = MockServer::start().await;
2224
2225        Mock::given(method("PATCH"))
2226            .and(path("/endpoints/ep123"))
2227            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({})))
2228            .mount(&server)
2229            .await;
2230
2231        let sdk = make_sdk(format!("{}/", server.uri()));
2232        sdk.admin
2233            .update_endpoint(
2234                "ep123",
2235                &UpdateEndpointRequest {
2236                    label: Some("New Name".to_string()),
2237                },
2238            )
2239            .await
2240            .unwrap();
2241    }
2242
2243    #[tokio::test]
2244    async fn archive_endpoint_success() {
2245        let server = MockServer::start().await;
2246
2247        Mock::given(method("DELETE"))
2248            .and(path("/endpoints/ep123"))
2249            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({})))
2250            .mount(&server)
2251            .await;
2252
2253        let sdk = make_sdk(format!("{}/", server.uri()));
2254        sdk.admin.archive_endpoint("ep123").await.unwrap();
2255    }
2256
2257    #[tokio::test]
2258    async fn update_endpoint_status_success() {
2259        let server = MockServer::start().await;
2260
2261        Mock::given(method("PATCH"))
2262            .and(path("/endpoints/ep123/status"))
2263            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2264                "data": "paused",
2265                "error": null
2266            })))
2267            .mount(&server)
2268            .await;
2269
2270        let sdk = make_sdk(format!("{}/", server.uri()));
2271        let resp = sdk
2272            .admin
2273            .update_endpoint_status(
2274                "ep123",
2275                &UpdateEndpointStatusRequest {
2276                    status: "paused".to_string(),
2277                },
2278            )
2279            .await
2280            .unwrap();
2281        assert_eq!(resp.data.unwrap(), "paused");
2282    }
2283
2284    #[tokio::test]
2285    async fn create_tag_success() {
2286        let server = MockServer::start().await;
2287
2288        Mock::given(method("POST"))
2289            .and(path("/endpoints/ep123/tags"))
2290            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({})))
2291            .mount(&server)
2292            .await;
2293
2294        let sdk = make_sdk(format!("{}/", server.uri()));
2295        sdk.admin
2296            .create_tag(
2297                "ep123",
2298                &CreateTagRequest {
2299                    label: Some("my-tag".to_string()),
2300                },
2301            )
2302            .await
2303            .unwrap();
2304    }
2305
2306    #[tokio::test]
2307    async fn delete_tag_success() {
2308        let server = MockServer::start().await;
2309
2310        Mock::given(method("DELETE"))
2311            .and(path("/endpoints/ep123/tags/tag456"))
2312            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({})))
2313            .mount(&server)
2314            .await;
2315
2316        let sdk = make_sdk(format!("{}/", server.uri()));
2317        sdk.admin.delete_tag("ep123", "tag456").await.unwrap();
2318    }
2319
2320    #[tokio::test]
2321    async fn get_usage_success() {
2322        let server = MockServer::start().await;
2323
2324        Mock::given(method("GET"))
2325            .and(path("/usage/rpc"))
2326            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2327                "data": {
2328                    "credits_used": 5000,
2329                    "credits_remaining": 95000,
2330                    "limit": 100000,
2331                    "overages": null,
2332                    "start_time": 1700000000,
2333                    "end_time": 1702592000
2334                },
2335                "error": null
2336            })))
2337            .mount(&server)
2338            .await;
2339
2340        let sdk = make_sdk(format!("{}/", server.uri()));
2341        let resp = sdk
2342            .admin
2343            .get_usage(&GetUsageRequest::default())
2344            .await
2345            .unwrap();
2346        assert_eq!(resp.data.unwrap().credits_used, 5000);
2347    }
2348
2349    #[tokio::test]
2350    async fn get_usage_by_endpoint_success() {
2351        let server = MockServer::start().await;
2352
2353        Mock::given(method("GET"))
2354            .and(path("/usage/rpc/by-endpoint"))
2355            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2356                "data": {
2357                    "endpoints": [{"name": "ep1", "chain": "eth", "network": "mainnet", "status": "active", "credits_used": 100, "label": null, "methods_breakdown": [], "requests": 50}],
2358                    "start_time": 1700000000,
2359                    "end_time": 1702592000
2360                },
2361                "error": null
2362            })))
2363            .mount(&server)
2364            .await;
2365
2366        let sdk = make_sdk(format!("{}/", server.uri()));
2367        let resp = sdk
2368            .admin
2369            .get_usage_by_endpoint(&GetUsageRequest::default())
2370            .await
2371            .unwrap();
2372        assert_eq!(resp.data.unwrap().endpoints.len(), 1);
2373    }
2374
2375    #[tokio::test]
2376    async fn get_usage_by_chain_success() {
2377        let server = MockServer::start().await;
2378
2379        Mock::given(method("GET"))
2380            .and(path("/usage/rpc/by-chain"))
2381            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2382                "data": {
2383                    "chains": [{"name": "ethereum", "credits_used": 1000}],
2384                    "start_time": 1700000000,
2385                    "end_time": 1702592000
2386                },
2387                "error": null
2388            })))
2389            .mount(&server)
2390            .await;
2391
2392        let sdk = make_sdk(format!("{}/", server.uri()));
2393        let resp = sdk
2394            .admin
2395            .get_usage_by_chain(&GetUsageRequest::default())
2396            .await
2397            .unwrap();
2398        assert_eq!(resp.data.unwrap().chains[0].name, "ethereum");
2399    }
2400
2401    #[tokio::test]
2402    async fn get_endpoint_logs_success() {
2403        let server = MockServer::start().await;
2404
2405        Mock::given(method("GET"))
2406            .and(path("/endpoints/ep123/logs"))
2407            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2408                "data": [
2409                    {
2410                        "timestamp": "2025-04-29T12:39:25.543Z",
2411                        "method": "eth_call",
2412                        "network": "mainnet",
2413                        "http_method": "POST",
2414                        "status": 200,
2415                        "error_code": null,
2416                        "url": "/",
2417                        "request_id": "abc-123",
2418                        "details": null
2419                    }
2420                ],
2421                "next_at": null
2422            })))
2423            .mount(&server)
2424            .await;
2425
2426        let sdk = make_sdk(format!("{}/", server.uri()));
2427        let params = GetEndpointLogsRequest {
2428            from: "2025-04-29T00:00:00Z".to_string(),
2429            to: "2025-04-29T23:59:59Z".to_string(),
2430            ..Default::default()
2431        };
2432        let resp = sdk.admin.get_endpoint_logs("ep123", &params).await.unwrap();
2433        assert_eq!(resp.data.len(), 1);
2434        assert_eq!(resp.data[0].method.as_deref(), Some("eth_call"));
2435    }
2436
2437    #[tokio::test]
2438    async fn get_log_details_success() {
2439        let server = MockServer::start().await;
2440
2441        Mock::given(method("GET"))
2442            .and(path("/endpoints/ep123/log_details"))
2443            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2444                "data": {
2445                    "request": "{\"method\":\"eth_call\"}",
2446                    "response": "{\"result\":\"0x1\"}"
2447                }
2448            })))
2449            .mount(&server)
2450            .await;
2451
2452        let sdk = make_sdk(format!("{}/", server.uri()));
2453        let resp = sdk.admin.get_log_details("ep123", "abc-123").await.unwrap();
2454        assert!(resp.data.is_some());
2455    }
2456
2457    #[tokio::test]
2458    async fn get_security_options_success() {
2459        let server = MockServer::start().await;
2460
2461        Mock::given(method("GET"))
2462            .and(path("/endpoints/ep123/security_options"))
2463            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2464                "data": [{"option": "tokens", "status": "enabled", "value": null}],
2465                "error": null
2466            })))
2467            .mount(&server)
2468            .await;
2469
2470        let sdk = make_sdk(format!("{}/", server.uri()));
2471        let resp = sdk.admin.get_security_options("ep123").await.unwrap();
2472        assert_eq!(resp.data.len(), 1);
2473        assert_eq!(resp.data[0].option, "tokens");
2474    }
2475
2476    #[tokio::test]
2477    async fn update_security_options_success() {
2478        let server = MockServer::start().await;
2479
2480        Mock::given(method("PATCH"))
2481            .and(path("/endpoints/ep123/security_options"))
2482            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2483                "data": [{"option": "tokens", "status": "disabled", "value": null}],
2484                "error": null
2485            })))
2486            .mount(&server)
2487            .await;
2488
2489        let sdk = make_sdk(format!("{}/", server.uri()));
2490        let params = UpdateSecurityOptionsRequest {
2491            options: SecurityOptionsUpdate {
2492                tokens: Some("disabled".to_string()),
2493                ..Default::default()
2494            },
2495        };
2496        let resp = sdk
2497            .admin
2498            .update_security_options("ep123", &params)
2499            .await
2500            .unwrap();
2501        assert_eq!(resp.data[0].status, "disabled");
2502    }
2503
2504    #[tokio::test]
2505    async fn create_token_success() {
2506        let server = MockServer::start().await;
2507
2508        Mock::given(method("POST"))
2509            .and(path("/endpoints/ep123/security/tokens"))
2510            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({})))
2511            .mount(&server)
2512            .await;
2513
2514        let sdk = make_sdk(format!("{}/", server.uri()));
2515        sdk.admin.create_token("ep123").await.unwrap();
2516    }
2517
2518    #[tokio::test]
2519    async fn delete_token_success() {
2520        let server = MockServer::start().await;
2521
2522        Mock::given(method("DELETE"))
2523            .and(path("/endpoints/ep123/security/tokens/tok1"))
2524            .respond_with(
2525                ResponseTemplate::new(200)
2526                    .set_body_json(serde_json::json!({"data": true, "error": null})),
2527            )
2528            .mount(&server)
2529            .await;
2530
2531        let sdk = make_sdk(format!("{}/", server.uri()));
2532        let resp = sdk.admin.delete_token("ep123", "tok1").await.unwrap();
2533        assert_eq!(resp.data, Some(true));
2534    }
2535
2536    #[tokio::test]
2537    async fn create_referrer_success() {
2538        let server = MockServer::start().await;
2539
2540        Mock::given(method("POST"))
2541            .and(path("/endpoints/ep123/security/referrers"))
2542            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({})))
2543            .mount(&server)
2544            .await;
2545
2546        let sdk = make_sdk(format!("{}/", server.uri()));
2547        sdk.admin
2548            .create_referrer(
2549                "ep123",
2550                &CreateReferrerRequest {
2551                    referrer: Some("example.com".to_string()),
2552                },
2553            )
2554            .await
2555            .unwrap();
2556    }
2557
2558    #[tokio::test]
2559    async fn enable_disable_multichain_success() {
2560        let server = MockServer::start().await;
2561
2562        Mock::given(method("POST"))
2563            .and(path("/endpoints/ep123/enable_multichain"))
2564            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({})))
2565            .mount(&server)
2566            .await;
2567
2568        Mock::given(method("POST"))
2569            .and(path("/endpoints/ep123/disable_multichain"))
2570            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({})))
2571            .mount(&server)
2572            .await;
2573
2574        let sdk = make_sdk(format!("{}/", server.uri()));
2575        sdk.admin.enable_multichain("ep123").await.unwrap();
2576        sdk.admin.disable_multichain("ep123").await.unwrap();
2577    }
2578
2579    #[tokio::test]
2580    async fn create_or_update_ip_custom_header_success() {
2581        let server = MockServer::start().await;
2582
2583        Mock::given(method("PATCH"))
2584            .and(path("/endpoints/ep123/ip_custom_header"))
2585            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2586                "data": {"header_name": "CF-Connecting-IP"},
2587                "error": null
2588            })))
2589            .mount(&server)
2590            .await;
2591
2592        let sdk = make_sdk(format!("{}/", server.uri()));
2593        let params = CreateOrUpdateIpCustomHeaderRequest {
2594            header_name: "CF-Connecting-IP".to_string(),
2595        };
2596        let resp = sdk
2597            .admin
2598            .create_or_update_ip_custom_header("ep123", &params)
2599            .await
2600            .unwrap();
2601        assert_eq!(resp.data.unwrap().header_name, "CF-Connecting-IP");
2602    }
2603
2604    #[tokio::test]
2605    async fn get_method_rate_limits_success() {
2606        let server = MockServer::start().await;
2607
2608        Mock::given(method("GET"))
2609            .and(path("/endpoints/ep123/method-rate-limits"))
2610            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2611                "data": {
2612                    "rate_limiters": [
2613                        {"id": "rl1", "interval": "second", "methods": ["eth_call"], "rate": 10, "status": "enabled", "created": "2024-01-01T00:00:00Z"}
2614                    ]
2615                },
2616                "error": null
2617            })))
2618            .mount(&server)
2619            .await;
2620
2621        let sdk = make_sdk(format!("{}/", server.uri()));
2622        let resp = sdk.admin.get_method_rate_limits("ep123").await.unwrap();
2623        assert_eq!(resp.data.unwrap().rate_limiters.len(), 1);
2624    }
2625
2626    #[tokio::test]
2627    async fn create_method_rate_limit_success() {
2628        let server = MockServer::start().await;
2629
2630        Mock::given(method("POST"))
2631            .and(path("/endpoints/ep123/method-rate-limits"))
2632            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2633                "data": {"id": "rl1", "interval": "second", "methods": ["eth_call"], "rate": 10, "status": "enabled", "created": "2024-01-01T00:00:00Z"},
2634                "error": null
2635            })))
2636            .mount(&server)
2637            .await;
2638
2639        let sdk = make_sdk(format!("{}/", server.uri()));
2640        let params = CreateMethodRateLimitRequest {
2641            interval: "second".to_string(),
2642            methods: vec!["eth_call".to_string()],
2643            rate: 10,
2644        };
2645        let resp = sdk
2646            .admin
2647            .create_method_rate_limit("ep123", &params)
2648            .await
2649            .unwrap();
2650        assert_eq!(resp.data.unwrap().id, "rl1");
2651    }
2652
2653    #[tokio::test]
2654    async fn update_method_rate_limit_success() {
2655        let server = MockServer::start().await;
2656
2657        Mock::given(method("PATCH"))
2658            .and(path("/endpoints/ep123/method-rate-limits/rl1"))
2659            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2660                "data": {"id": "rl1", "interval": "day", "methods": ["eth_call"], "rate": 30, "status": "enabled", "created": "2024-01-01T00:00:00Z"},
2661                "error": null
2662            })))
2663            .mount(&server)
2664            .await;
2665
2666        let sdk = make_sdk(format!("{}/", server.uri()));
2667        let params = UpdateMethodRateLimitRequest {
2668            rate: Some(30),
2669            ..Default::default()
2670        };
2671        let resp = sdk
2672            .admin
2673            .update_method_rate_limit("ep123", "rl1", &params)
2674            .await
2675            .unwrap();
2676        assert_eq!(resp.data.unwrap().rate, 30);
2677    }
2678
2679    #[tokio::test]
2680    async fn delete_method_rate_limit_success() {
2681        let server = MockServer::start().await;
2682
2683        Mock::given(method("DELETE"))
2684            .and(path("/endpoints/ep123/method-rate-limits/rl1"))
2685            .respond_with(
2686                ResponseTemplate::new(200)
2687                    .set_body_json(serde_json::json!({"data": "deleted", "error": null})),
2688            )
2689            .mount(&server)
2690            .await;
2691
2692        let sdk = make_sdk(format!("{}/", server.uri()));
2693        sdk.admin
2694            .delete_method_rate_limit("ep123", "rl1")
2695            .await
2696            .unwrap();
2697    }
2698
2699    #[tokio::test]
2700    async fn update_rate_limits_success() {
2701        let server = MockServer::start().await;
2702
2703        Mock::given(method("PATCH"))
2704            .and(path("/endpoints/ep123/rate-limits"))
2705            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({})))
2706            .mount(&server)
2707            .await;
2708
2709        let sdk = make_sdk(format!("{}/", server.uri()));
2710        let params = UpdateRateLimitsRequest {
2711            rate_limits: RateLimitSettings {
2712                rps: Some(100),
2713                rpm: None,
2714                rpd: None,
2715            },
2716        };
2717        sdk.admin
2718            .update_rate_limits("ep123", &params)
2719            .await
2720            .unwrap();
2721    }
2722
2723    #[tokio::test]
2724    async fn get_rate_limits_success() {
2725        let server = MockServer::start().await;
2726
2727        Mock::given(method("GET"))
2728            .and(path("/endpoints/ep123/rate-limits"))
2729            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2730                "data": {
2731                    "rate_limits": [
2732                        {"bucket": "rps", "rate_limit": 100, "source": "plan_default"},
2733                        {"bucket": "rpm", "rate_limit": 6000, "source": "user_override", "id": "ovr-1"}
2734                    ]
2735                },
2736                "error": null
2737            })))
2738            .mount(&server)
2739            .await;
2740
2741        let sdk = make_sdk(format!("{}/", server.uri()));
2742        let resp = sdk.admin.get_rate_limits("ep123").await.unwrap();
2743        let rows = resp.data.unwrap().rate_limits;
2744        assert_eq!(rows.len(), 2);
2745        assert_eq!(rows[0].source, "plan_default");
2746        assert!(rows[0].id.is_none());
2747        assert_eq!(rows[1].source, "user_override");
2748        assert_eq!(rows[1].rate_limit, 6000);
2749        assert_eq!(rows[1].id.as_deref(), Some("ovr-1"));
2750    }
2751
2752    #[tokio::test]
2753    async fn get_rate_limits_api_error() {
2754        let server = MockServer::start().await;
2755
2756        Mock::given(method("GET"))
2757            .and(path("/endpoints/missing/rate-limits"))
2758            .respond_with(ResponseTemplate::new(404).set_body_string("not found"))
2759            .mount(&server)
2760            .await;
2761
2762        let sdk = make_sdk(format!("{}/", server.uri()));
2763        let err = sdk.admin.get_rate_limits("missing").await.unwrap_err();
2764        match err {
2765            SdkError::Api { status, .. } => assert_eq!(status.as_u16(), 404),
2766            other => panic!("expected SdkError::Api, got {:?}", other),
2767        }
2768    }
2769
2770    #[tokio::test]
2771    async fn delete_rate_limit_override_success() {
2772        let server = MockServer::start().await;
2773
2774        Mock::given(method("DELETE"))
2775            .and(path("/endpoints/ep123/rate-limits/ovr-1"))
2776            .respond_with(ResponseTemplate::new(200).set_body_string(""))
2777            .mount(&server)
2778            .await;
2779
2780        let sdk = make_sdk(format!("{}/", server.uri()));
2781        sdk.admin
2782            .delete_rate_limit_override("ep123", "ovr-1")
2783            .await
2784            .unwrap();
2785    }
2786
2787    #[tokio::test]
2788    async fn delete_rate_limit_override_not_found() {
2789        let server = MockServer::start().await;
2790
2791        Mock::given(method("DELETE"))
2792            .and(path("/endpoints/ep123/rate-limits/bogus"))
2793            .respond_with(ResponseTemplate::new(404).set_body_string("override not found"))
2794            .mount(&server)
2795            .await;
2796
2797        let sdk = make_sdk(format!("{}/", server.uri()));
2798        let err = sdk
2799            .admin
2800            .delete_rate_limit_override("ep123", "bogus")
2801            .await
2802            .unwrap_err();
2803        match err {
2804            SdkError::Api { status, .. } => assert_eq!(status.as_u16(), 404),
2805            other => panic!("expected SdkError::Api, got {:?}", other),
2806        }
2807    }
2808
2809    #[tokio::test]
2810    async fn get_endpoint_urls_success() {
2811        let server = MockServer::start().await;
2812
2813        Mock::given(method("GET"))
2814            .and(path("/endpoints/ep123/urls"))
2815            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2816                "data": {
2817                    "http_url": "https://example.quiknode.pro/abc/",
2818                    "wss_url": "wss://example.quiknode.pro/abc/",
2819                    "multichain_urls": null
2820                },
2821                "error": null
2822            })))
2823            .mount(&server)
2824            .await;
2825
2826        let sdk = make_sdk(format!("{}/", server.uri()));
2827        let resp = sdk.admin.get_endpoint_urls("ep123").await.unwrap();
2828        let data = resp.data.unwrap();
2829        assert_eq!(data.http_url, "https://example.quiknode.pro/abc/");
2830        assert!(data.multichain_urls.is_none());
2831    }
2832
2833    #[tokio::test]
2834    async fn get_endpoint_urls_multichain() {
2835        let server = MockServer::start().await;
2836
2837        Mock::given(method("GET"))
2838            .and(path("/endpoints/ep123/urls"))
2839            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2840                "data": {
2841                    "http_url": "https://example.quiknode.pro/abc/",
2842                    "wss_url": null,
2843                    "multichain_urls": {
2844                        "ethereum-mainnet": {
2845                            "http_url": "https://example.quiknode.pro/abc/eth/",
2846                            "wss_url": "wss://example.quiknode.pro/abc/eth/"
2847                        }
2848                    }
2849                },
2850                "error": null
2851            })))
2852            .mount(&server)
2853            .await;
2854
2855        let sdk = make_sdk(format!("{}/", server.uri()));
2856        let resp = sdk.admin.get_endpoint_urls("ep123").await.unwrap();
2857        let data = resp.data.unwrap();
2858        let mc = data.multichain_urls.unwrap();
2859        assert_eq!(mc.len(), 1);
2860        assert_eq!(
2861            mc.get("ethereum-mainnet").unwrap().http_url,
2862            "https://example.quiknode.pro/abc/eth/"
2863        );
2864    }
2865
2866    #[tokio::test]
2867    async fn get_endpoint_metrics_success() {
2868        let server = MockServer::start().await;
2869
2870        Mock::given(method("GET"))
2871            .and(path("/endpoints/ep123/metrics"))
2872            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2873                "data": [{"data": [[1700000000, 42]], "tag": ["network", "mainnet"]}],
2874                "error": null
2875            })))
2876            .mount(&server)
2877            .await;
2878
2879        let sdk = make_sdk(format!("{}/", server.uri()));
2880        let params = GetEndpointMetricsRequest {
2881            period: "day".to_string(),
2882            metric: "credits_over_time".to_string(),
2883        };
2884        let resp = sdk
2885            .admin
2886            .get_endpoint_metrics("ep123", &params)
2887            .await
2888            .unwrap();
2889        assert_eq!(resp.data.len(), 1);
2890        assert_eq!(
2891            resp.data[0].tag,
2892            vec!["network".to_string(), "mainnet".to_string()]
2893        );
2894    }
2895
2896    #[tokio::test]
2897    async fn get_account_metrics_success() {
2898        let server = MockServer::start().await;
2899
2900        Mock::given(method("GET"))
2901            .and(path("/metrics"))
2902            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2903                "data": [{"data": [[1700000000, 100]], "tag": "total"}],
2904                "error": null
2905            })))
2906            .mount(&server)
2907            .await;
2908
2909        let sdk = make_sdk(format!("{}/", server.uri()));
2910        let params = GetAccountMetricsRequest {
2911            period: "week".to_string(),
2912            metric: "credits_over_time".to_string(),
2913            percentile: None,
2914        };
2915        let resp = sdk.admin.get_account_metrics(&params).await.unwrap();
2916        assert_eq!(resp.data.len(), 1);
2917        assert_eq!(resp.data[0].tag, vec!["total".to_string()]);
2918    }
2919
2920    // Regression: the metrics endpoints return `tag` as either a plain string
2921    // (single-axis series) or a `[key, value]` tuple (multi-axis series).
2922    // Exercise both shapes so any future serde change that breaks either
2923    // branch fails loudly.
2924    #[tokio::test]
2925    async fn get_account_metrics_decodes_tuple_tag() {
2926        let server = MockServer::start().await;
2927
2928        Mock::given(method("GET"))
2929            .and(path("/metrics"))
2930            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2931                "data": [
2932                    {"tag": ["network", "arbitrum-mainnet"], "data": [[1779109200, 40]]},
2933                    {"tag": ["network", "mainnet"], "data": [[1779116400, 40]]},
2934                    {"tag": "p95", "data": [[1779116400, 12]]}
2935                ],
2936                "error": null
2937            })))
2938            .mount(&server)
2939            .await;
2940
2941        let sdk = make_sdk(format!("{}/", server.uri()));
2942        let params = GetAccountMetricsRequest {
2943            period: "day".to_string(),
2944            metric: "credits_over_time".to_string(),
2945            percentile: None,
2946        };
2947        let resp = sdk.admin.get_account_metrics(&params).await.unwrap();
2948        assert_eq!(resp.data.len(), 3);
2949        assert_eq!(
2950            resp.data[0].tag,
2951            vec!["network".to_string(), "arbitrum-mainnet".to_string()]
2952        );
2953        assert_eq!(
2954            resp.data[1].tag,
2955            vec!["network".to_string(), "mainnet".to_string()]
2956        );
2957        assert_eq!(resp.data[2].tag, vec!["p95".to_string()]);
2958    }
2959
2960    #[tokio::test]
2961    async fn list_chains_success() {
2962        let server = MockServer::start().await;
2963
2964        Mock::given(method("GET"))
2965            .and(path("/chains"))
2966            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2967                "data": [
2968                    {
2969                        "slug": "eth",
2970                        "networks": [{"slug": "mainnet", "name": "Ethereum Mainnet", "chain_id": 1}],
2971                        "is_select_chain": true
2972                    }
2973                ],
2974                "error": null
2975            })))
2976            .mount(&server)
2977            .await;
2978
2979        let sdk = make_sdk(format!("{}/", server.uri()));
2980        let resp = sdk.admin.list_chains().await.unwrap();
2981        assert_eq!(resp.data.len(), 1);
2982        assert_eq!(resp.data[0].slug, "eth");
2983        assert_eq!(resp.data[0].networks[0].chain_id, Some(1));
2984    }
2985
2986    #[tokio::test]
2987    async fn list_invoices_success() {
2988        let server = MockServer::start().await;
2989
2990        Mock::given(method("GET"))
2991            .and(path("/billing/invoices"))
2992            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2993                "data": {
2994                    "invoices": [
2995                        {
2996                            "id": "inv123",
2997                            "status": "paid",
2998                            "billing_reason": "subscription",
2999                            "lines": [{"description": "Pro plan", "amount": 4900}],
3000                            "amount_due": 4900,
3001                            "amount_paid": 4900,
3002                            "period_start": 1700000000,
3003                            "period_end": 1702592000,
3004                            "created": 1700000000,
3005                            "subtotal": 4900
3006                        }
3007                    ]
3008                },
3009                "error": null
3010            })))
3011            .mount(&server)
3012            .await;
3013
3014        let sdk = make_sdk(format!("{}/", server.uri()));
3015        let resp = sdk.admin.list_invoices().await.unwrap();
3016        let data = resp.data.unwrap();
3017        assert_eq!(data.invoices.len(), 1);
3018        assert_eq!(data.invoices[0].id, "inv123");
3019    }
3020
3021    #[tokio::test]
3022    async fn list_invoices_api_error() {
3023        let server = MockServer::start().await;
3024
3025        Mock::given(method("GET"))
3026            .and(path("/billing/invoices"))
3027            .respond_with(ResponseTemplate::new(401).set_body_string("Unauthorized"))
3028            .mount(&server)
3029            .await;
3030
3031        let sdk = make_sdk(format!("{}/", server.uri()));
3032        let err = sdk.admin.list_invoices().await.unwrap_err();
3033        match err {
3034            SdkError::Api { status, .. } => assert_eq!(status.as_u16(), 401),
3035            other => panic!("expected SdkError::Api, got {:?}", other),
3036        }
3037    }
3038
3039    #[tokio::test]
3040    async fn list_payments_success() {
3041        let server = MockServer::start().await;
3042
3043        Mock::given(method("GET"))
3044            .and(path("/billing/payments"))
3045            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
3046                "data": {
3047                    "payments": [
3048                        {
3049                            "amount": "49.00",
3050                            "card_last_4": "4242",
3051                            "created_at": "2024-01-01T00:00:00Z",
3052                            "currency": "usd",
3053                            "status": "succeeded",
3054                            "marketplace_amount": "9.0"
3055                        }
3056                    ]
3057                },
3058                "error": null
3059            })))
3060            .mount(&server)
3061            .await;
3062
3063        let sdk = make_sdk(format!("{}/", server.uri()));
3064        let resp = sdk.admin.list_payments().await.unwrap();
3065        let data = resp.data.unwrap();
3066        assert_eq!(data.payments.len(), 1);
3067        assert_eq!(data.payments[0].currency, "usd");
3068    }
3069
3070    #[tokio::test]
3071    async fn list_teams_success() {
3072        let server = MockServer::start().await;
3073
3074        Mock::given(method("GET"))
3075            .and(path("/teams"))
3076            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
3077                "data": [{"id": 1, "name": "Engineering", "members_count": 5, "users": []}],
3078                "error": null
3079            })))
3080            .mount(&server)
3081            .await;
3082
3083        let sdk = make_sdk(format!("{}/", server.uri()));
3084        let resp = sdk.admin.list_teams().await.unwrap();
3085        assert_eq!(resp.data.len(), 1);
3086        assert_eq!(resp.data[0].name, "Engineering");
3087    }
3088
3089    #[tokio::test]
3090    async fn list_teams_api_error() {
3091        let server = MockServer::start().await;
3092
3093        Mock::given(method("GET"))
3094            .and(path("/teams"))
3095            .respond_with(ResponseTemplate::new(401).set_body_string("Unauthorized"))
3096            .mount(&server)
3097            .await;
3098
3099        let sdk = make_sdk(format!("{}/", server.uri()));
3100        let err = sdk.admin.list_teams().await.unwrap_err();
3101        match err {
3102            SdkError::Api { status, .. } => assert_eq!(status.as_u16(), 401),
3103            other => panic!("expected SdkError::Api, got {:?}", other),
3104        }
3105    }
3106
3107    #[tokio::test]
3108    async fn create_team_success() {
3109        let server = MockServer::start().await;
3110
3111        Mock::given(method("POST"))
3112            .and(path("/teams"))
3113            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
3114                "data": {"id": 42, "name": "New Team", "default_role": null, "members_count": 0},
3115                "error": null
3116            })))
3117            .mount(&server)
3118            .await;
3119
3120        let sdk = make_sdk(format!("{}/", server.uri()));
3121        let params = CreateTeamRequest {
3122            name: "New Team".to_string(),
3123        };
3124        let resp = sdk.admin.create_team(&params).await.unwrap();
3125        assert_eq!(resp.data.unwrap().id, 42);
3126    }
3127
3128    #[tokio::test]
3129    async fn get_team_success() {
3130        let server = MockServer::start().await;
3131
3132        Mock::given(method("GET"))
3133            .and(path("/teams/1"))
3134            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
3135                "data": {
3136                    "id": 1,
3137                    "name": "Engineering",
3138                    "default_role": "member",
3139                    "members_count": 3,
3140                    "users": [],
3141                    "pending_invites": []
3142                },
3143                "error": null
3144            })))
3145            .mount(&server)
3146            .await;
3147
3148        let sdk = make_sdk(format!("{}/", server.uri()));
3149        let resp = sdk.admin.get_team(1).await.unwrap();
3150        assert_eq!(resp.data.unwrap().name, "Engineering");
3151    }
3152
3153    #[tokio::test]
3154    async fn delete_team_success() {
3155        let server = MockServer::start().await;
3156
3157        Mock::given(method("DELETE"))
3158            .and(path("/teams/1"))
3159            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
3160                "data": {"message": "Team deleted"},
3161                "error": null
3162            })))
3163            .mount(&server)
3164            .await;
3165
3166        let sdk = make_sdk(format!("{}/", server.uri()));
3167        let resp = sdk.admin.delete_team(1).await.unwrap();
3168        assert!(resp.data.is_some());
3169    }
3170
3171    #[tokio::test]
3172    async fn list_team_endpoints_success() {
3173        let server = MockServer::start().await;
3174
3175        Mock::given(method("GET"))
3176            .and(path("/teams/1/endpoints"))
3177            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
3178                "data": [{"id": 10, "subdomain": "abc123", "chain": "ethereum", "network": "mainnet"}],
3179                "error": null
3180            })))
3181            .mount(&server)
3182            .await;
3183
3184        let sdk = make_sdk(format!("{}/", server.uri()));
3185        let resp = sdk.admin.list_team_endpoints(1).await.unwrap();
3186        assert_eq!(resp.data.len(), 1);
3187        assert_eq!(resp.data[0].subdomain, "abc123");
3188    }
3189
3190    #[tokio::test]
3191    async fn update_team_endpoints_success() {
3192        let server = MockServer::start().await;
3193
3194        Mock::given(method("PATCH"))
3195            .and(path("/teams/1/endpoints"))
3196            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
3197                "data": {"success": true},
3198                "error": null
3199            })))
3200            .mount(&server)
3201            .await;
3202
3203        let sdk = make_sdk(format!("{}/", server.uri()));
3204        let params = UpdateTeamEndpointsRequest {
3205            endpoint_ids: vec!["ep1".to_string()],
3206        };
3207        let resp = sdk.admin.update_team_endpoints(1, &params).await.unwrap();
3208        assert!(resp.data.unwrap().success.unwrap());
3209    }
3210
3211    // Wire-inspection regression: confirm an empty endpoint_ids array reaches
3212    // the wire as `[]` (not omitted), so any future `skip_serializing_if`
3213    // change that drops the empty case fails loudly.
3214    #[tokio::test]
3215    async fn update_team_endpoints_empty_array_wire_body() {
3216        use wiremock::matchers::body_json;
3217        let server = MockServer::start().await;
3218        Mock::given(method("PATCH"))
3219            .and(path("/teams/1/endpoints"))
3220            .and(body_json(serde_json::json!({ "endpoint_ids": [] })))
3221            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
3222                "data": {"success": true},
3223                "error": null
3224            })))
3225            .mount(&server)
3226            .await;
3227        let sdk = make_sdk(format!("{}/", server.uri()));
3228        let params = UpdateTeamEndpointsRequest {
3229            endpoint_ids: vec![],
3230        };
3231        sdk.admin.update_team_endpoints(1, &params).await.unwrap();
3232    }
3233
3234    #[tokio::test]
3235    async fn invite_team_member_success() {
3236        let server = MockServer::start().await;
3237
3238        Mock::given(method("POST"))
3239            .and(path("/teams/1/members"))
3240            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
3241                "data": {
3242                    "id": 99,
3243                    "email": "user@example.com",
3244                    "full_name": null,
3245                    "role": "member",
3246                    "status": "pending",
3247                    "created_at": null,
3248                    "photo_url": null,
3249                    "account_primary_user": null
3250                },
3251                "error": null
3252            })))
3253            .mount(&server)
3254            .await;
3255
3256        let sdk = make_sdk(format!("{}/", server.uri()));
3257        let params = InviteTeamMemberRequest {
3258            email: "user@example.com".to_string(),
3259            full_name: None,
3260            role: None,
3261        };
3262        let resp = sdk.admin.invite_team_member(1, &params).await.unwrap();
3263        assert_eq!(resp.data.unwrap().email, "user@example.com");
3264    }
3265
3266    #[tokio::test]
3267    async fn remove_team_member_success() {
3268        let server = MockServer::start().await;
3269
3270        Mock::given(method("DELETE"))
3271            .and(path("/teams/1/members/99"))
3272            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
3273                "data": {"message": "Member removed"},
3274                "error": null
3275            })))
3276            .mount(&server)
3277            .await;
3278
3279        let sdk = make_sdk(format!("{}/", server.uri()));
3280        let params = RemoveTeamMemberRequest { destroy_user: None };
3281        let resp = sdk.admin.remove_team_member(1, 99, &params).await.unwrap();
3282        assert!(resp.data.is_some());
3283    }
3284
3285    #[tokio::test]
3286    async fn resend_team_invite_success() {
3287        let server = MockServer::start().await;
3288
3289        Mock::given(method("POST"))
3290            .and(path("/teams/1/members/99/resend_invite"))
3291            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
3292                "data": {"message": "Invite resent"},
3293                "error": null
3294            })))
3295            .mount(&server)
3296            .await;
3297
3298        let sdk = make_sdk(format!("{}/", server.uri()));
3299        let resp = sdk.admin.resend_team_invite(1, 99).await.unwrap();
3300        assert!(resp.data.is_some());
3301    }
3302
3303    #[tokio::test]
3304    async fn bulk_update_endpoint_status_success() {
3305        let server = MockServer::start().await;
3306        Mock::given(method("POST"))
3307            .and(path("/endpoints/bulk/status"))
3308            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
3309                "data": {
3310                    "total": 2,
3311                    "updated_count": 2,
3312                    "failed_count": 0,
3313                    "results": [
3314                        { "id": "a", "success": true },
3315                        { "id": "b", "success": true }
3316                    ]
3317                }
3318            })))
3319            .mount(&server)
3320            .await;
3321
3322        let sdk = make_sdk(format!("{}/", server.uri()));
3323        let params = BulkUpdateEndpointStatusRequest {
3324            ids: vec!["a".to_string(), "b".to_string()],
3325            status: "paused".to_string(),
3326        };
3327        let resp = sdk
3328            .admin
3329            .bulk_update_endpoint_status(&params)
3330            .await
3331            .unwrap();
3332        let data = resp.data.expect("data present");
3333        assert_eq!(data.total, 2);
3334        assert_eq!(data.updated_count, 2);
3335        assert_eq!(data.results.len(), 2);
3336    }
3337
3338    #[tokio::test]
3339    async fn bulk_update_endpoint_status_api_error() {
3340        let server = MockServer::start().await;
3341        Mock::given(method("POST"))
3342            .and(path("/endpoints/bulk/status"))
3343            .respond_with(ResponseTemplate::new(400).set_body_string("bad request"))
3344            .mount(&server)
3345            .await;
3346
3347        let sdk = make_sdk(format!("{}/", server.uri()));
3348        let params = BulkUpdateEndpointStatusRequest {
3349            ids: vec!["a".to_string()],
3350            status: "paused".to_string(),
3351        };
3352        let err = sdk
3353            .admin
3354            .bulk_update_endpoint_status(&params)
3355            .await
3356            .unwrap_err();
3357        match err {
3358            SdkError::Api { status, .. } => assert_eq!(status.as_u16(), 400),
3359            other => panic!("expected Api, got {:?}", other),
3360        }
3361    }
3362
3363    #[tokio::test]
3364    async fn bulk_add_tag_success() {
3365        let server = MockServer::start().await;
3366        Mock::given(method("POST"))
3367            .and(path("/endpoints/bulk/tags"))
3368            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
3369                "data": {
3370                    "total": 1,
3371                    "updated_count": 1,
3372                    "failed_count": 0,
3373                    "results": [{ "id": "a", "success": true }],
3374                    "tag": { "tag_id": 7, "label": "prod" }
3375                }
3376            })))
3377            .mount(&server)
3378            .await;
3379
3380        let sdk = make_sdk(format!("{}/", server.uri()));
3381        let params = BulkAddTagRequest {
3382            ids: vec!["a".to_string()],
3383            label: "prod".to_string(),
3384        };
3385        let resp = sdk.admin.bulk_add_tag(&params).await.unwrap();
3386        let data = resp.data.expect("data present");
3387        assert_eq!(data.tag.tag_id, 7);
3388        assert_eq!(data.tag.label, "prod");
3389    }
3390
3391    #[tokio::test]
3392    async fn bulk_remove_tag_success() {
3393        let server = MockServer::start().await;
3394        Mock::given(method("DELETE"))
3395            .and(path("/endpoints/bulk/tags"))
3396            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
3397                "data": {
3398                    "total": 2,
3399                    "updated_count": 2,
3400                    "failed_count": 0,
3401                    "results": [
3402                        { "id": "a", "success": true },
3403                        { "id": "b", "success": true }
3404                    ]
3405                }
3406            })))
3407            .mount(&server)
3408            .await;
3409
3410        let sdk = make_sdk(format!("{}/", server.uri()));
3411        let params = BulkRemoveTagRequest {
3412            ids: vec!["a".to_string(), "b".to_string()],
3413            tag_id: 42,
3414        };
3415        let resp = sdk.admin.bulk_remove_tag(&params).await.unwrap();
3416        assert_eq!(resp.data.expect("data").updated_count, 2);
3417    }
3418
3419    #[test]
3420    fn endpoint_token_debug_is_redacted() {
3421        let t = EndpointToken {
3422            id: "tok_1".to_string(),
3423            token: "super-secret".to_string(),
3424        };
3425        let dbg = format!("{t:?}");
3426        assert!(dbg.contains("tok_1"));
3427        assert!(!dbg.contains("super-secret"));
3428        assert!(dbg.contains("[redacted]"));
3429    }
3430
3431    #[test]
3432    fn endpoint_jwt_debug_redacts_public_key() {
3433        let j = EndpointJwt {
3434            id: "jwt_1".to_string(),
3435            public_key: "-----BEGIN PUBLIC KEY-----\nAAAA\n-----END PUBLIC KEY-----".to_string(),
3436            kid: "kid1".to_string(),
3437            name: "myjwt".to_string(),
3438        };
3439        let dbg = format!("{j:?}");
3440        assert!(dbg.contains("jwt_1"));
3441        assert!(dbg.contains("kid1"));
3442        assert!(!dbg.contains("BEGIN PUBLIC KEY"));
3443        assert!(dbg.contains("[redacted]"));
3444    }
3445
3446    #[tokio::test]
3447    async fn bulk_methods_reject_empty_ids() {
3448        // No MockServer: the guards must fail before any HTTP request fires.
3449        let sdk = make_sdk("http://127.0.0.1:1/".to_string());
3450
3451        let err = sdk
3452            .admin
3453            .bulk_update_endpoint_status(&BulkUpdateEndpointStatusRequest {
3454                ids: vec![],
3455                status: "paused".to_string(),
3456            })
3457            .await
3458            .unwrap_err();
3459        assert!(matches!(err, SdkError::Config(_)));
3460
3461        let err = sdk
3462            .admin
3463            .bulk_add_tag(&BulkAddTagRequest {
3464                ids: vec![],
3465                label: "x".to_string(),
3466            })
3467            .await
3468            .unwrap_err();
3469        assert!(matches!(err, SdkError::Config(_)));
3470
3471        let err = sdk
3472            .admin
3473            .bulk_remove_tag(&BulkRemoveTagRequest {
3474                ids: vec![],
3475                tag_id: 1,
3476            })
3477            .await
3478            .unwrap_err();
3479        assert!(matches!(err, SdkError::Config(_)));
3480    }
3481
3482    #[tokio::test]
3483    async fn list_tags_success() {
3484        let server = MockServer::start().await;
3485        Mock::given(method("GET"))
3486            .and(path("/endpoints/tags"))
3487            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
3488                "data": {
3489                    "tags": [
3490                        { "id": 1, "label": "prod", "usage_count": 3 },
3491                        { "id": 2, "label": "staging", "usage_count": 0 }
3492                    ]
3493                },
3494                "error": null
3495            })))
3496            .mount(&server)
3497            .await;
3498
3499        let sdk = make_sdk(format!("{}/", server.uri()));
3500        let resp = sdk.admin.list_tags().await.unwrap();
3501        let data = resp.data.expect("data present");
3502        assert_eq!(data.tags.len(), 2);
3503        assert_eq!(data.tags[0].label, "prod");
3504        assert_eq!(data.tags[1].usage_count, 0);
3505    }
3506
3507    #[tokio::test]
3508    async fn list_tags_api_error() {
3509        let server = MockServer::start().await;
3510        Mock::given(method("GET"))
3511            .and(path("/endpoints/tags"))
3512            .respond_with(ResponseTemplate::new(500).set_body_string("oops"))
3513            .mount(&server)
3514            .await;
3515
3516        let sdk = make_sdk(format!("{}/", server.uri()));
3517        let err = sdk.admin.list_tags().await.unwrap_err();
3518        match err {
3519            SdkError::Api { status, .. } => assert_eq!(status.as_u16(), 500),
3520            other => panic!("expected Api, got {:?}", other),
3521        }
3522    }
3523
3524    #[tokio::test]
3525    async fn rename_tag_success() {
3526        let server = MockServer::start().await;
3527        Mock::given(method("PATCH"))
3528            .and(path("/endpoints/tags/7"))
3529            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
3530                "data": { "id": 7, "label": "prod-v2", "usage_count": 3 },
3531                "error": null
3532            })))
3533            .mount(&server)
3534            .await;
3535
3536        let sdk = make_sdk(format!("{}/", server.uri()));
3537        let params = RenameTagRequest {
3538            label: "prod-v2".to_string(),
3539        };
3540        let resp = sdk.admin.rename_tag(7, &params).await.unwrap();
3541        let tag = resp.data.expect("tag present");
3542        assert_eq!(tag.id, 7);
3543        assert_eq!(tag.label, "prod-v2");
3544    }
3545
3546    #[tokio::test]
3547    async fn delete_account_tag_success() {
3548        let server = MockServer::start().await;
3549        Mock::given(method("DELETE"))
3550            .and(path("/endpoints/tags/7"))
3551            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
3552                "data": { "success": true },
3553                "error": null
3554            })))
3555            .mount(&server)
3556            .await;
3557
3558        let sdk = make_sdk(format!("{}/", server.uri()));
3559        let resp = sdk.admin.delete_account_tag(7).await.unwrap();
3560        assert!(resp.data.expect("data").success);
3561    }
3562
3563    #[tokio::test]
3564    async fn delete_account_tag_still_in_use() {
3565        let server = MockServer::start().await;
3566        Mock::given(method("DELETE"))
3567            .and(path("/endpoints/tags/7"))
3568            .respond_with(ResponseTemplate::new(400).set_body_string("tag still in use"))
3569            .mount(&server)
3570            .await;
3571
3572        let sdk = make_sdk(format!("{}/", server.uri()));
3573        let err = sdk.admin.delete_account_tag(7).await.unwrap_err();
3574        match err {
3575            SdkError::Api { status, .. } => assert_eq!(status.as_u16(), 400),
3576            other => panic!("expected Api, got {:?}", other),
3577        }
3578    }
3579
3580    #[tokio::test]
3581    async fn get_usage_by_tag_success() {
3582        let server = MockServer::start().await;
3583        Mock::given(method("GET"))
3584            .and(path("/usage/rpc/by-tag"))
3585            .and(query_param("start_time", "1700000000"))
3586            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
3587                "data": {
3588                    "tags": [
3589                        { "tag_id": 1, "label": "prod", "credits_used": 1234, "requests": 10 },
3590                        { "tag_id": null, "label": "untagged", "credits_used": 50, "requests": 2 }
3591                    ],
3592                    "start_time": 1700000000,
3593                    "end_time": 1700003600
3594                },
3595                "error": null
3596            })))
3597            .mount(&server)
3598            .await;
3599
3600        let sdk = make_sdk(format!("{}/", server.uri()));
3601        let params = GetUsageRequest {
3602            start_time: Some(1_700_000_000),
3603            ..Default::default()
3604        };
3605        let resp = sdk.admin.get_usage_by_tag(&params).await.unwrap();
3606        let data = resp.data.expect("data present");
3607        assert_eq!(data.tags.len(), 2);
3608        assert_eq!(data.tags[0].tag_id, Some(1));
3609        assert_eq!(data.tags[1].tag_id, None);
3610        assert_eq!(data.tags[1].label, "untagged");
3611    }
3612
3613    #[tokio::test]
3614    async fn get_endpoint_security_success() {
3615        let server = MockServer::start().await;
3616        Mock::given(method("GET"))
3617            .and(path("/endpoints/abc123/security"))
3618            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
3619                "data": {
3620                    "options": { "tokens": true, "ips": false },
3621                    "tokens": [{ "id": "tok_1", "token": "secret" }],
3622                    "jwts": [],
3623                    "referrers": [],
3624                    "domain_masks": [],
3625                    "ips": [],
3626                    "request_filters": []
3627                },
3628                "error": null
3629            })))
3630            .mount(&server)
3631            .await;
3632
3633        let sdk = make_sdk(format!("{}/", server.uri()));
3634        let resp = sdk.admin.get_endpoint_security("abc123").await.unwrap();
3635        let data = resp.data.expect("data present");
3636        let tokens = data.tokens.expect("tokens present");
3637        assert_eq!(tokens.len(), 1);
3638        assert_eq!(tokens[0].id, "tok_1");
3639    }
3640
3641    #[tokio::test]
3642    async fn get_endpoint_security_not_found() {
3643        let server = MockServer::start().await;
3644        Mock::given(method("GET"))
3645            .and(path("/endpoints/missing/security"))
3646            .respond_with(ResponseTemplate::new(404).set_body_string("not found"))
3647            .mount(&server)
3648            .await;
3649
3650        let sdk = make_sdk(format!("{}/", server.uri()));
3651        let err = sdk
3652            .admin
3653            .get_endpoint_security("missing")
3654            .await
3655            .unwrap_err();
3656        match err {
3657            SdkError::Api { status, .. } => assert_eq!(status.as_u16(), 404),
3658            other => panic!("expected Api, got {:?}", other),
3659        }
3660    }
3661
3662    #[test]
3663    fn negative_timeout_secs_returns_error() {
3664        use crate::{HttpConfig, SdkConfig, SdkFullConfig};
3665        let result = SdkConfig::new(&SdkFullConfig {
3666            api_key: "test-key".to_string(),
3667            http: Some(HttpConfig {
3668                timeout_secs: Some(-1),
3669                pool_max_idle_per_host: None,
3670            }),
3671            admin: None,
3672            streams: None,
3673            webhooks: None,
3674            kvstore: None,
3675        });
3676        assert!(matches!(result, Err(crate::errors::SdkError::Config(_))));
3677    }
3678}