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..7e7d883 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"`), `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). + +**Returns**: `GetRateLimitsResponse` with `data.rate_limits: Vec`. + +```rust +// Rust +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); +} +``` + +##### `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("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("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..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( @@ -571,6 +576,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 after PATCH: {:?}", 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 +841,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..45c4e3e 100644 --- a/crates/core/src/admin/endpoint_rate_limits.rs +++ b/crates/core/src/admin/endpoint_rate_limits.rs @@ -137,3 +137,47 @@ 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 rate_limit: i32, + /// Where the value comes from: `plan_default` or `user_override`. + pub source: String, + /// 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`. +#[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..bda588f 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,149 @@ 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", "rate_limit": 100, "source": "plan_default"}, + {"bucket": "rpm", "rate_limit": 6000, "source": "user_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[0].source, "plan_default"); + assert!(rows[0].id.is_none()); + assert_eq!(rows[1].source, "user_override"); + assert_eq!(rows[1].rate_limit, 6000); + assert_eq!(rows[1].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..c1474be 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"`), `rateLimit`, and `source` (`"plan_default"` or `"user_override"`). User-set overrides expose an `id` 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("123"); +for (const row of resp.data.rateLimits) { + console.log(row.bucket, row.rateLimit, row.source, row.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 `getRateLimits`, required). + +**Returns**: nothing. + +```typescript +// Node.js +await qn.admin.deleteRateLimitOverride("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("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..6fa464a 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,39 @@ 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 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}`, + ); + } + } } // ── Error handling ────────────────────────────────────────────────── @@ -52,6 +83,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..79e0443 100644 --- a/npm/index.d.ts +++ b/npm/index.d.ts @@ -302,6 +302,37 @@ 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. */ + rateLimit: number + /** Where the value comes from: `plan_default` or `user_override`. */ + source: string + /** + * 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. + */ + id?: 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 +455,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 +550,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 +596,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 +1947,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..8caeab2 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"`), `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). + +**Returns**: `GetRateLimitsResponse` with `data.rate_limits: RateLimitEntry[]`. + +```python +# Python +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) +``` + +##### `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("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("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..63efd9e 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,37 @@ 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_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. try: @@ -51,6 +79,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..b5cb51c 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,58 @@ 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 rate_limit(self) -> builtins.int: + r""" + The enforced value for this bucket. + """ + @rate_limit.setter + def rate_limit(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 id(self) -> typing.Optional[builtins.str]: + r""" + 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. + """ + @id.setter + def id(self, value: typing.Optional[builtins.str]) -> None: + r""" + 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 class RateLimitSettings: r""" @@ -5019,6 +5238,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..afe0bb2 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"`), `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). + +**Returns**: `GetRateLimitsResponse` (as a `Hashie::Mash`) with `data.rate_limits: Array`. + +```ruby +# Ruby +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 +``` + +##### `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: "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: "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..deb807b 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]) + 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 722fc74..7db2f57 100644 --- a/ruby/examples/admin_e2e.rb +++ b/ruby/examples/admin_e2e.rb @@ -240,8 +240,17 @@ # ── 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 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}" resp = qn.admin.get_method_rate_limits(id: endpoint_id) puts "get_method_rate_limits: #{resp[:data]}" @@ -353,6 +362,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