Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions crates/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -987,6 +987,19 @@ Returns details about the account, including its id, name, creation timestamp, b
let resp = qn.admin.account_info().await?;
```

##### `get_api_credits` / `getApiCredits`

Returns the per-method API credit costs for a chain, identified by its slug (the same slugs returned by `list_chains`, e.g. `ethereum`). An unknown chain slug returns a 404 (surfaced as `ApiError`).

**Parameters**: `chain` (string, required) — the chain slug.

**Returns**: `GetApiCreditsResponse` with `data: Vec<ApiCredit>`, where each `ApiCredit` has `method` and `credits`.

```rust
// Rust
let resp = qn.admin.get_api_credits("ethereum").await?;
```

#### Billing

##### `list_invoices` / `listInvoices`
Expand Down
12 changes: 12 additions & 0 deletions crates/core/examples/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ async fn main() {
Err(e) => eprintln!("account_info error: {e}"),
}

match qn.admin.get_api_credits("ethereum").await {
Ok(resp) => {
if let Some(credits) = &resp.data {
println!("ethereum api credits: {} methods", credits.len());
for c in credits.iter().take(3) {
println!(" {} = {}", c.method, c.credits);
}
}
}
Err(e) => eprintln!("get_api_credits error: {e}"),
}

let params = GetEndpointsRequest::builder()
.limit(20)
.sort_by("created_at".to_string())
Expand Down
32 changes: 32 additions & 0 deletions crates/core/src/admin/api_credits.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#[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};

/// The API credit cost of a single RPC method on a chain.
#[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 ApiCredit {
/// RPC method name (e.g. `eth_chainId`).
pub method: String,
/// Number of API credits the method costs.
pub credits: i64,
}

/// Response from `get_api_credits`.
#[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 GetApiCreditsResponse {
/// Per-method API credit costs for the chain, when the request succeeded.
/// `None` for an unknown chain slug.
pub data: Option<Vec<ApiCredit>>,
/// Error message when the request did not succeed.
pub error: Option<String>,
}
96 changes: 96 additions & 0 deletions crates/core/src/admin/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod account;
pub mod api_credits;
pub mod billing;
pub mod bulk;
pub mod chains;
Expand All @@ -13,6 +14,7 @@ pub mod teams;
pub mod usage;

pub use account::{AccountInfo, AccountInfoResponse, AccountSubscription};
pub use api_credits::{ApiCredit, GetApiCreditsResponse};
pub use billing::{
Invoice, InvoiceLine, ListInvoicesData, ListInvoicesResponse, ListPaymentsData,
ListPaymentsResponse, Payment,
Expand Down Expand Up @@ -1584,6 +1586,34 @@ impl AdminApiClient {
serde_json::from_str(&body).map_err(|source| SdkError::Decode { source, body })
}

/// Returns the per-method API credit costs for a chain, identified by its
/// slug (the same slugs returned by `list_chains`, e.g. `ethereum`). Each
/// item carries the RPC `method` name and its `credits` cost, resolved for
/// the calling account's billing version. An unknown chain slug returns a
/// 404 (surfaced as `SdkError::Api`).
pub async fn get_api_credits(&self, chain: &str) -> Result<GetApiCreditsResponse, SdkError> {
let url = self
.config
.admin()
.base_url
.join(&format!("api-credits/{}", chain))?;
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 the account's invoices, including id, status, billing reason,
/// amounts due and paid, line items with descriptions and billing periods,
/// and creation timestamps.
Expand Down Expand Up @@ -3060,6 +3090,72 @@ mod tests {
assert_eq!(status.as_u16(), 401);
}

#[tokio::test]
async fn get_api_credits_success() {
let server = MockServer::start().await;

Mock::given(method("GET"))
.and(path("/api-credits/ethereum"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"data": [
{"method": "eth_chainId", "credits": 20},
{"method": "eth_sendRawTransaction", "credits": 40}
],
"error": null
})))
.mount(&server)
.await;

let sdk = make_sdk(format!("{}/", server.uri()));
let resp = sdk.admin.get_api_credits("ethereum").await.unwrap();
let data = resp.data.expect("expected credits data");
assert_eq!(data.len(), 2);
assert_eq!(data[0].method, "eth_chainId");
assert_eq!(data[0].credits, 20);
assert_eq!(data[1].method, "eth_sendRawTransaction");
assert_eq!(data[1].credits, 40);
}

#[tokio::test]
async fn get_api_credits_unknown_chain() {
let server = MockServer::start().await;

Mock::given(method("GET"))
.and(path("/api-credits/not-a-chain"))
.respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
"data": null,
"error": "Chain not found"
})))
.mount(&server)
.await;

let sdk = make_sdk(format!("{}/", server.uri()));
let err = sdk.admin.get_api_credits("not-a-chain").await.unwrap_err();
let SdkError::Api { status, body } = err else {
unreachable!("expected SdkError::Api, got {err:?}");
};
assert_eq!(status.as_u16(), 404);
assert!(body.contains("Chain not found"));
}

#[tokio::test]
async fn get_api_credits_api_error() {
let server = MockServer::start().await;

Mock::given(method("GET"))
.and(path("/api-credits/ethereum"))
.respond_with(ResponseTemplate::new(401).set_body_string("Unauthorized"))
.mount(&server)
.await;

let sdk = make_sdk(format!("{}/", server.uri()));
let err = sdk.admin.get_api_credits("ethereum").await.unwrap_err();
let SdkError::Api { status, .. } = err else {
unreachable!("expected SdkError::Api, got {err:?}");
};
assert_eq!(status.as_u16(), 401);
}

#[tokio::test]
async fn list_invoices_success() {
let server = MockServer::start().await;
Expand Down
15 changes: 15 additions & 0 deletions crates/node/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,21 @@ impl AdminApiClient {
self.inner.account_info().await.map_err(errors::map_sdk_err)
}

/// Returns the per-method API credit costs for a chain, identified by its
/// slug (the same slugs returned by `list_chains`, e.g. `ethereum`). Each
/// item carries the RPC `method` name and its `credits` cost. An unknown
/// chain slug rejects with `ApiError` (status 404).
#[napi]
pub async fn get_api_credits(
&self,
chain: String,
) -> Result<core::admin::GetApiCreditsResponse> {
self.inner
.get_api_credits(&chain)
.await
.map_err(errors::map_sdk_err)
}

/// Returns the account's invoices, including id, status, billing reason,
/// amounts due and paid, line items with descriptions and billing periods,
/// and creation timestamps.
Expand Down
17 changes: 17 additions & 0 deletions crates/python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1086,6 +1086,23 @@ impl AdminApiClient {
})
}

/// Returns the per-method API credit costs for a chain, identified by its
/// slug (the same slugs returned by `list_chains`, e.g. `ethereum`). Each
/// item carries the RPC `method` name and its `credits` cost. An unknown
/// chain slug raises `ApiError` with status 404.
#[gen_stub(override_return_type(
type_repr = "typing.Coroutine[typing.Any, typing.Any, GetApiCreditsResponse]"
))]
fn get_api_credits<'py>(&self, py: Python<'py>, chain: String) -> PyResult<Bound<'py, PyAny>> {
let client = self.inner.clone();
pyo3_async_runtimes::tokio::future_into_py(py, async move {
client
.get_api_credits(&chain)
.await
.map_err(errors::map_sdk_err)
})
}

/// Returns the account's invoices, including id, status, billing reason,
/// amounts due and paid, line items with descriptions and billing periods,
/// and creation timestamps.
Expand Down
14 changes: 14 additions & 0 deletions crates/ruby/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -918,6 +918,16 @@ impl AdminApiClient {
.and_then(to_ruby)
}

fn get_api_credits(&self, opts: RHash) -> Result<magnus::Value, Error> {
validate_keys(&opts, &["chain"])?;
let client = self.inner.clone();
let chain = hash_require_string(&opts, "chain")?;
runtime()
.block_on(client.get_api_credits(&chain))
.map_err(map_err)
.and_then(to_ruby)
}

fn list_invoices(&self) -> Result<magnus::Value, Error> {
let client = self.inner.clone();
runtime()
Expand Down Expand Up @@ -1996,6 +2006,10 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
)?;
admin.define_method("list_chains", method!(AdminApiClient::list_chains, 0))?;
admin.define_method("account_info", method!(AdminApiClient::account_info, 0))?;
admin.define_method(
"get_api_credits",
method!(AdminApiClient::get_api_credits, 1),
)?;
admin.define_method("list_invoices", method!(AdminApiClient::list_invoices, 0))?;
admin.define_method("list_payments", method!(AdminApiClient::list_payments, 0))?;
admin.define_method("list_teams", method!(AdminApiClient::list_teams, 0))?;
Expand Down
13 changes: 13 additions & 0 deletions npm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -948,6 +948,19 @@ Returns details about the account, including its id, name, creation timestamp, b
const resp = await qn.admin.accountInfo();
```

##### `get_api_credits` / `getApiCredits`

Returns the per-method API credit costs for a chain, identified by its slug (the same slugs returned by `list_chains`, e.g. `ethereum`). An unknown chain slug rejects with `ApiError` (status 404).

**Parameters**: `chain` (string, required) — the chain slug.

**Returns**: `GetApiCreditsResponse` with `data: ApiCredit[]`, where each `ApiCredit` has `method` and `credits`.

```typescript
// Node.js
const resp = await qn.admin.getApiCredits("ethereum");
```

#### Billing

##### `list_invoices` / `listInvoices`
Expand Down
8 changes: 8 additions & 0 deletions npm/examples/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ async function main() {
console.log(`account ${a.id} | ${a.name} | billing=${a.billingVersion} | plan=${plan}`);
}

const credits = await qn.admin.getApiCredits("ethereum");
if (credits.data) {
console.log(`ethereum api credits: ${credits.data.length} methods`);
for (const c of credits.data.slice(0, 3)) {
console.log(` ${c.method} = ${c.credits}`);
}
}

const response = await qn.admin.getEndpoints({
limit: 20,
sortBy: "created_at",
Expand Down
26 changes: 26 additions & 0 deletions npm/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,14 @@ export interface AdminConfig {
baseUrl?: string
}

/** The API credit cost of a single RPC method on a chain. */
export interface ApiCredit {
/** RPC method name (e.g. `eth_chainId`). */
method: string
/** Number of API credits the method costs. */
credits: number
}

/** Configuration for delivering stream batches to Azure Blob Storage. */
export interface AzureAttributes {
/** Azure storage account name. */
Expand Down Expand Up @@ -759,6 +767,17 @@ export interface GetAccountMetricsResponse {
error?: string
}

/** Response from `get_api_credits`. */
export interface GetApiCreditsResponse {
/**
* Per-method API credit costs for the chain, when the request succeeded.
* `None` for an unknown chain slug.
*/
data?: Array<ApiCredit>
/** Error message when the request did not succeed. */
error?: string
}

/** Parameters for `get_endpoint_logs`. */
export interface GetEndpointLogsRequest {
/** Start of the query window (timestamp). */
Expand Down Expand Up @@ -2239,6 +2258,13 @@ export declare class AdminApiClient {
* timestamp, billing version, and current subscription.
*/
accountInfo(): Promise<AccountInfoResponse>
/**
* Returns the per-method API credit costs for a chain, identified by its
* slug (the same slugs returned by `list_chains`, e.g. `ethereum`). Each
* item carries the RPC `method` name and its `credits` cost. An unknown
* chain slug rejects with `ApiError` (status 404).
*/
getApiCredits(chain: string): Promise<GetApiCreditsResponse>
/**
* Returns the account's invoices, including id, status, billing reason,
* amounts due and paid, line items with descriptions and billing periods,
Expand Down
3 changes: 3 additions & 0 deletions npm/sdk.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,9 @@ export type {
AccountSubscription,
AccountInfo,
AccountInfoResponse,
// api credits
ApiCredit,
GetApiCreditsResponse,
// metrics
GetEndpointMetricsRequest,
GetAccountMetricsRequest,
Expand Down
13 changes: 13 additions & 0 deletions python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -949,6 +949,19 @@ Returns details about the account, including its id, name, creation timestamp, b
resp = await qn.admin.account_info()
```

##### `get_api_credits` / `getApiCredits`

Returns the per-method API credit costs for a chain, identified by its slug (the same slugs returned by `list_chains`, e.g. `ethereum`). An unknown chain slug raises `ApiError` (status 404).

**Parameters**: `chain` (string, required) — the chain slug.

**Returns**: `GetApiCreditsResponse` with `data: list[ApiCredit]`, where each `ApiCredit` has `method` and `credits`.

```python
# Python
resp = await qn.admin.get_api_credits("ethereum")
```

#### Billing

##### `list_invoices` / `listInvoices`
Expand Down
6 changes: 6 additions & 0 deletions python/examples/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ async def main():
plan = a.subscription.plan_name if a.subscription is not None else "<none>"
print(f"account {a.id} | {a.name} | billing={a.billing_version} | plan={plan}")

credits = await qn.admin.get_api_credits("ethereum")
if credits.data is not None:
print(f"ethereum api credits: {len(credits.data)} methods")
for c in credits.data[:3]:
print(f" {c.method} = {c.credits}")

response = await qn.admin.get_endpoints(
limit=20,
sort_by="created_at",
Expand Down
Loading
Loading