From 3ec1de782b6ee9dc335bcdc813babcf32884cfe1 Mon Sep 17 00:00:00 2001 From: John Mitsch Date: Wed, 20 May 2026 15:00:02 -0400 Subject: [PATCH 1/3] Add new admin endpoints + is_multichain field (DX-5284) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Surface four admin endpoints the API now supports but the SDK didn't: get_rate_limits, delete_rate_limit_override, and get_endpoint_urls are new; update_rate_limits switches from the deprecated PUT to the canonical PATCH (signature unchanged). Adds the is_multichain field to Endpoint and SingleEndpoint to match the spec. Also gates the streams_e2e example on the rust feature in crates/core/Cargo.toml — without it, the default-feature build couldn't find CreateStreamParams::builder() (bon-derived under feature = "rust"). One-line manifest fix folded in. --- crates/core/Cargo.toml | 4 + crates/core/README.md | 55 +++- crates/core/examples/admin.rs | 32 ++- crates/core/examples/admin_e2e.rs | 26 ++ crates/core/src/admin/endpoint_rate_limits.rs | 42 +++ crates/core/src/admin/endpoint_urls.rs | 47 ++++ crates/core/src/admin/endpoints.rs | 6 + crates/core/src/admin/mod.rs | 243 +++++++++++++++++- crates/node/src/lib.rs | 45 +++- crates/python/src/lib.rs | 65 ++++- crates/ruby/src/lib.rs | 42 +++ npm/README.md | 53 +++- npm/examples/admin.ts | 29 ++- npm/index.d.ts | 87 ++++++- npm/sdk.d.ts | 7 + python/README.md | 50 +++- python/examples/admin.py | 26 +- python/sdk/__init__.py | 12 + python/sdk/__init__.pyi | 12 + python/sdk/_core/__init__.pyi | 233 ++++++++++++++++- python/sdk/init_manual_override.pyi | 12 + ruby/README.md | 51 +++- ruby/examples/admin.rb | 13 +- ruby/examples/admin_e2e.rb | 18 ++ ruby/sig/quicknode_sdk.rbs | 3 + 25 files changed, 1181 insertions(+), 32 deletions(-) create mode 100644 crates/core/src/admin/endpoint_urls.rs diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 4183040..2f3aa4e 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -47,6 +47,10 @@ required-features = ["rust"] name = "streams" required-features = ["rust"] +[[example]] +name = "streams_e2e" +required-features = ["rust"] + [[example]] name = "webhooks_e2e" required-features = ["rust"] diff --git a/crates/core/README.md b/crates/core/README.md index 3a2bdb0..cc722d5 100644 --- a/crates/core/README.md +++ b/crates/core/README.md @@ -28,6 +28,7 @@ This is one of four language bindings published from the same Rust core. See the - [IP Custom Headers](#ip-custom-headers) - [Method Rate Limits](#method-rate-limits) - [Endpoint Rate Limits](#endpoint-rate-limits) + - [Endpoint URLs](#endpoint-urls) - [Metrics](#metrics) - [Chains](#chains) - [Billing](#billing) @@ -815,7 +816,7 @@ qn.admin.delete_method_rate_limit("ep-123", "rl-1").await?; ##### `update_rate_limits` / `updateRateLimits` -Updates the endpoint-level RPS / RPM / RPD caps. +Partial update of the endpoint-level RPS / RPM / RPD caps. Only buckets included in the request are modified — omitted buckets are left unchanged. Values are capped by the account's plan tier. Sends `PATCH`. **Parameters**: `id` (endpoint id, required); `rate_limits`: `RateLimitSettings` (`rps`, `rpm`, `rpd`, all optional). @@ -828,6 +829,58 @@ let params = UpdateRateLimitsRequest { rate_limits }; qn.admin.update_rate_limits("ep-123", ¶ms).await?; ``` +##### `get_rate_limits` / `getRateLimits` + +Returns the rate-limit rows currently enforced on the endpoint, each identifying its `bucket` (`"rps"` / `"rpm"` / `"rpd"`), `value`, and `source` (`"plan_default"` or `"user_override"`). User-set overrides expose an `override_id` you can pass to `delete_rate_limit_override`. + +**Parameters**: `id` (endpoint id, required). + +**Returns**: `GetRateLimitsResponse` with `data.rate_limits: Vec`. + +```rust +// Rust +let resp = qn.admin.get_rate_limits("ep-123").await?; +for row in resp.data.unwrap().rate_limits { + println!("{} {} {} {:?}", row.bucket, row.value, row.source, row.override_id); +} +``` + +##### `delete_rate_limit_override` / `deleteRateLimitOverride` + +Deletes a user-set rate-limit override by UUID. Plan defaults are not deletable — passing a UUID that does not match a user-set override on the endpoint returns 404. + +**Parameters**: `id` (endpoint id, required); `override_id` (UUID returned by `get_rate_limits`, required). + +**Returns**: nothing. + +```rust +// Rust +qn.admin.delete_rate_limit_override("ep-123", "ovr-uuid").await?; +``` + +#### Endpoint URLs + +##### `get_endpoint_urls` / `getEndpointUrls` + +Returns the HTTP and WebSocket URLs for the endpoint without fetching the full endpoint record. For multichain endpoints, `multichain_urls` is a per-network map of additional URLs; for single-chain endpoints it is `None`. + +**Parameters**: `id` (endpoint id, required). + +**Returns**: `GetEndpointUrlsResponse` with `data.http_url`, `data.wss_url`, and `data.multichain_urls`. + +```rust +// Rust +let resp = qn.admin.get_endpoint_urls("ep-123").await?; +if let Some(data) = resp.data { + println!("{}", data.http_url); + if let Some(mc) = data.multichain_urls { + for (network, urls) in mc { + println!("{network} {}", urls.http_url); + } + } +} +``` + #### Metrics ##### `get_endpoint_metrics` / `getEndpointMetrics` diff --git a/crates/core/examples/admin.rs b/crates/core/examples/admin.rs index 6af1867..b1d9d4d 100644 --- a/crates/core/examples/admin.rs +++ b/crates/core/examples/admin.rs @@ -12,7 +12,7 @@ async fn main() { .sort_direction("desc".to_string()) .build(); - match qn.admin.get_endpoints(¶ms).await { + let first_endpoint_id = match qn.admin.get_endpoints(¶ms).await { Ok(resp) => { if let Some(p) = &resp.pagination { println!( @@ -25,11 +25,35 @@ async fn main() { } for ep in &resp.data { println!( - "{} | {} | {} | {} | dedicated={} flat={}", - ep.id, ep.name, ep.status, ep.chain, ep.is_dedicated, ep.is_flat_rate + "{} | {} | {} | {} | dedicated={} flat={} multichain={}", + ep.id, + ep.name, + ep.status, + ep.chain, + ep.is_dedicated, + ep.is_flat_rate, + ep.is_multichain ); } + resp.data.first().map(|ep| ep.id.clone()) } - Err(e) => eprintln!("Error: {e}"), + Err(e) => { + eprintln!("get_endpoints error: {e}"); + None + } + }; + + let Some(endpoint_id) = first_endpoint_id else { + return; + }; + + match qn.admin.get_rate_limits(&endpoint_id).await { + Ok(resp) => println!("get_rate_limits: {:?}", resp.data), + Err(e) => eprintln!("get_rate_limits error: {e}"), + } + + match qn.admin.get_endpoint_urls(&endpoint_id).await { + Ok(resp) => println!("get_endpoint_urls: {:?}", resp.data), + Err(e) => eprintln!("get_endpoint_urls error: {e}"), } } diff --git a/crates/core/examples/admin_e2e.rs b/crates/core/examples/admin_e2e.rs index e78496d..54aaa96 100644 --- a/crates/core/examples/admin_e2e.rs +++ b/crates/core/examples/admin_e2e.rs @@ -571,6 +571,16 @@ async fn main() { Err(e) => eprintln!("update_rate_limits error: {e}"), } + match qn.admin.get_rate_limits(&endpoint_id).await { + Ok(resp) => println!("get_rate_limits: {:?}", resp.data), + Err(e) => eprintln!("get_rate_limits error: {e}"), + } + + match qn.admin.get_endpoint_urls(&endpoint_id).await { + Ok(resp) => println!("get_endpoint_urls: {:?}", resp.data), + Err(e) => eprintln!("get_endpoint_urls error: {e}"), + } + match qn.admin.get_method_rate_limits(&endpoint_id).await { Ok(resp) => println!("get_method_rate_limits: {:?}", resp.data), Err(e) => eprintln!("get_method_rate_limits error: {e}"), @@ -826,6 +836,22 @@ async fn main() { other => eprintln!("expected Api 404, got {other:?}"), } + // 1b) Rate-limit override delete with a bogus override id — also a 404. + match qn + .admin + .delete_rate_limit_override("does-not-exist", "00000000-0000-0000-0000-000000000000") + .await + { + Err(SdkError::Api { status, body }) => { + println!( + "delete_rate_limit_override api error {status}: {}", + &body[..body.len().min(80)] + ); + assert_eq!(status.as_u16(), 404); + } + other => eprintln!("expected Api 404 from delete_rate_limit_override, got {other:?}"), + } + // 2) Timeout path — unreachable base URL + 1s timeout forces a timeout // from reqwest, which maps to SdkError::Http with http_kind() == Timeout. let blackhole = SdkFullConfig { diff --git a/crates/core/src/admin/endpoint_rate_limits.rs b/crates/core/src/admin/endpoint_rate_limits.rs index 543f8d3..a4512a5 100644 --- a/crates/core/src/admin/endpoint_rate_limits.rs +++ b/crates/core/src/admin/endpoint_rate_limits.rs @@ -137,3 +137,45 @@ pub struct UpdateRateLimitsRequest { /// Rate limit values to apply. pub rate_limits: RateLimitSettings, } + +/// A single rate-limit row returned by `get_rate_limits`, identifying the +/// bucket (`rps`/`rpm`/`rpd`), the value enforced, and whether the value comes +/// from the plan default or a user-set override. +#[cfg_attr(feature = "python", gen_stub_pyclass)] +#[cfg_attr(feature = "python", pyclass(get_all, set_all))] +#[cfg_attr(feature = "node", napi(object))] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RateLimitEntry { + /// Which bucket this row applies to: `rps`, `rpm`, or `rpd`. + pub bucket: String, + /// The enforced value for this bucket. + pub value: i32, + /// Where the value comes from: `plan_default` or `user_override`. + pub source: String, + /// Identifier of the user-set override, present only when `source` is + /// `user_override`. Pass this to `delete_rate_limit_override` to remove it. + pub override_id: Option, +} + +/// Inner data for `get_rate_limits`. +#[cfg_attr(feature = "python", gen_stub_pyclass)] +#[cfg_attr(feature = "python", pyclass(get_all, set_all))] +#[cfg_attr(feature = "node", napi(object))] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetRateLimitsData { + /// One row per enforced bucket. + #[serde(default)] + pub rate_limits: Vec, +} + +/// Response from `get_rate_limits`. +#[cfg_attr(feature = "python", gen_stub_pyclass)] +#[cfg_attr(feature = "python", pyclass(get_all, set_all))] +#[cfg_attr(feature = "node", napi(object))] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetRateLimitsResponse { + /// Rate-limit rows with their source. + pub data: Option, + /// Error message when the request did not succeed. + pub error: Option, +} diff --git a/crates/core/src/admin/endpoint_urls.rs b/crates/core/src/admin/endpoint_urls.rs new file mode 100644 index 0000000..b8f69e6 --- /dev/null +++ b/crates/core/src/admin/endpoint_urls.rs @@ -0,0 +1,47 @@ +#[cfg(feature = "node")] +use napi_derive::napi; +#[cfg(feature = "python")] +use pyo3::pyclass; +#[cfg(feature = "python")] +use pyo3_stub_gen::derive::gen_stub_pyclass; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +/// HTTP/WSS URL pair for a single network on a multichain endpoint. +#[cfg_attr(feature = "python", gen_stub_pyclass)] +#[cfg_attr(feature = "python", pyclass(get_all, set_all))] +#[cfg_attr(feature = "node", napi(object))] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EndpointUrl { + /// HTTP RPC URL. + pub http_url: String, + /// WebSocket RPC URL, when available. + pub wss_url: Option, +} + +/// Inner data for `get_endpoint_urls` — the http/wss URLs for the endpoint and, +/// when the endpoint is multichain, a per-network map of additional URLs. +#[cfg_attr(feature = "python", gen_stub_pyclass)] +#[cfg_attr(feature = "python", pyclass(get_all, set_all))] +#[cfg_attr(feature = "node", napi(object))] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetEndpointUrlsData { + /// HTTP RPC URL. + pub http_url: String, + /// WebSocket RPC URL, when available. + pub wss_url: Option, + /// Per-network URLs for multichain endpoints; `None` for single-chain endpoints. + pub multichain_urls: Option>, +} + +/// Response from `get_endpoint_urls`. +#[cfg_attr(feature = "python", gen_stub_pyclass)] +#[cfg_attr(feature = "python", pyclass(get_all, set_all))] +#[cfg_attr(feature = "node", napi(object))] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetEndpointUrlsResponse { + /// URLs for the endpoint. + pub data: Option, + /// Error message when the request did not succeed. + pub error: Option, +} diff --git a/crates/core/src/admin/endpoints.rs b/crates/core/src/admin/endpoints.rs index 9a058ea..df9ed22 100644 --- a/crates/core/src/admin/endpoints.rs +++ b/crates/core/src/admin/endpoints.rs @@ -111,6 +111,9 @@ pub struct Endpoint { /// Tags applied to the endpoint. #[serde(default)] pub tags: Vec, + /// Whether the endpoint is configured to serve multiple chains/networks. + #[serde(default)] + pub is_multichain: bool, } /// Tag reference as returned on an endpoint. @@ -179,6 +182,9 @@ pub struct SingleEndpoint { /// Tags applied to the endpoint. #[serde(default)] pub tags: Vec, + /// Whether the endpoint is configured to serve multiple chains/networks. + #[serde(default)] + pub is_multichain: bool, } /// Rate limits applied to an endpoint. diff --git a/crates/core/src/admin/mod.rs b/crates/core/src/admin/mod.rs index 42ec3b2..b1f487c 100644 --- a/crates/core/src/admin/mod.rs +++ b/crates/core/src/admin/mod.rs @@ -4,6 +4,7 @@ pub mod chains; pub mod endpoint_metrics; pub mod endpoint_rate_limits; pub mod endpoint_security; +pub mod endpoint_urls; pub mod endpoints; pub mod logs; pub mod tags; @@ -26,8 +27,9 @@ pub use endpoint_metrics::{ }; pub use endpoint_rate_limits::{ CreateMethodRateLimitRequest, CreateMethodRateLimitResponse, GetMethodRateLimitsData, - GetMethodRateLimitsResponse, MethodRateLimiter, RateLimitSettings, - UpdateMethodRateLimitRequest, UpdateMethodRateLimitResponse, UpdateRateLimitsRequest, + GetMethodRateLimitsResponse, GetRateLimitsData, GetRateLimitsResponse, MethodRateLimiter, + RateLimitEntry, RateLimitSettings, UpdateMethodRateLimitRequest, UpdateMethodRateLimitResponse, + UpdateRateLimitsRequest, }; pub use endpoint_security::{ CreateDomainMaskRequest, CreateIpRequest, CreateJwtRequest, @@ -37,6 +39,7 @@ pub use endpoint_security::{ IpCustomHeaderData, SecurityOption, SecurityOptionsUpdate, UpdateRequestFilterRequest, UpdateSecurityOptionsRequest, UpdateSecurityOptionsResponse, }; +pub use endpoint_urls::{EndpointUrl, GetEndpointUrlsData, GetEndpointUrlsResponse}; pub use endpoints::{ CreateEndpointRequest, CreateEndpointResponse, CreateTagRequest, Endpoint, EndpointDomainMask, EndpointIp, EndpointIpCustomHeaderOption, EndpointJwt, EndpointRateLimits, EndpointReferrer, @@ -1363,9 +1366,11 @@ impl AdminApiClient { Ok(()) } - /// Updates the overall rate limits on an endpoint. Accepts `rps` - /// (requests per second), `rpm` (requests per minute), and `rpd` (requests - /// per day). + /// Partial update of the endpoint-level rate-limit overrides. Accepts + /// `rps` (requests per second), `rpm` (requests per minute), and `rpd` + /// (requests per day). Only buckets included in the request body are + /// modified — omitted buckets are left unchanged. Values are capped by the + /// account's plan tier. pub async fn update_rate_limits( &self, id: &str, @@ -1379,7 +1384,7 @@ impl AdminApiClient { let resp = self .config .http_client() - .put(url) + .patch(url) .json(params) .send() .await @@ -1394,6 +1399,90 @@ impl AdminApiClient { Ok(()) } + /// Returns the endpoint-level rate limits currently enforced, with each + /// row identifying its bucket (`rps`/`rpm`/`rpd`), value, and source + /// (`plan_default` or `user_override`). User-set overrides expose an + /// `override_id` that can be passed to `delete_rate_limit_override`. + pub async fn get_rate_limits(&self, id: &str) -> Result { + let url = self + .config + .admin() + .base_url + .join(&format!("endpoints/{}/rate-limits", id))?; + let resp = self + .config + .http_client() + .get(url) + .send() + .await + .map_err(SdkError::Http)?; + + let status = resp.status(); + let body = resp.text().await.map_err(SdkError::Http)?; + + if !status.is_success() { + return Err(SdkError::Api { status, body }); + } + serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body }) + } + + /// Deletes a user-set rate-limit override by its UUID. Plan defaults are + /// not deletable — passing a UUID that does not match a user-set override + /// on the endpoint returns 404. + pub async fn delete_rate_limit_override( + &self, + id: &str, + override_id: &str, + ) -> Result<(), SdkError> { + let url = self + .config + .admin() + .base_url + .join(&format!("endpoints/{}/rate-limits/{}", id, override_id))?; + let resp = self + .config + .http_client() + .delete(url) + .send() + .await + .map_err(SdkError::Http)?; + + let status = resp.status(); + let body = resp.text().await.map_err(SdkError::Http)?; + + if !status.is_success() { + return Err(SdkError::Api { status, body }); + } + Ok(()) + } + + /// Returns the HTTP and WebSocket URLs for the endpoint without fetching + /// the full endpoint record. For multichain endpoints, `multichain_urls` + /// is a per-network map of additional URLs; for single-chain endpoints it + /// is `None`. + pub async fn get_endpoint_urls(&self, id: &str) -> Result { + let url = self + .config + .admin() + .base_url + .join(&format!("endpoints/{}/urls", id))?; + let resp = self + .config + .http_client() + .get(url) + .send() + .await + .map_err(SdkError::Http)?; + + let status = resp.status(); + let body = resp.text().await.map_err(SdkError::Http)?; + + if !status.is_success() { + return Err(SdkError::Api { status, body }); + } + serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body }) + } + /// Returns time-series metrics for a specific endpoint. Requires a /// `period` (`hour`, `day`, `week`, or `month`) and a metric type such as /// `method_calls_over_time` or `response_status_breakdown`. @@ -2611,7 +2700,7 @@ mod tests { async fn update_rate_limits_success() { let server = MockServer::start().await; - Mock::given(method("PUT")) + Mock::given(method("PATCH")) .and(path("/endpoints/ep123/rate-limits")) .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({}))) .mount(&server) @@ -2631,6 +2720,146 @@ mod tests { .unwrap(); } + #[tokio::test] + async fn get_rate_limits_success() { + let server = MockServer::start().await; + + Mock::given(method("GET")) + .and(path("/endpoints/ep123/rate-limits")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "data": { + "rate_limits": [ + {"bucket": "rps", "value": 100, "source": "plan_default", "override_id": null}, + {"bucket": "rpm", "value": 6000, "source": "user_override", "override_id": "ovr-1"} + ] + }, + "error": null + }))) + .mount(&server) + .await; + + let sdk = make_sdk(format!("{}/", server.uri())); + let resp = sdk.admin.get_rate_limits("ep123").await.unwrap(); + let rows = resp.data.unwrap().rate_limits; + assert_eq!(rows.len(), 2); + assert_eq!(rows[1].source, "user_override"); + assert_eq!(rows[1].override_id.as_deref(), Some("ovr-1")); + } + + #[tokio::test] + async fn get_rate_limits_api_error() { + let server = MockServer::start().await; + + Mock::given(method("GET")) + .and(path("/endpoints/missing/rate-limits")) + .respond_with(ResponseTemplate::new(404).set_body_string("not found")) + .mount(&server) + .await; + + let sdk = make_sdk(format!("{}/", server.uri())); + let err = sdk.admin.get_rate_limits("missing").await.unwrap_err(); + match err { + SdkError::Api { status, .. } => assert_eq!(status.as_u16(), 404), + other => panic!("expected SdkError::Api, got {:?}", other), + } + } + + #[tokio::test] + async fn delete_rate_limit_override_success() { + let server = MockServer::start().await; + + Mock::given(method("DELETE")) + .and(path("/endpoints/ep123/rate-limits/ovr-1")) + .respond_with(ResponseTemplate::new(200).set_body_string("")) + .mount(&server) + .await; + + let sdk = make_sdk(format!("{}/", server.uri())); + sdk.admin + .delete_rate_limit_override("ep123", "ovr-1") + .await + .unwrap(); + } + + #[tokio::test] + async fn delete_rate_limit_override_not_found() { + let server = MockServer::start().await; + + Mock::given(method("DELETE")) + .and(path("/endpoints/ep123/rate-limits/bogus")) + .respond_with(ResponseTemplate::new(404).set_body_string("override not found")) + .mount(&server) + .await; + + let sdk = make_sdk(format!("{}/", server.uri())); + let err = sdk + .admin + .delete_rate_limit_override("ep123", "bogus") + .await + .unwrap_err(); + match err { + SdkError::Api { status, .. } => assert_eq!(status.as_u16(), 404), + other => panic!("expected SdkError::Api, got {:?}", other), + } + } + + #[tokio::test] + async fn get_endpoint_urls_success() { + let server = MockServer::start().await; + + Mock::given(method("GET")) + .and(path("/endpoints/ep123/urls")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "data": { + "http_url": "https://example.quiknode.pro/abc/", + "wss_url": "wss://example.quiknode.pro/abc/", + "multichain_urls": null + }, + "error": null + }))) + .mount(&server) + .await; + + let sdk = make_sdk(format!("{}/", server.uri())); + let resp = sdk.admin.get_endpoint_urls("ep123").await.unwrap(); + let data = resp.data.unwrap(); + assert_eq!(data.http_url, "https://example.quiknode.pro/abc/"); + assert!(data.multichain_urls.is_none()); + } + + #[tokio::test] + async fn get_endpoint_urls_multichain() { + let server = MockServer::start().await; + + Mock::given(method("GET")) + .and(path("/endpoints/ep123/urls")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "data": { + "http_url": "https://example.quiknode.pro/abc/", + "wss_url": null, + "multichain_urls": { + "ethereum-mainnet": { + "http_url": "https://example.quiknode.pro/abc/eth/", + "wss_url": "wss://example.quiknode.pro/abc/eth/" + } + } + }, + "error": null + }))) + .mount(&server) + .await; + + let sdk = make_sdk(format!("{}/", server.uri())); + let resp = sdk.admin.get_endpoint_urls("ep123").await.unwrap(); + let data = resp.data.unwrap(); + let mc = data.multichain_urls.unwrap(); + assert_eq!(mc.len(), 1); + assert_eq!( + mc.get("ethereum-mainnet").unwrap().http_url, + "https://example.quiknode.pro/abc/eth/" + ); + } + #[tokio::test] async fn get_endpoint_metrics_success() { let server = MockServer::start().await; diff --git a/crates/node/src/lib.rs b/crates/node/src/lib.rs index 7d282a3..385faf3 100644 --- a/crates/node/src/lib.rs +++ b/crates/node/src/lib.rs @@ -591,9 +591,11 @@ impl AdminApiClient { .map_err(errors::map_sdk_err) } - /// Updates the overall rate limits on an endpoint. Accepts `rps` - /// (requests per second), `rpm` (requests per minute), and `rpd` (requests - /// per day). + /// Partial update of the endpoint-level rate-limit overrides. Accepts + /// `rps` (requests per second), `rpm` (requests per minute), and `rpd` + /// (requests per day). Only buckets included are modified — omitted + /// buckets are left unchanged. Values are capped by the account's plan + /// tier. #[napi] pub async fn update_rate_limits( &self, @@ -606,6 +608,43 @@ impl AdminApiClient { .map_err(errors::map_sdk_err) } + /// Returns the endpoint-level rate limits currently enforced, with each + /// row identifying its bucket (`rps`/`rpm`/`rpd`), value, and source + /// (`plan_default` or `user_override`). User-set overrides expose an + /// `overrideId` that can be passed to `deleteRateLimitOverride`. + #[napi] + pub async fn get_rate_limits(&self, id: String) -> Result { + self.inner + .get_rate_limits(&id) + .await + .map_err(errors::map_sdk_err) + } + + /// Deletes a user-set rate-limit override by its UUID. Plan defaults are + /// not deletable. + #[napi] + pub async fn delete_rate_limit_override(&self, id: String, override_id: String) -> Result<()> { + self.inner + .delete_rate_limit_override(&id, &override_id) + .await + .map_err(errors::map_sdk_err) + } + + /// Returns the HTTP and WebSocket URLs for the endpoint without fetching + /// the full endpoint record. For multichain endpoints, `multichainUrls` + /// is a per-network mapping of additional URLs; for single-chain endpoints + /// it is `null`. + #[napi] + pub async fn get_endpoint_urls( + &self, + id: String, + ) -> Result { + self.inner + .get_endpoint_urls(&id) + .await + .map_err(errors::map_sdk_err) + } + /// Returns time-series metrics for a specific endpoint. Requires a /// `period` (`hour`, `day`, `week`, or `month`) and a metric type such as /// `method_calls_over_time` or `response_status_breakdown`. diff --git a/crates/python/src/lib.rs b/crates/python/src/lib.rs index ae0da52..421ea4b 100644 --- a/crates/python/src/lib.rs +++ b/crates/python/src/lib.rs @@ -900,9 +900,10 @@ impl AdminApiClient { }) } - /// Updates the overall rate limits on an endpoint. Accepts `rps` - /// (requests per second), `rpm` (requests per minute), and `rpd` (requests - /// per day). + /// Partial update of the endpoint-level rate-limit overrides. Accepts + /// `rps` (requests per second), `rpm` (requests per minute), and `rpd` + /// (requests per day). Only buckets passed are modified — omitted buckets + /// are left unchanged. Values are capped by the account's plan tier. #[pyo3(signature = (id, rps=None, rpm=None, rpd=None))] #[gen_stub(override_return_type(type_repr = "typing.Coroutine[typing.Any, typing.Any, None]"))] fn update_rate_limits<'py>( @@ -925,6 +926,58 @@ impl AdminApiClient { }) } + /// Returns the endpoint-level rate limits currently enforced, with each + /// row identifying its bucket (`rps`/`rpm`/`rpd`), value, and source + /// (`plan_default` or `user_override`). User-set overrides expose an + /// `override_id` that can be passed to `delete_rate_limit_override`. + #[gen_stub(override_return_type( + type_repr = "typing.Coroutine[typing.Any, typing.Any, GetRateLimitsResponse]" + ))] + fn get_rate_limits<'py>(&self, py: Python<'py>, id: String) -> PyResult> { + let client = self.inner.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + client + .get_rate_limits(&id) + .await + .map_err(errors::map_sdk_err) + }) + } + + /// Deletes a user-set rate-limit override by its UUID. Plan defaults are + /// not deletable. + #[gen_stub(override_return_type(type_repr = "typing.Coroutine[typing.Any, typing.Any, None]"))] + fn delete_rate_limit_override<'py>( + &self, + py: Python<'py>, + id: String, + override_id: String, + ) -> PyResult> { + let client = self.inner.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + client + .delete_rate_limit_override(&id, &override_id) + .await + .map_err(errors::map_sdk_err) + }) + } + + /// Returns the HTTP and WebSocket URLs for the endpoint without fetching + /// the full endpoint record. For multichain endpoints, `multichain_urls` + /// is a per-network mapping of additional URLs; for single-chain endpoints + /// it is `None`. + #[gen_stub(override_return_type( + type_repr = "typing.Coroutine[typing.Any, typing.Any, GetEndpointUrlsResponse]" + ))] + fn get_endpoint_urls<'py>(&self, py: Python<'py>, id: String) -> PyResult> { + let client = self.inner.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + client + .get_endpoint_urls(&id) + .await + .map_err(errors::map_sdk_err) + }) + } + /// Returns time-series metrics for a specific endpoint. Requires a /// `period` (`hour`, `day`, `week`, or `month`) and a metric type such as /// `method_calls_over_time` or `response_status_breakdown`. @@ -2350,6 +2403,12 @@ fn _core(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/crates/ruby/src/lib.rs b/crates/ruby/src/lib.rs index 4b49dac..63259ef 100644 --- a/crates/ruby/src/lib.rs +++ b/crates/ruby/src/lib.rs @@ -802,6 +802,36 @@ impl AdminApiClient { .map_err(map_err) } + fn get_rate_limits(&self, opts: RHash) -> Result { + validate_keys(&opts, &["id"])?; + let client = self.inner.clone(); + let id = hash_require_string(&opts, "id")?; + runtime() + .block_on(client.get_rate_limits(&id)) + .map_err(map_err) + .and_then(to_ruby) + } + + fn delete_rate_limit_override(&self, opts: RHash) -> Result<(), Error> { + validate_keys(&opts, &["id", "override_id"])?; + let client = self.inner.clone(); + let id = hash_require_string(&opts, "id")?; + let override_id = hash_require_string(&opts, "override_id")?; + runtime() + .block_on(client.delete_rate_limit_override(&id, &override_id)) + .map_err(map_err) + } + + fn get_endpoint_urls(&self, opts: RHash) -> Result { + validate_keys(&opts, &["id"])?; + let client = self.inner.clone(); + let id = hash_require_string(&opts, "id")?; + runtime() + .block_on(client.get_endpoint_urls(&id)) + .map_err(map_err) + .and_then(to_ruby) + } + fn get_endpoint_metrics(&self, opts: RHash) -> Result { validate_keys(&opts, &["id", "period", "metric"])?; let client = self.inner.clone(); @@ -1859,6 +1889,18 @@ fn init(ruby: &Ruby) -> Result<(), Error> { "update_rate_limits", method!(AdminApiClient::update_rate_limits, 1), )?; + admin.define_method( + "get_rate_limits", + method!(AdminApiClient::get_rate_limits, 1), + )?; + admin.define_method( + "delete_rate_limit_override", + method!(AdminApiClient::delete_rate_limit_override, 1), + )?; + admin.define_method( + "get_endpoint_urls", + method!(AdminApiClient::get_endpoint_urls, 1), + )?; admin.define_method( "get_endpoint_metrics", method!(AdminApiClient::get_endpoint_metrics, 1), diff --git a/npm/README.md b/npm/README.md index 9b0e694..b50c227 100644 --- a/npm/README.md +++ b/npm/README.md @@ -28,6 +28,7 @@ This is one of four language bindings published from the same Rust core. See the - [IP Custom Headers](#ip-custom-headers) - [Method Rate Limits](#method-rate-limits) - [Endpoint Rate Limits](#endpoint-rate-limits) + - [Endpoint URLs](#endpoint-urls) - [Metrics](#metrics) - [Chains](#chains) - [Billing](#billing) @@ -777,7 +778,7 @@ await qn.admin.deleteMethodRateLimit("ep-123", "rl-1"); ##### `update_rate_limits` / `updateRateLimits` -Updates the endpoint-level RPS / RPM / RPD caps. +Partial update of the endpoint-level RPS / RPM / RPD caps. Only buckets included in the request are modified — omitted buckets are left unchanged. Values are capped by the account's plan tier. Sends `PATCH`. **Parameters**: `id` (endpoint id, required); `rate_limits`: `RateLimitSettings` (`rps`, `rpm`, `rpd`, all optional). @@ -788,6 +789,56 @@ Updates the endpoint-level RPS / RPM / RPD caps. await qn.admin.updateRateLimits("ep-123", { rateLimits: { rps: 100, rpm: 5000 } }); ``` +##### `get_rate_limits` / `getRateLimits` + +Returns the rate-limit rows currently enforced on the endpoint, each identifying its `bucket` (`"rps"` / `"rpm"` / `"rpd"`), `value`, and `source` (`"plan_default"` or `"user_override"`). User-set overrides expose an `override_id` / `overrideId` you can pass to `deleteRateLimitOverride`. + +**Parameters**: `id` (endpoint id, required). + +**Returns**: `GetRateLimitsResponse` with `data.rateLimits: RateLimitEntry[]`. + +```typescript +// Node.js +const resp = await qn.admin.getRateLimits("ep-123"); +for (const row of resp.data.rateLimits) { + console.log(row.bucket, row.value, row.source, row.overrideId); +} +``` + +##### `delete_rate_limit_override` / `deleteRateLimitOverride` + +Deletes a user-set rate-limit override by UUID. Plan defaults are not deletable — passing a UUID that does not match a user-set override on the endpoint returns 404. + +**Parameters**: `id` (endpoint id, required); `override_id` / `overrideId` (UUID returned by `getRateLimits`, required). + +**Returns**: nothing. + +```typescript +// Node.js +await qn.admin.deleteRateLimitOverride("ep-123", "ovr-uuid"); +``` + +#### Endpoint URLs + +##### `get_endpoint_urls` / `getEndpointUrls` + +Returns the HTTP and WebSocket URLs for the endpoint without fetching the full endpoint record. For multichain endpoints, `multichain_urls` / `multichainUrls` is a per-network map of additional URLs; for single-chain endpoints it is `null`. + +**Parameters**: `id` (endpoint id, required). + +**Returns**: `GetEndpointUrlsResponse` with `data.httpUrl`, `data.wssUrl`, and `data.multichainUrls`. + +```typescript +// Node.js +const resp = await qn.admin.getEndpointUrls("ep-123"); +console.log(resp.data.httpUrl); +if (resp.data.multichainUrls) { + for (const [network, urls] of Object.entries(resp.data.multichainUrls)) { + console.log(network, urls.httpUrl); + } +} +``` + #### Metrics ##### `get_endpoint_metrics` / `getEndpointMetrics` diff --git a/npm/examples/admin.ts b/npm/examples/admin.ts index a36f418..22888db 100644 --- a/npm/examples/admin.ts +++ b/npm/examples/admin.ts @@ -20,7 +20,7 @@ async function main() { for (const ep of response.data) { console.log( `${ep.id} | ${ep.name} | ${ep.status} | ${ep.network} | ` + - `dedicated=${ep.isDedicated} flat=${ep.isFlatRate}`, + `dedicated=${ep.isDedicated} flat=${ep.isFlatRate} multichain=${ep.isMultichain}`, ); } @@ -37,8 +37,21 @@ async function main() { console.log(`getAccountMetrics: ${metrics.data.length} series, first tag: ${firstTag}`); if (response.data.length > 0) { - const sec = await qn.admin.getEndpointSecurity(response.data[0].id); + const epId = response.data[0].id; + const sec = await qn.admin.getEndpointSecurity(epId); console.log(`getEndpointSecurity: has_data=${sec.data !== undefined && sec.data !== null}`); + + const urls = await qn.admin.getEndpointUrls(epId); + if (urls.data) { + const mc = urls.data.multichainUrls; + const networks = mc ? Object.keys(mc) : null; + console.log(`getEndpointUrls: http=${urls.data.httpUrl} multichain_networks=${networks}`); + } + + const rl = await qn.admin.getRateLimits(epId); + if (rl.data) { + console.log(`getRateLimits: ${rl.data.rateLimits.length} rows`); + } } // ── Error handling ────────────────────────────────────────────────── @@ -52,6 +65,18 @@ async function main() { console.log(`api error ${e.status}: ${e.body.slice(0, 80)}`); } + // 1b) Rate-limit override delete with a bogus override id — also a 404. + try { + await qn.admin.deleteRateLimitOverride( + "does-not-exist", + "00000000-0000-0000-0000-000000000000", + ); + } catch (e) { + if (!(e instanceof ApiError)) throw e; + console.assert(e.status === 404); + console.log(`deleteRateLimitOverride api error ${e.status}: ${e.body.slice(0, 80)}`); + } + // 2) Timeout path — unreachable base URL + 1s timeout forces a timeout. const blackhole = new QuicknodeSdk({ apiKey: process.env.QN_SDK__API_KEY ?? "", diff --git a/npm/index.d.ts b/npm/index.d.ts index 91db289..cf504c0 100644 --- a/npm/index.d.ts +++ b/npm/index.d.ts @@ -302,6 +302,36 @@ export interface UpdateRateLimitsRequest { /** Rate limit values to apply. */ rateLimits: RateLimitSettings } +/** + * A single rate-limit row returned by `get_rate_limits`, identifying the + * bucket (`rps`/`rpm`/`rpd`), the value enforced, and whether the value comes + * from the plan default or a user-set override. + */ +export interface RateLimitEntry { + /** Which bucket this row applies to: `rps`, `rpm`, or `rpd`. */ + bucket: string + /** The enforced value for this bucket. */ + value: number + /** Where the value comes from: `plan_default` or `user_override`. */ + source: string + /** + * Identifier of the user-set override, present only when `source` is + * `user_override`. Pass this to `delete_rate_limit_override` to remove it. + */ + overrideId?: string +} +/** Inner data for `get_rate_limits`. */ +export interface GetRateLimitsData { + /** One row per enforced bucket. */ + rateLimits: Array +} +/** Response from `get_rate_limits`. */ +export interface GetRateLimitsResponse { + /** Rate-limit rows with their source. */ + data?: GetRateLimitsData + /** Error message when the request did not succeed. */ + error?: string +} /** A single security feature's name, status, and optional value. */ export interface SecurityOption { /** Name of the security feature (e.g. `tokens`, `jwts`, `ips`). */ @@ -424,6 +454,32 @@ export interface DeleteBoolResponse { /** Error message when the request did not succeed. */ error?: string } +/** HTTP/WSS URL pair for a single network on a multichain endpoint. */ +export interface EndpointUrl { + /** HTTP RPC URL. */ + httpUrl: string + /** WebSocket RPC URL, when available. */ + wssUrl?: string +} +/** + * Inner data for `get_endpoint_urls` — the http/wss URLs for the endpoint and, + * when the endpoint is multichain, a per-network map of additional URLs. + */ +export interface GetEndpointUrlsData { + /** HTTP RPC URL. */ + httpUrl: string + /** WebSocket RPC URL, when available. */ + wssUrl?: string + /** Per-network URLs for multichain endpoints; `None` for single-chain endpoints. */ + multichainUrls?: Record +} +/** Response from `get_endpoint_urls`. */ +export interface GetEndpointUrlsResponse { + /** URLs for the endpoint. */ + data?: GetEndpointUrlsData + /** Error message when the request did not succeed. */ + error?: string +} /** Parameters for `get_endpoints`. */ export interface GetEndpointsRequest { /** Maximum number of endpoints returned. */ @@ -493,6 +549,8 @@ export interface Endpoint { wssUrl?: string /** Tags applied to the endpoint. */ tags: Array + /** Whether the endpoint is configured to serve multiple chains/networks. */ + isMultichain: boolean } /** Tag reference as returned on an endpoint. */ export interface EndpointTag { @@ -537,6 +595,8 @@ export interface SingleEndpoint { rateLimits?: EndpointRateLimits /** Tags applied to the endpoint. */ tags: Array + /** Whether the endpoint is configured to serve multiple chains/networks. */ + isMultichain: boolean } /** Rate limits applied to an endpoint. */ export interface EndpointRateLimits { @@ -1886,11 +1946,32 @@ export declare class AdminApiClient { /** Removes a method rate limit from an endpoint by method rate limit id. */ deleteMethodRateLimit(id: string, methodRateLimitId: string): Promise /** - * Updates the overall rate limits on an endpoint. Accepts `rps` - * (requests per second), `rpm` (requests per minute), and `rpd` (requests - * per day). + * Partial update of the endpoint-level rate-limit overrides. Accepts + * `rps` (requests per second), `rpm` (requests per minute), and `rpd` + * (requests per day). Only buckets included are modified — omitted + * buckets are left unchanged. Values are capped by the account's plan + * tier. */ updateRateLimits(id: string, params: UpdateRateLimitsRequest): Promise + /** + * Returns the endpoint-level rate limits currently enforced, with each + * row identifying its bucket (`rps`/`rpm`/`rpd`), value, and source + * (`plan_default` or `user_override`). User-set overrides expose an + * `overrideId` that can be passed to `deleteRateLimitOverride`. + */ + getRateLimits(id: string): Promise + /** + * Deletes a user-set rate-limit override by its UUID. Plan defaults are + * not deletable. + */ + deleteRateLimitOverride(id: string, overrideId: string): Promise + /** + * Returns the HTTP and WebSocket URLs for the endpoint without fetching + * the full endpoint record. For multichain endpoints, `multichainUrls` + * is a per-network mapping of additional URLs; for single-chain endpoints + * it is `null`. + */ + getEndpointUrls(id: string): Promise /** * Returns time-series metrics for a specific endpoint. Requires a * `period` (`hour`, `day`, `week`, or `month`) and a metric type such as diff --git a/npm/sdk.d.ts b/npm/sdk.d.ts index b62a321..4907472 100644 --- a/npm/sdk.d.ts +++ b/npm/sdk.d.ts @@ -138,6 +138,13 @@ export type { UpdateMethodRateLimitResponse, RateLimitSettings, UpdateRateLimitsRequest, + RateLimitEntry, + GetRateLimitsData, + GetRateLimitsResponse, + // endpoint URLs + EndpointUrl, + GetEndpointUrlsData, + GetEndpointUrlsResponse, // security options SecurityOption, GetSecurityOptionsResponse, diff --git a/python/README.md b/python/README.md index c7a62c4..c617eb8 100644 --- a/python/README.md +++ b/python/README.md @@ -28,6 +28,7 @@ This is one of four language bindings published from the same Rust core. See the - [IP Custom Headers](#ip-custom-headers) - [Method Rate Limits](#method-rate-limits) - [Endpoint Rate Limits](#endpoint-rate-limits) + - [Endpoint URLs](#endpoint-urls) - [Metrics](#metrics) - [Chains](#chains) - [Billing](#billing) @@ -783,7 +784,7 @@ await qn.admin.delete_method_rate_limit("ep-123", "rl-1") ##### `update_rate_limits` / `updateRateLimits` -Updates the endpoint-level RPS / RPM / RPD caps. +Partial update of the endpoint-level RPS / RPM / RPD caps. Only buckets included in the request are modified — omitted buckets are left unchanged. Values are capped by the account's plan tier. Sends `PATCH`. **Parameters**: `id` (endpoint id, required); `rate_limits`: `RateLimitSettings` (`rps`, `rpm`, `rpd`, all optional). @@ -794,6 +795,53 @@ Updates the endpoint-level RPS / RPM / RPD caps. await qn.admin.update_rate_limits("ep-123", rps=100, rpm=5000) ``` +##### `get_rate_limits` / `getRateLimits` + +Returns the rate-limit rows currently enforced on the endpoint, each identifying its `bucket` (`"rps"` / `"rpm"` / `"rpd"`), `value`, and `source` (`"plan_default"` or `"user_override"`). User-set overrides expose an `override_id` / `overrideId` you can pass to `delete_rate_limit_override`. + +**Parameters**: `id` (endpoint id, required). + +**Returns**: `GetRateLimitsResponse` with `data.rate_limits: RateLimitEntry[]`. + +```python +# Python +resp = await qn.admin.get_rate_limits("ep-123") +for row in resp.data.rate_limits: + print(row.bucket, row.value, row.source, row.override_id) +``` + +##### `delete_rate_limit_override` / `deleteRateLimitOverride` + +Deletes a user-set rate-limit override by UUID. Plan defaults are not deletable — passing a UUID that does not match a user-set override on the endpoint returns 404. + +**Parameters**: `id` (endpoint id, required); `override_id` / `overrideId` (UUID returned by `get_rate_limits`, required). + +**Returns**: nothing. + +```python +# Python +await qn.admin.delete_rate_limit_override("ep-123", "ovr-uuid") +``` + +#### Endpoint URLs + +##### `get_endpoint_urls` / `getEndpointUrls` + +Returns the HTTP and WebSocket URLs for the endpoint without fetching the full endpoint record. For multichain endpoints, `multichain_urls` / `multichainUrls` is a per-network map of additional URLs; for single-chain endpoints it is `None` / `null`. + +**Parameters**: `id` (endpoint id, required). + +**Returns**: `GetEndpointUrlsResponse` with `data.http_url`, `data.wss_url`, and `data.multichain_urls`. + +```python +# Python +resp = await qn.admin.get_endpoint_urls("ep-123") +print(resp.data.http_url) +if resp.data.multichain_urls: + for network, urls in resp.data.multichain_urls.items(): + print(network, urls.http_url) +``` + #### Metrics ##### `get_endpoint_metrics` / `getEndpointMetrics` diff --git a/python/examples/admin.py b/python/examples/admin.py index 842a108..7a3d72d 100644 --- a/python/examples/admin.py +++ b/python/examples/admin.py @@ -25,7 +25,7 @@ async def main(): for ep in response.data: print( f"{ep.id} | {ep.name} | {ep.status} | {ep.network} | " - f"dedicated={ep.is_dedicated} flat={ep.is_flat_rate}" + f"dedicated={ep.is_dedicated} flat={ep.is_flat_rate} multichain={ep.is_multichain}" ) tags = await qn.admin.list_tags() @@ -39,9 +39,22 @@ async def main(): print(f"get_account_metrics: {len(metrics.data)} series, first tag: {first_tag}") if response.data: - sec = await qn.admin.get_endpoint_security(response.data[0].id) + ep_id = response.data[0].id + sec = await qn.admin.get_endpoint_security(ep_id) print(f"get_endpoint_security: has_data={sec.data is not None}") + urls = await qn.admin.get_endpoint_urls(ep_id) + if urls.data is not None: + mc = urls.data.multichain_urls + print( + f"get_endpoint_urls: http={urls.data.http_url} " + f"multichain_networks={list(mc.keys()) if mc is not None else None}" + ) + + rl = await qn.admin.get_rate_limits(ep_id) + if rl.data is not None: + print(f"get_rate_limits: {len(rl.data.rate_limits)} rows") + # ── Error handling ────────────────────────────────────────────────── # 1) API error path — 404 on a bogus endpoint id. try: @@ -51,6 +64,15 @@ async def main(): assert e.status == 404 print(f"api error {e.status}: {e.body[:80]}") + # 1b) Rate-limit override delete with a bogus override id — also a 404. + try: + await qn.admin.delete_rate_limit_override( + "does-not-exist", "00000000-0000-0000-0000-000000000000" + ) + except ApiError as e: + assert e.status == 404 + print(f"delete_rate_limit_override api error {e.status}: {e.body[:80]}") + # 2) Timeout path — unreachable base URL + 1s timeout forces a timeout. blackhole = QuicknodeSdk( SdkFullConfig( diff --git a/python/sdk/__init__.py b/python/sdk/__init__.py index 9c246b9..16034b3 100644 --- a/python/sdk/__init__.py +++ b/python/sdk/__init__.py @@ -84,6 +84,12 @@ UpdateMethodRateLimitResponse, RateLimitSettings, UpdateRateLimitsRequest, + RateLimitEntry, + GetRateLimitsData, + GetRateLimitsResponse, + EndpointUrl, + GetEndpointUrlsData, + GetEndpointUrlsResponse, GetEndpointMetricsRequest, GetAccountMetricsRequest, EndpointMetric, @@ -287,6 +293,12 @@ "UpdateMethodRateLimitResponse", "RateLimitSettings", "UpdateRateLimitsRequest", + "RateLimitEntry", + "GetRateLimitsData", + "GetRateLimitsResponse", + "EndpointUrl", + "GetEndpointUrlsData", + "GetEndpointUrlsResponse", "GetEndpointMetricsRequest", "GetAccountMetricsRequest", "EndpointMetric", diff --git a/python/sdk/__init__.pyi b/python/sdk/__init__.pyi index c0a3745..872dca6 100644 --- a/python/sdk/__init__.pyi +++ b/python/sdk/__init__.pyi @@ -76,6 +76,12 @@ from sdk._core import ( UpdateMethodRateLimitResponse, RateLimitSettings, UpdateRateLimitsRequest, + RateLimitEntry, + GetRateLimitsData, + GetRateLimitsResponse, + EndpointUrl, + GetEndpointUrlsData, + GetEndpointUrlsResponse, GetEndpointMetricsRequest, GetAccountMetricsRequest, EndpointMetric, @@ -269,6 +275,12 @@ __all__ = [ "UpdateMethodRateLimitResponse", "RateLimitSettings", "UpdateRateLimitsRequest", + "RateLimitEntry", + "GetRateLimitsData", + "GetRateLimitsResponse", + "EndpointUrl", + "GetEndpointUrlsData", + "GetEndpointUrlsResponse", "GetEndpointMetricsRequest", "GetAccountMetricsRequest", "EndpointMetric", diff --git a/python/sdk/_core/__init__.pyi b/python/sdk/_core/__init__.pyi index 4e76384..f76de58 100644 --- a/python/sdk/_core/__init__.pyi +++ b/python/sdk/_core/__init__.pyi @@ -62,6 +62,7 @@ __all__ = [ "EndpointSecurityOptions", "EndpointTag", "EndpointToken", + "EndpointUrl", "EndpointUsage", "EvmAbiFilterArgs", "EvmAbiFilterTemplate", @@ -76,6 +77,8 @@ __all__ = [ "GetEndpointMetricsRequest", "GetEndpointMetricsResponse", "GetEndpointSecurityResponse", + "GetEndpointUrlsData", + "GetEndpointUrlsResponse", "GetEndpointsRequest", "GetEndpointsResponse", "GetListData", @@ -85,6 +88,8 @@ __all__ = [ "GetLogDetailsResponse", "GetMethodRateLimitsData", "GetMethodRateLimitsResponse", + "GetRateLimitsData", + "GetRateLimitsResponse", "GetSecurityOptionsResponse", "GetSetResponse", "GetSetsResponse", @@ -128,6 +133,7 @@ __all__ = [ "Payment", "PostgresAttributes", "QuicknodeSdk", + "RateLimitEntry", "RateLimitSettings", "RemoveTeamMemberRequest", "RemoveTeamMemberResponse", @@ -458,9 +464,29 @@ class AdminApiClient: """ def update_rate_limits(self, id: builtins.str, rps: typing.Optional[builtins.int] = None, rpm: typing.Optional[builtins.int] = None, rpd: typing.Optional[builtins.int] = None) -> typing.Coroutine[typing.Any, typing.Any, None]: r""" - Updates the overall rate limits on an endpoint. Accepts `rps` - (requests per second), `rpm` (requests per minute), and `rpd` (requests - per day). + Partial update of the endpoint-level rate-limit overrides. Accepts + `rps` (requests per second), `rpm` (requests per minute), and `rpd` + (requests per day). Only buckets passed are modified — omitted buckets + are left unchanged. Values are capped by the account's plan tier. + """ + def get_rate_limits(self, id: builtins.str) -> typing.Coroutine[typing.Any, typing.Any, GetRateLimitsResponse]: + r""" + Returns the endpoint-level rate limits currently enforced, with each + row identifying its bucket (`rps`/`rpm`/`rpd`), value, and source + (`plan_default` or `user_override`). User-set overrides expose an + `override_id` that can be passed to `delete_rate_limit_override`. + """ + def delete_rate_limit_override(self, id: builtins.str, override_id: builtins.str) -> typing.Coroutine[typing.Any, typing.Any, None]: + r""" + Deletes a user-set rate-limit override by its UUID. Plan defaults are + not deletable. + """ + def get_endpoint_urls(self, id: builtins.str) -> typing.Coroutine[typing.Any, typing.Any, GetEndpointUrlsResponse]: + r""" + Returns the HTTP and WebSocket URLs for the endpoint without fetching + the full endpoint record. For multichain endpoints, `multichain_urls` + is a per-network mapping of additional URLs; for single-chain endpoints + it is `None`. """ def get_endpoint_metrics(self, id: builtins.str, period: builtins.str, metric: builtins.str) -> typing.Coroutine[typing.Any, typing.Any, GetEndpointMetricsResponse]: r""" @@ -1797,6 +1823,16 @@ class Endpoint: r""" Tags applied to the endpoint. """ + @property + def is_multichain(self) -> builtins.bool: + r""" + Whether the endpoint is configured to serve multiple chains/networks. + """ + @is_multichain.setter + def is_multichain(self, value: builtins.bool) -> None: + r""" + Whether the endpoint is configured to serve multiple chains/networks. + """ @typing.final class EndpointDomainMask: @@ -2352,6 +2388,32 @@ class EndpointToken: Token secret. """ +@typing.final +class EndpointUrl: + r""" + HTTP/WSS URL pair for a single network on a multichain endpoint. + """ + @property + def http_url(self) -> builtins.str: + r""" + HTTP RPC URL. + """ + @http_url.setter + def http_url(self, value: builtins.str) -> None: + r""" + HTTP RPC URL. + """ + @property + def wss_url(self) -> typing.Optional[builtins.str]: + r""" + WebSocket RPC URL, when available. + """ + @wss_url.setter + def wss_url(self, value: typing.Optional[builtins.str]) -> None: + r""" + WebSocket RPC URL, when available. + """ + @typing.final class EndpointUsage: r""" @@ -2752,6 +2814,69 @@ class GetEndpointSecurityResponse: Error message when the request did not succeed. """ +@typing.final +class GetEndpointUrlsData: + r""" + Inner data for `get_endpoint_urls` — the http/wss URLs for the endpoint and, + when the endpoint is multichain, a per-network map of additional URLs. + """ + @property + def http_url(self) -> builtins.str: + r""" + HTTP RPC URL. + """ + @http_url.setter + def http_url(self, value: builtins.str) -> None: + r""" + HTTP RPC URL. + """ + @property + def wss_url(self) -> typing.Optional[builtins.str]: + r""" + WebSocket RPC URL, when available. + """ + @wss_url.setter + def wss_url(self, value: typing.Optional[builtins.str]) -> None: + r""" + WebSocket RPC URL, when available. + """ + @property + def multichain_urls(self) -> typing.Optional[builtins.dict[builtins.str, EndpointUrl]]: + r""" + Per-network URLs for multichain endpoints; `None` for single-chain endpoints. + """ + @multichain_urls.setter + def multichain_urls(self, value: typing.Optional[builtins.dict[builtins.str, EndpointUrl]]) -> None: + r""" + Per-network URLs for multichain endpoints; `None` for single-chain endpoints. + """ + +@typing.final +class GetEndpointUrlsResponse: + r""" + Response from `get_endpoint_urls`. + """ + @property + def data(self) -> typing.Optional[GetEndpointUrlsData]: + r""" + URLs for the endpoint. + """ + @data.setter + def data(self, value: typing.Optional[GetEndpointUrlsData]) -> None: + r""" + URLs for the endpoint. + """ + @property + def error(self) -> typing.Optional[builtins.str]: + r""" + Error message when the request did not succeed. + """ + @error.setter + def error(self, value: typing.Optional[builtins.str]) -> None: + r""" + Error message when the request did not succeed. + """ + @typing.final class GetEndpointsRequest: r""" @@ -3060,6 +3185,48 @@ class GetMethodRateLimitsResponse: Error message when the request did not succeed. """ +@typing.final +class GetRateLimitsData: + r""" + Inner data for `get_rate_limits`. + """ + @property + def rate_limits(self) -> builtins.list[RateLimitEntry]: + r""" + One row per enforced bucket. + """ + @rate_limits.setter + def rate_limits(self, value: builtins.list[RateLimitEntry]) -> None: + r""" + One row per enforced bucket. + """ + +@typing.final +class GetRateLimitsResponse: + r""" + Response from `get_rate_limits`. + """ + @property + def data(self) -> typing.Optional[GetRateLimitsData]: + r""" + Rate-limit rows with their source. + """ + @data.setter + def data(self, value: typing.Optional[GetRateLimitsData]) -> None: + r""" + Rate-limit rows with their source. + """ + @property + def error(self) -> typing.Optional[builtins.str]: + r""" + Error message when the request did not succeed. + """ + @error.setter + def error(self, value: typing.Optional[builtins.str]) -> None: + r""" + Error message when the request did not succeed. + """ + @typing.final class GetSecurityOptionsResponse: r""" @@ -4474,6 +4641,56 @@ class QuicknodeSdk: Creates a new SDK instance using configuration from environment variables. """ +@typing.final +class RateLimitEntry: + r""" + A single rate-limit row returned by `get_rate_limits`, identifying the + bucket (`rps`/`rpm`/`rpd`), the value enforced, and whether the value comes + from the plan default or a user-set override. + """ + @property + def bucket(self) -> builtins.str: + r""" + Which bucket this row applies to: `rps`, `rpm`, or `rpd`. + """ + @bucket.setter + def bucket(self, value: builtins.str) -> None: + r""" + Which bucket this row applies to: `rps`, `rpm`, or `rpd`. + """ + @property + def value(self) -> builtins.int: + r""" + The enforced value for this bucket. + """ + @value.setter + def value(self, value: builtins.int) -> None: + r""" + The enforced value for this bucket. + """ + @property + def source(self) -> builtins.str: + r""" + Where the value comes from: `plan_default` or `user_override`. + """ + @source.setter + def source(self, value: builtins.str) -> None: + r""" + Where the value comes from: `plan_default` or `user_override`. + """ + @property + def override_id(self) -> typing.Optional[builtins.str]: + r""" + Identifier of the user-set override, present only when `source` is + `user_override`. Pass this to `delete_rate_limit_override` to remove it. + """ + @override_id.setter + def override_id(self, value: typing.Optional[builtins.str]) -> None: + r""" + Identifier of the user-set override, present only when `source` is + `user_override`. Pass this to `delete_rate_limit_override` to remove it. + """ + @typing.final class RateLimitSettings: r""" @@ -5019,6 +5236,16 @@ class SingleEndpoint: r""" Tags applied to the endpoint. """ + @property + def is_multichain(self) -> builtins.bool: + r""" + Whether the endpoint is configured to serve multiple chains/networks. + """ + @is_multichain.setter + def is_multichain(self, value: builtins.bool) -> None: + r""" + Whether the endpoint is configured to serve multiple chains/networks. + """ @typing.final class SolanaWalletFilterArgs: diff --git a/python/sdk/init_manual_override.pyi b/python/sdk/init_manual_override.pyi index c0a3745..872dca6 100644 --- a/python/sdk/init_manual_override.pyi +++ b/python/sdk/init_manual_override.pyi @@ -76,6 +76,12 @@ from sdk._core import ( UpdateMethodRateLimitResponse, RateLimitSettings, UpdateRateLimitsRequest, + RateLimitEntry, + GetRateLimitsData, + GetRateLimitsResponse, + EndpointUrl, + GetEndpointUrlsData, + GetEndpointUrlsResponse, GetEndpointMetricsRequest, GetAccountMetricsRequest, EndpointMetric, @@ -269,6 +275,12 @@ __all__ = [ "UpdateMethodRateLimitResponse", "RateLimitSettings", "UpdateRateLimitsRequest", + "RateLimitEntry", + "GetRateLimitsData", + "GetRateLimitsResponse", + "EndpointUrl", + "GetEndpointUrlsData", + "GetEndpointUrlsResponse", "GetEndpointMetricsRequest", "GetAccountMetricsRequest", "EndpointMetric", diff --git a/ruby/README.md b/ruby/README.md index bb5c938..16c6d00 100644 --- a/ruby/README.md +++ b/ruby/README.md @@ -28,6 +28,7 @@ This is one of four language bindings published from the same Rust core. See the - [IP Custom Headers](#ip-custom-headers) - [Method Rate Limits](#method-rate-limits) - [Endpoint Rate Limits](#endpoint-rate-limits) + - [Endpoint URLs](#endpoint-urls) - [Metrics](#metrics) - [Chains](#chains) - [Billing](#billing) @@ -772,7 +773,7 @@ qn.admin.delete_method_rate_limit(id: "ep-123", method_rate_limit_id: "rl-1") ##### `update_rate_limits` / `updateRateLimits` -Updates the endpoint-level RPS / RPM / RPD caps. +Partial update of the endpoint-level RPS / RPM / RPD caps. Only buckets included in the request are modified — omitted buckets are left unchanged. Values are capped by the account's plan tier. Sends `PATCH`. **Parameters**: `id` (endpoint id, required); `rate_limits`: `RateLimitSettings` (`rps`, `rpm`, `rpd`, all optional). @@ -783,6 +784,54 @@ Updates the endpoint-level RPS / RPM / RPD caps. qn.admin.update_rate_limits(id: "ep-123", rps: 100, rpm: 5000) ``` +##### `get_rate_limits` / `getRateLimits` + +Returns the rate-limit rows currently enforced on the endpoint, each identifying its `bucket` (`"rps"` / `"rpm"` / `"rpd"`), `value`, and `source` (`"plan_default"` or `"user_override"`). User-set overrides expose an `override_id` you can pass to `delete_rate_limit_override`. + +**Parameters**: `id` (endpoint id, required). + +**Returns**: `GetRateLimitsResponse` (as a `Hashie::Mash`) with `data.rate_limits: Array`. + +```ruby +# Ruby +resp = qn.admin.get_rate_limits(id: "ep-123") +resp.data.rate_limits.each do |row| + puts "#{row.bucket} #{row.value} #{row.source} #{row.override_id}" +end +``` + +##### `delete_rate_limit_override` / `deleteRateLimitOverride` + +Deletes a user-set rate-limit override by UUID. Plan defaults are not deletable — passing a UUID that does not match a user-set override on the endpoint returns 404. + +**Parameters**: `id` (endpoint id, required); `override_id` (UUID returned by `get_rate_limits`, required). + +**Returns**: nothing. + +```ruby +# Ruby +qn.admin.delete_rate_limit_override(id: "ep-123", override_id: "ovr-uuid") +``` + +#### Endpoint URLs + +##### `get_endpoint_urls` / `getEndpointUrls` + +Returns the HTTP and WebSocket URLs for the endpoint without fetching the full endpoint record. For multichain endpoints, `multichain_urls` is a per-network map of additional URLs; for single-chain endpoints it is `nil`. + +**Parameters**: `id` (endpoint id, required). + +**Returns**: `GetEndpointUrlsResponse` (as a `Hashie::Mash`) with `data.http_url`, `data.wss_url`, and `data.multichain_urls`. + +```ruby +# Ruby +resp = qn.admin.get_endpoint_urls(id: "ep-123") +puts resp.data.http_url +resp.data.multichain_urls&.each do |network, urls| + puts "#{network} #{urls.http_url}" +end +``` + #### Metrics ##### `get_endpoint_metrics` / `getEndpointMetrics` diff --git a/ruby/examples/admin.rb b/ruby/examples/admin.rb index de94878..da502a0 100644 --- a/ruby/examples/admin.rb +++ b/ruby/examples/admin.rb @@ -15,7 +15,7 @@ response[:data].each do |ep| puts "#{ep[:id]} | #{ep[:name]} | #{ep[:status]} | #{ep[:network]} | " \ - "dedicated=#{ep[:is_dedicated]} flat=#{ep[:is_flat_rate]}" + "dedicated=#{ep[:is_dedicated]} flat=#{ep[:is_flat_rate]} multichain=#{ep[:is_multichain]}" end tags = qn.admin.list_tags @@ -25,4 +25,15 @@ if first sec = qn.admin.get_endpoint_security(id: first[:id]) puts "get_endpoint_security: has_data=#{!sec[:data].nil?}" + + urls = qn.admin.get_endpoint_urls(id: first[:id]) + if urls[:data] + mc = urls.dig(:data, :multichain_urls) + puts "get_endpoint_urls: http=#{urls.dig(:data, :http_url)} multichain_networks=#{mc&.keys}" + end + + rl = qn.admin.get_rate_limits(id: first[:id]) + if rl[:data] + puts "get_rate_limits: #{rl.dig(:data, :rate_limits)&.length || 0} rows" + end end diff --git a/ruby/examples/admin_e2e.rb b/ruby/examples/admin_e2e.rb index 722fc74..31159ae 100644 --- a/ruby/examples/admin_e2e.rb +++ b/ruby/examples/admin_e2e.rb @@ -243,6 +243,12 @@ #qn.admin.update_rate_limits(id: endpoint_id, rps: 3) #puts "update_rate_limits: ok" +resp = qn.admin.get_rate_limits(id: endpoint_id) +puts "get_rate_limits: #{resp[:data]}" + +resp = qn.admin.get_endpoint_urls(id: endpoint_id) +puts "get_endpoint_urls: http=#{resp.dig(:data, :http_url)} multichain_networks=#{resp.dig(:data, :multichain_urls)&.keys}" + resp = qn.admin.get_method_rate_limits(id: endpoint_id) puts "get_method_rate_limits: #{resp[:data]}" @@ -353,6 +359,18 @@ puts "api error #{e.status}: #{e.body[0, 80]}" end +# 1b) Rate-limit override delete with a bogus override id — also a 404. +begin + qn.admin.delete_rate_limit_override( + id: "does-not-exist", + override_id: "00000000-0000-0000-0000-000000000000" + ) + raise "expected 404" +rescue QuicknodeSdk::ApiError => e + raise "expected 404, got #{e.status}" unless e.status == 404 + puts "delete_rate_limit_override api error #{e.status}: #{e.body[0, 80]}" +end + # 2) Timeout path — unreachable base URL + 1s timeout forces a timeout prev_url = ENV["QN_SDK__ADMIN__BASE_URL"] prev_timeout = ENV["QN_SDK__HTTP__TIMEOUT_SECS"] diff --git a/ruby/sig/quicknode_sdk.rbs b/ruby/sig/quicknode_sdk.rbs index 15a6f77..8ea9d6a 100644 --- a/ruby/sig/quicknode_sdk.rbs +++ b/ruby/sig/quicknode_sdk.rbs @@ -83,6 +83,9 @@ module QuicknodeSdk def update_method_rate_limit: (id: String, method_rate_limit_id: String, ?methods: Array[String], ?status: String, ?rate: Integer) -> untyped def delete_method_rate_limit: (id: String, method_rate_limit_id: String) -> void def update_rate_limits: (id: String, ?rps: Integer, ?rpm: Integer, ?rpd: Integer) -> void + def get_rate_limits: (id: String) -> untyped + def delete_rate_limit_override: (id: String, override_id: String) -> void + def get_endpoint_urls: (id: String) -> untyped def get_endpoint_metrics: (id: String, period: String, metric: String) -> untyped def get_account_metrics: (period: String, metric: String, ?percentile: String) -> untyped def list_chains: () -> untyped From fb20dca1c8dcc21d0b8e7108428d4c4740d303b0 Mon Sep 17 00:00:00 2001 From: John Mitsch Date: Thu, 21 May 2026 10:03:03 -0400 Subject: [PATCH 2/3] e2e updates --- crates/core/README.md | 4 ++-- crates/core/examples/admin_e2e.rs | 7 +++++- crates/core/src/admin/endpoint_rate_limits.rs | 10 ++++---- crates/core/src/admin/mod.rs | 9 ++++--- npm/README.md | 4 ++-- npm/examples/admin.ts | 24 ++++++++++++++++--- npm/index.d.ts | 9 +++---- python/README.md | 4 ++-- python/examples/admin.py | 21 +++++++++++++--- python/sdk/_core/__init__.pyi | 22 +++++++++-------- ruby/README.md | 4 ++-- ruby/examples/admin.rb | 4 ++-- ruby/examples/admin_e2e.rb | 9 ++++--- 13 files changed, 90 insertions(+), 41 deletions(-) diff --git a/crates/core/README.md b/crates/core/README.md index cc722d5..1565755 100644 --- a/crates/core/README.md +++ b/crates/core/README.md @@ -831,7 +831,7 @@ qn.admin.update_rate_limits("ep-123", ¶ms).await?; ##### `get_rate_limits` / `getRateLimits` -Returns the rate-limit rows currently enforced on the endpoint, each identifying its `bucket` (`"rps"` / `"rpm"` / `"rpd"`), `value`, and `source` (`"plan_default"` or `"user_override"`). User-set overrides expose an `override_id` you can pass to `delete_rate_limit_override`. +Returns the rate-limit rows currently enforced on the endpoint, each identifying its `bucket` (`"rps"` / `"rpm"` / `"rpd"`), `rate_limit`, and `source` (`"plan_default"` or `"user_override"`). User-set overrides expose an `id` you can pass to `delete_rate_limit_override`. **Parameters**: `id` (endpoint id, required). @@ -841,7 +841,7 @@ Returns the rate-limit rows currently enforced on the endpoint, each identifying // Rust let resp = qn.admin.get_rate_limits("ep-123").await?; for row in resp.data.unwrap().rate_limits { - println!("{} {} {} {:?}", row.bucket, row.value, row.source, row.override_id); + println!("{} {} {} {:?}", row.bucket, row.rate_limit, row.source, row.id); } ``` diff --git a/crates/core/examples/admin_e2e.rs b/crates/core/examples/admin_e2e.rs index 54aaa96..5b406ec 100644 --- a/crates/core/examples/admin_e2e.rs +++ b/crates/core/examples/admin_e2e.rs @@ -554,6 +554,11 @@ async fn main() { // --- Rate limits --- + match qn.admin.get_rate_limits(&endpoint_id).await { + Ok(resp) => println!("get_rate_limits before PATCH: {:?}", resp.data), + Err(e) => eprintln!("get_rate_limits error: {e}"), + } + match qn .admin .update_rate_limits( @@ -572,7 +577,7 @@ async fn main() { } match qn.admin.get_rate_limits(&endpoint_id).await { - Ok(resp) => println!("get_rate_limits: {:?}", resp.data), + Ok(resp) => println!("get_rate_limits after PATCH: {:?}", resp.data), Err(e) => eprintln!("get_rate_limits error: {e}"), } diff --git a/crates/core/src/admin/endpoint_rate_limits.rs b/crates/core/src/admin/endpoint_rate_limits.rs index a4512a5..45c4e3e 100644 --- a/crates/core/src/admin/endpoint_rate_limits.rs +++ b/crates/core/src/admin/endpoint_rate_limits.rs @@ -149,12 +149,14 @@ pub struct RateLimitEntry { /// Which bucket this row applies to: `rps`, `rpm`, or `rpd`. pub bucket: String, /// The enforced value for this bucket. - pub value: i32, + pub rate_limit: i32, /// Where the value comes from: `plan_default` or `user_override`. pub source: String, - /// Identifier of the user-set override, present only when `source` is - /// `user_override`. Pass this to `delete_rate_limit_override` to remove it. - pub override_id: Option, + /// Row identifier. Present on `user_override` rows — pass it to + /// `delete_rate_limit_override` to remove the override. May be absent on + /// `plan_default` rows and cannot be deleted there. + #[serde(default)] + pub id: Option, } /// Inner data for `get_rate_limits`. diff --git a/crates/core/src/admin/mod.rs b/crates/core/src/admin/mod.rs index b1f487c..bda588f 100644 --- a/crates/core/src/admin/mod.rs +++ b/crates/core/src/admin/mod.rs @@ -2729,8 +2729,8 @@ mod tests { .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ "data": { "rate_limits": [ - {"bucket": "rps", "value": 100, "source": "plan_default", "override_id": null}, - {"bucket": "rpm", "value": 6000, "source": "user_override", "override_id": "ovr-1"} + {"bucket": "rps", "rate_limit": 100, "source": "plan_default"}, + {"bucket": "rpm", "rate_limit": 6000, "source": "user_override", "id": "ovr-1"} ] }, "error": null @@ -2742,8 +2742,11 @@ mod tests { let resp = sdk.admin.get_rate_limits("ep123").await.unwrap(); let rows = resp.data.unwrap().rate_limits; assert_eq!(rows.len(), 2); + assert_eq!(rows[0].source, "plan_default"); + assert!(rows[0].id.is_none()); assert_eq!(rows[1].source, "user_override"); - assert_eq!(rows[1].override_id.as_deref(), Some("ovr-1")); + assert_eq!(rows[1].rate_limit, 6000); + assert_eq!(rows[1].id.as_deref(), Some("ovr-1")); } #[tokio::test] diff --git a/npm/README.md b/npm/README.md index b50c227..d21c87a 100644 --- a/npm/README.md +++ b/npm/README.md @@ -791,7 +791,7 @@ await qn.admin.updateRateLimits("ep-123", { rateLimits: { rps: 100, rpm: 5000 } ##### `get_rate_limits` / `getRateLimits` -Returns the rate-limit rows currently enforced on the endpoint, each identifying its `bucket` (`"rps"` / `"rpm"` / `"rpd"`), `value`, and `source` (`"plan_default"` or `"user_override"`). User-set overrides expose an `override_id` / `overrideId` you can pass to `deleteRateLimitOverride`. +Returns the rate-limit rows currently enforced on the endpoint, each identifying its `bucket` (`"rps"` / `"rpm"` / `"rpd"`), `rateLimit`, and `source` (`"plan_default"` or `"user_override"`). User-set overrides expose an `id` you can pass to `deleteRateLimitOverride`. **Parameters**: `id` (endpoint id, required). @@ -801,7 +801,7 @@ Returns the rate-limit rows currently enforced on the endpoint, each identifying // Node.js const resp = await qn.admin.getRateLimits("ep-123"); for (const row of resp.data.rateLimits) { - console.log(row.bucket, row.value, row.source, row.overrideId); + console.log(row.bucket, row.rateLimit, row.source, row.id); } ``` diff --git a/npm/examples/admin.ts b/npm/examples/admin.ts index 22888db..6fa464a 100644 --- a/npm/examples/admin.ts +++ b/npm/examples/admin.ts @@ -48,9 +48,27 @@ async function main() { console.log(`getEndpointUrls: http=${urls.data.httpUrl} multichain_networks=${networks}`); } - const rl = await qn.admin.getRateLimits(epId); - if (rl.data) { - console.log(`getRateLimits: ${rl.data.rateLimits.length} rows`); + const rlBefore = await qn.admin.getRateLimits(epId); + if (rlBefore.data) { + for (const row of rlBefore.data.rateLimits) { + console.log( + `getRateLimits before PATCH: bucket=${row.bucket} ` + + `rate_limit=${row.rateLimit} source=${row.source} id=${row.id}`, + ); + } + } + + await qn.admin.updateRateLimits(epId, { rateLimits: { rps: 3 } }); + console.log("updateRateLimits: ok"); + + const rlAfter = await qn.admin.getRateLimits(epId); + if (rlAfter.data) { + for (const row of rlAfter.data.rateLimits) { + console.log( + `getRateLimits after PATCH: bucket=${row.bucket} ` + + `rate_limit=${row.rateLimit} source=${row.source} id=${row.id}`, + ); + } } } diff --git a/npm/index.d.ts b/npm/index.d.ts index cf504c0..79e0443 100644 --- a/npm/index.d.ts +++ b/npm/index.d.ts @@ -311,14 +311,15 @@ export interface RateLimitEntry { /** Which bucket this row applies to: `rps`, `rpm`, or `rpd`. */ bucket: string /** The enforced value for this bucket. */ - value: number + rateLimit: number /** Where the value comes from: `plan_default` or `user_override`. */ source: string /** - * Identifier of the user-set override, present only when `source` is - * `user_override`. Pass this to `delete_rate_limit_override` to remove it. + * Row identifier. Present on `user_override` rows — pass it to + * `delete_rate_limit_override` to remove the override. May be absent on + * `plan_default` rows and cannot be deleted there. */ - overrideId?: string + id?: string } /** Inner data for `get_rate_limits`. */ export interface GetRateLimitsData { diff --git a/python/README.md b/python/README.md index c617eb8..6f7950e 100644 --- a/python/README.md +++ b/python/README.md @@ -797,7 +797,7 @@ await qn.admin.update_rate_limits("ep-123", rps=100, rpm=5000) ##### `get_rate_limits` / `getRateLimits` -Returns the rate-limit rows currently enforced on the endpoint, each identifying its `bucket` (`"rps"` / `"rpm"` / `"rpd"`), `value`, and `source` (`"plan_default"` or `"user_override"`). User-set overrides expose an `override_id` / `overrideId` you can pass to `delete_rate_limit_override`. +Returns the rate-limit rows currently enforced on the endpoint, each identifying its `bucket` (`"rps"` / `"rpm"` / `"rpd"`), `rate_limit`, and `source` (`"plan_default"` or `"user_override"`). User-set overrides expose an `id` (camelCased `id` in Node) you can pass to `delete_rate_limit_override`. **Parameters**: `id` (endpoint id, required). @@ -807,7 +807,7 @@ Returns the rate-limit rows currently enforced on the endpoint, each identifying # Python resp = await qn.admin.get_rate_limits("ep-123") for row in resp.data.rate_limits: - print(row.bucket, row.value, row.source, row.override_id) + print(row.bucket, row.rate_limit, row.source, row.id) ``` ##### `delete_rate_limit_override` / `deleteRateLimitOverride` diff --git a/python/examples/admin.py b/python/examples/admin.py index 7a3d72d..63efd9e 100644 --- a/python/examples/admin.py +++ b/python/examples/admin.py @@ -51,9 +51,24 @@ async def main(): f"multichain_networks={list(mc.keys()) if mc is not None else None}" ) - rl = await qn.admin.get_rate_limits(ep_id) - if rl.data is not None: - print(f"get_rate_limits: {len(rl.data.rate_limits)} rows") + rl_before = await qn.admin.get_rate_limits(ep_id) + if rl_before.data is not None: + for row in rl_before.data.rate_limits: + print( + f"get_rate_limits before PATCH: bucket={row.bucket} " + f"rate_limit={row.rate_limit} source={row.source} id={row.id}" + ) + + await qn.admin.update_rate_limits(ep_id, rps=3) + print("update_rate_limits: ok") + + rl_after = await qn.admin.get_rate_limits(ep_id) + if rl_after.data is not None: + for row in rl_after.data.rate_limits: + print( + f"get_rate_limits after PATCH: bucket={row.bucket} " + f"rate_limit={row.rate_limit} source={row.source} id={row.id}" + ) # ── Error handling ────────────────────────────────────────────────── # 1) API error path — 404 on a bogus endpoint id. diff --git a/python/sdk/_core/__init__.pyi b/python/sdk/_core/__init__.pyi index f76de58..b5cb51c 100644 --- a/python/sdk/_core/__init__.pyi +++ b/python/sdk/_core/__init__.pyi @@ -4659,12 +4659,12 @@ class RateLimitEntry: Which bucket this row applies to: `rps`, `rpm`, or `rpd`. """ @property - def value(self) -> builtins.int: + def rate_limit(self) -> builtins.int: r""" The enforced value for this bucket. """ - @value.setter - def value(self, value: builtins.int) -> None: + @rate_limit.setter + def rate_limit(self, value: builtins.int) -> None: r""" The enforced value for this bucket. """ @@ -4679,16 +4679,18 @@ class RateLimitEntry: Where the value comes from: `plan_default` or `user_override`. """ @property - def override_id(self) -> typing.Optional[builtins.str]: + def id(self) -> typing.Optional[builtins.str]: r""" - Identifier of the user-set override, present only when `source` is - `user_override`. Pass this to `delete_rate_limit_override` to remove it. + Row identifier. Present on `user_override` rows — pass it to + `delete_rate_limit_override` to remove the override. May be absent on + `plan_default` rows and cannot be deleted there. """ - @override_id.setter - def override_id(self, value: typing.Optional[builtins.str]) -> None: + @id.setter + def id(self, value: typing.Optional[builtins.str]) -> None: r""" - Identifier of the user-set override, present only when `source` is - `user_override`. Pass this to `delete_rate_limit_override` to remove it. + Row identifier. Present on `user_override` rows — pass it to + `delete_rate_limit_override` to remove the override. May be absent on + `plan_default` rows and cannot be deleted there. """ @typing.final diff --git a/ruby/README.md b/ruby/README.md index 16c6d00..2869e9a 100644 --- a/ruby/README.md +++ b/ruby/README.md @@ -786,7 +786,7 @@ qn.admin.update_rate_limits(id: "ep-123", rps: 100, rpm: 5000) ##### `get_rate_limits` / `getRateLimits` -Returns the rate-limit rows currently enforced on the endpoint, each identifying its `bucket` (`"rps"` / `"rpm"` / `"rpd"`), `value`, and `source` (`"plan_default"` or `"user_override"`). User-set overrides expose an `override_id` you can pass to `delete_rate_limit_override`. +Returns the rate-limit rows currently enforced on the endpoint, each identifying its `bucket` (`"rps"` / `"rpm"` / `"rpd"`), `rate_limit`, and `source` (`"plan_default"` or `"user_override"`). User-set overrides expose an `id` you can pass to `delete_rate_limit_override`. **Parameters**: `id` (endpoint id, required). @@ -796,7 +796,7 @@ Returns the rate-limit rows currently enforced on the endpoint, each identifying # Ruby resp = qn.admin.get_rate_limits(id: "ep-123") resp.data.rate_limits.each do |row| - puts "#{row.bucket} #{row.value} #{row.source} #{row.override_id}" + puts "#{row.bucket} #{row.rate_limit} #{row.source} #{row.id}" end ``` diff --git a/ruby/examples/admin.rb b/ruby/examples/admin.rb index da502a0..deb807b 100644 --- a/ruby/examples/admin.rb +++ b/ruby/examples/admin.rb @@ -33,7 +33,7 @@ end rl = qn.admin.get_rate_limits(id: first[:id]) - if rl[:data] - puts "get_rate_limits: #{rl.dig(:data, :rate_limits)&.length || 0} rows" + rl.dig(:data, :rate_limits)&.each do |row| + puts "get_rate_limits: bucket=#{row[:bucket]} rate_limit=#{row[:rate_limit]} source=#{row[:source]} id=#{row[:id]}" end end diff --git a/ruby/examples/admin_e2e.rb b/ruby/examples/admin_e2e.rb index 31159ae..7db2f57 100644 --- a/ruby/examples/admin_e2e.rb +++ b/ruby/examples/admin_e2e.rb @@ -240,11 +240,14 @@ # ── Rate limits ─────────────────────────────────────────────────────────────── -#qn.admin.update_rate_limits(id: endpoint_id, rps: 3) -#puts "update_rate_limits: ok" +resp = qn.admin.get_rate_limits(id: endpoint_id) +puts "get_rate_limits before PATCH: #{resp[:data]}" + +qn.admin.update_rate_limits(id: endpoint_id, rps: 3) +puts "update_rate_limits: ok" resp = qn.admin.get_rate_limits(id: endpoint_id) -puts "get_rate_limits: #{resp[:data]}" +puts "get_rate_limits after PATCH: #{resp[:data]}" resp = qn.admin.get_endpoint_urls(id: endpoint_id) puts "get_endpoint_urls: http=#{resp.dig(:data, :http_url)} multichain_networks=#{resp.dig(:data, :multichain_urls)&.keys}" From 63e078363d0a3136fdf26a63ec8d9aaceaca4a6b Mon Sep 17 00:00:00 2001 From: John Mitsch Date: Thu, 21 May 2026 10:14:49 -0400 Subject: [PATCH 3/3] Use numeric endpoint id in README examples Endpoint ids are numeric, so replace the placeholder "ep-123" with "123" in the new get_rate_limits / delete_rate_limit_override / get_endpoint_urls snippets across all four per-language READMEs. --- crates/core/README.md | 6 +++--- npm/README.md | 6 +++--- python/README.md | 6 +++--- ruby/README.md | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/crates/core/README.md b/crates/core/README.md index 1565755..7e7d883 100644 --- a/crates/core/README.md +++ b/crates/core/README.md @@ -839,7 +839,7 @@ Returns the rate-limit rows currently enforced on the endpoint, each identifying ```rust // Rust -let resp = qn.admin.get_rate_limits("ep-123").await?; +let resp = qn.admin.get_rate_limits("123").await?; for row in resp.data.unwrap().rate_limits { println!("{} {} {} {:?}", row.bucket, row.rate_limit, row.source, row.id); } @@ -855,7 +855,7 @@ Deletes a user-set rate-limit override by UUID. Plan defaults are not deletable ```rust // Rust -qn.admin.delete_rate_limit_override("ep-123", "ovr-uuid").await?; +qn.admin.delete_rate_limit_override("123", "ovr-uuid").await?; ``` #### Endpoint URLs @@ -870,7 +870,7 @@ Returns the HTTP and WebSocket URLs for the endpoint without fetching the full e ```rust // Rust -let resp = qn.admin.get_endpoint_urls("ep-123").await?; +let resp = qn.admin.get_endpoint_urls("123").await?; if let Some(data) = resp.data { println!("{}", data.http_url); if let Some(mc) = data.multichain_urls { diff --git a/npm/README.md b/npm/README.md index d21c87a..c1474be 100644 --- a/npm/README.md +++ b/npm/README.md @@ -799,7 +799,7 @@ Returns the rate-limit rows currently enforced on the endpoint, each identifying ```typescript // Node.js -const resp = await qn.admin.getRateLimits("ep-123"); +const resp = await qn.admin.getRateLimits("123"); for (const row of resp.data.rateLimits) { console.log(row.bucket, row.rateLimit, row.source, row.id); } @@ -815,7 +815,7 @@ Deletes a user-set rate-limit override by UUID. Plan defaults are not deletable ```typescript // Node.js -await qn.admin.deleteRateLimitOverride("ep-123", "ovr-uuid"); +await qn.admin.deleteRateLimitOverride("123", "ovr-uuid"); ``` #### Endpoint URLs @@ -830,7 +830,7 @@ Returns the HTTP and WebSocket URLs for the endpoint without fetching the full e ```typescript // Node.js -const resp = await qn.admin.getEndpointUrls("ep-123"); +const resp = await qn.admin.getEndpointUrls("123"); console.log(resp.data.httpUrl); if (resp.data.multichainUrls) { for (const [network, urls] of Object.entries(resp.data.multichainUrls)) { diff --git a/python/README.md b/python/README.md index 6f7950e..8caeab2 100644 --- a/python/README.md +++ b/python/README.md @@ -805,7 +805,7 @@ Returns the rate-limit rows currently enforced on the endpoint, each identifying ```python # Python -resp = await qn.admin.get_rate_limits("ep-123") +resp = await qn.admin.get_rate_limits("123") for row in resp.data.rate_limits: print(row.bucket, row.rate_limit, row.source, row.id) ``` @@ -820,7 +820,7 @@ Deletes a user-set rate-limit override by UUID. Plan defaults are not deletable ```python # Python -await qn.admin.delete_rate_limit_override("ep-123", "ovr-uuid") +await qn.admin.delete_rate_limit_override("123", "ovr-uuid") ``` #### Endpoint URLs @@ -835,7 +835,7 @@ Returns the HTTP and WebSocket URLs for the endpoint without fetching the full e ```python # Python -resp = await qn.admin.get_endpoint_urls("ep-123") +resp = await qn.admin.get_endpoint_urls("123") print(resp.data.http_url) if resp.data.multichain_urls: for network, urls in resp.data.multichain_urls.items(): diff --git a/ruby/README.md b/ruby/README.md index 2869e9a..afe0bb2 100644 --- a/ruby/README.md +++ b/ruby/README.md @@ -794,7 +794,7 @@ Returns the rate-limit rows currently enforced on the endpoint, each identifying ```ruby # Ruby -resp = qn.admin.get_rate_limits(id: "ep-123") +resp = qn.admin.get_rate_limits(id: "123") resp.data.rate_limits.each do |row| puts "#{row.bucket} #{row.rate_limit} #{row.source} #{row.id}" end @@ -810,7 +810,7 @@ Deletes a user-set rate-limit override by UUID. Plan defaults are not deletable ```ruby # Ruby -qn.admin.delete_rate_limit_override(id: "ep-123", override_id: "ovr-uuid") +qn.admin.delete_rate_limit_override(id: "123", override_id: "ovr-uuid") ``` #### Endpoint URLs @@ -825,7 +825,7 @@ Returns the HTTP and WebSocket URLs for the endpoint without fetching the full e ```ruby # Ruby -resp = qn.admin.get_endpoint_urls(id: "ep-123") +resp = qn.admin.get_endpoint_urls(id: "123") puts resp.data.http_url resp.data.multichain_urls&.each do |network, urls| puts "#{network} #{urls.http_url}"