diff --git a/CLAUDE.md b/CLAUDE.md index 3f4f216..b378855 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -154,7 +154,9 @@ Core clients are tested using mocked API calls with wiremock. All functions maki ### Polyglot consistency - When adding a new public type to `crates/core`, export it across all four layers: Rust re-exports in `lib.rs`, Python `__init__.py` + `init_manual_override.pyi`, TypeScript `sdk.d.ts`, and the Ruby binding in `crates/ruby/src/lib.rs` -- **Discriminated unions (Rust enum-with-data)** cannot be annotated with `#[pyclass]` or `#[napi(object)]`. Pattern: keep the enum pure-Rust in `crates/core` with `#[serde(tag = "...", content = "...")]`, then in each binding crate provide a typed surface that converts to/from the enum at the FFI boundary. Any core struct that flattens such an enum (e.g. via `#[serde(flatten)]`) also loses its `#[pyclass]` / `#[napi(object)]` and needs a wrapper in each binding. See `crates/core/src/streams/stream.rs::DestinationAttributes` and `crates/{python,node}/src/streams_destination.rs` for the reference implementation. +- **Discriminated unions (Rust enum-with-data)** cannot be annotated with `#[pyclass]` or `#[napi(object)]`. Pattern: keep the enum pure-Rust in `crates/core` with `#[serde(tag = "...", content = "...")]`, then in each binding crate provide a typed surface that converts to/from the enum at the FFI boundary. Any core struct that flattens such an enum (e.g. via `#[serde(flatten)]`) also loses its `#[pyclass]` / `#[napi(object)]` and needs a wrapper in each binding. Reference implementations: + - `crates/core/src/streams/stream.rs::DestinationAttributes` — stream destinations, wrapped by `crates/{python,node}/src/streams_destination.rs`. + - `crates/core/src/webhooks/webhook.rs::TemplateArgs` — webhook filter templates, wrapped by `crates/{python,node}/src/webhooks_template.rs`. Ruby accepts the wire JSON directly (`{"templateId":..., "templateArgs":...}`) and relies on serde to deserialize into the enum. - `python/sdk/__init__.py` is **manually maintained** — it is NOT auto-generated. Every new public struct/type must be added to both the `from sdk._core import (...)` block and the `__all__` list in this file - When adding a new type with `#[cfg_attr(feature = "node", napi(object))]`, also add it to the named `export type { ... }` block in `npm/sdk.d.ts` — this is the user-facing type file and is not auto-updated by napi-rs - When adding a new `#[napi(string_enum)]` Rust enum, it generates a TypeScript `const enum` in `npm/index.d.ts`. In `npm/sdk.d.ts`, these must be re-exported using a regular `export { ... }` (not `export type { ... }`), otherwise TypeScript consumers cannot use them as values (e.g., `StreamDataset.Block`) diff --git a/README.md b/README.md index dd211e8..78762fb 100644 --- a/README.md +++ b/README.md @@ -2542,7 +2542,7 @@ Paginated list of webhooks. **Parameters** (all optional): `limit` (i64), `offset` (i64). -**Returns**: `ListWebhooksResponse` with `data: Webhook[]` and `pageInfo`. +**Returns**: `ListWebhooksResponse` with `data: Webhook[]` and `pageInfo: WebhookPageInfo { limit, offset, total }`. ```rust // Rust @@ -2596,15 +2596,15 @@ webhook = JSON.parse(qn.webhooks.get_webhook(id: "wh-1")) Creates a webhook from a predefined filter template. -**Parameters**: `name` (required), `network` (required), `destination_attributes` (`WebhookDestinationAttributes`, required), `template_args` (`TemplateArgs`, required), `notification_email` (optional). +**Parameters**: `name` (required), `network` (required), `destination_attributes` (`WebhookDestinationAttributes`, required), `template_args` (required — use the `TemplateArgs` enum variant for the chosen template), `notification_email` (optional). **Returns**: `Webhook`. ```rust // Rust -let template_args = TemplateArgs::evm_wallet_filter(&EvmWalletFilterTemplate { +let template_args = TemplateArgs::EvmWalletFilter(EvmWalletFilterTemplate { wallets: vec!["0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48".to_string()], -})?; +}); let params = CreateWebhookFromTemplateParams { name: "Wallet Webhook".to_string(), network: "ethereum-mainnet".to_string(), @@ -2621,13 +2621,13 @@ let webhook = qn.webhooks.create_webhook_from_template(¶ms).await?; ```python # Python -from sdk import EvmWalletFilterTemplate, TemplateArgs, WebhookDestinationAttributes +from sdk import EvmWalletFilterArgs, EvmWalletFilterTemplate, WebhookDestinationAttributes webhook = await qn.webhooks.create_webhook_from_template( name="Wallet Webhook", network="ethereum-mainnet", destination_attributes=WebhookDestinationAttributes(url="https://webhook.site/..."), - template_args=TemplateArgs.evm_wallet_filter( + template_args=EvmWalletFilterArgs( EvmWalletFilterTemplate(wallets=["0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"]) ), ) @@ -2654,8 +2654,8 @@ destination_attributes = JSON.generate({ compression: "none" }) template_args = JSON.generate({ - template_id: "evmWalletFilter", - value: JSON.generate({ wallets: ["0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"] }) + templateId: "evmWalletFilter", + templateArgs: { wallets: ["0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"] } }) webhook = JSON.parse(qn.webhooks.create_webhook_from_template( name: "Wallet Webhook", @@ -2707,9 +2707,9 @@ Updates the template args (and optionally name, email, destination) on an existi ```rust // Rust -let template_args = TemplateArgs::evm_wallet_filter(&EvmWalletFilterTemplate { +let template_args = TemplateArgs::EvmWalletFilter(EvmWalletFilterTemplate { wallets: vec!["0xnewwallet".to_string()], -})?; +}); let params = UpdateWebhookTemplateParams { name: None, notification_email: None, @@ -2723,7 +2723,7 @@ let webhook = qn.webhooks.update_webhook_template("wh-1", ¶ms).await?; # Python webhook = await qn.webhooks.update_webhook_template( "wh-1", - template_args=TemplateArgs.evm_wallet_filter( + template_args=EvmWalletFilterArgs( EvmWalletFilterTemplate(wallets=["0xnewwallet"]) ), ) @@ -2739,8 +2739,8 @@ const webhook = await qn.webhooks.updateWebhookTemplate("wh-1", { ```ruby # Ruby template_args = JSON.generate({ - template_id: "evmWalletFilter", - value: JSON.generate({ wallets: ["0xnewwallet"] }) + templateId: "evmWalletFilter", + templateArgs: { wallets: ["0xnewwallet"] } }) webhook = JSON.parse(qn.webhooks.update_webhook_template( webhook_id: "wh-1", diff --git a/crates/core/examples/webhooks_e2e.rs b/crates/core/examples/webhooks_e2e.rs index d7dae34..8ac6f20 100644 --- a/crates/core/examples/webhooks_e2e.rs +++ b/crates/core/examples/webhooks_e2e.rs @@ -20,7 +20,11 @@ async fn main() { .list_webhooks(&GetWebhooksParams::default()) .await .expect("list_webhooks failed"); - println!("webhooks before: {}", before.data.len()); + println!( + "webhooks before: {} (total={})", + before.data.len(), + before.page_info.total + ); let count = qn .webhooks @@ -29,10 +33,9 @@ async fn main() { .expect("get_enabled_count failed"); println!("enabled count: {}", count.total); - let template_args = TemplateArgs::evm_wallet_filter(&EvmWalletFilterTemplate { + let template_args = TemplateArgs::EvmWalletFilter(EvmWalletFilterTemplate { wallets: vec!["0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48".to_string()], - }) - .expect("template args are valid"); + }); let create_params = CreateWebhookFromTemplateParams { name: "E2E Test Webhook".to_string(), @@ -100,5 +103,9 @@ async fn main() { .list_webhooks(&GetWebhooksParams::default()) .await .expect("list_webhooks failed"); - println!("webhooks after: {}", after.data.len()); + println!( + "webhooks after: {} (total={})", + after.data.len(), + after.page_info.total + ); } diff --git a/crates/core/src/webhooks/mod.rs b/crates/core/src/webhooks/mod.rs index 194cd43..dadf52c 100644 --- a/crates/core/src/webhooks/mod.rs +++ b/crates/core/src/webhooks/mod.rs @@ -6,7 +6,8 @@ pub use webhook::{ HyperliquidWalletEventsFilterTemplate, ListWebhooksResponse, SolanaWalletFilterTemplate, StellarWalletTransactionsFilterTemplate, TemplateArgs, UpdateWebhookParams, UpdateWebhookTemplateParams, Webhook, WebhookDestinationAttributes, - WebhookEnabledCountResponse, WebhookStartFrom, WebhookTemplateId, XrplWalletFilterTemplate, + WebhookEnabledCountResponse, WebhookPageInfo, WebhookStartFrom, WebhookTemplateId, + XrplWalletFilterTemplate, }; use crate::{config::WebhooksConfig, errors::SdkError, SdkConfig}; @@ -263,30 +264,17 @@ impl WebhooksApiClient { &self, params: &CreateWebhookFromTemplateParams, ) -> Result { - let template_id = params.template_args.template_id.as_str(); + let template_id = params.template_args.tag().as_str(); let url = self .config .webhooks() .base_url .join(&format!("webhooks/template/{template_id}"))?; - - #[allow(clippy::needless_borrows_for_generic_args)] - let mut body = - serde_json::to_value(¶ms).map_err(|e| SdkError::Config(e.to_string()))?; - let obj = body.as_object_mut().ok_or_else(|| { - SdkError::Config("failed to serialize request body as JSON object".into()) - })?; - obj.insert( - "templateArgs".to_string(), - serde_json::from_str(¶ms.template_args.value) - .map_err(|e| SdkError::Config(e.to_string()))?, - ); - let resp = self .config .http_client() .post(url) - .json(&body) + .json(params) .send() .await .map_err(SdkError::Http)?; @@ -309,30 +297,17 @@ impl WebhooksApiClient { webhook_id: &str, params: &UpdateWebhookTemplateParams, ) -> Result { - let template_id = params.template_args.template_id.as_str(); + let template_id = params.template_args.tag().as_str(); let url = self .config .webhooks() .base_url .join(&format!("webhooks/{webhook_id}/template/{template_id}"))?; - - #[allow(clippy::needless_borrows_for_generic_args)] - let mut body = - serde_json::to_value(¶ms).map_err(|e| SdkError::Config(e.to_string()))?; - let obj = body.as_object_mut().ok_or_else(|| { - SdkError::Config("failed to serialize request body as JSON object".into()) - })?; - obj.insert( - "templateArgs".to_string(), - serde_json::from_str(¶ms.template_args.value) - .map_err(|e| SdkError::Config(e.to_string()))?, - ); - let resp = self .config .http_client() .patch(url) - .json(&body) + .json(params) .send() .await .map_err(SdkError::Http)?; @@ -384,7 +359,12 @@ mod tests { async fn list_webhooks_success() { let server = MockServer::start().await; let response = serde_json::json!({ - "data": [webhook_response_json()] + "data": [webhook_response_json()], + "pageInfo": { + "limit": 20, + "offset": 0, + "total": 1, + } }); Mock::given(method("GET")) .and(path("/webhooks")) @@ -399,6 +379,9 @@ mod tests { .unwrap(); assert_eq!(resp.data.len(), 1); assert_eq!(resp.data[0].id, "wh-1234-5678"); + assert_eq!(resp.page_info.limit, 20); + assert_eq!(resp.page_info.offset, 0); + assert_eq!(resp.page_info.total, 1); } #[tokio::test] @@ -650,10 +633,9 @@ mod tests { .mount(&server) .await; let sdk = make_sdk(format!("{}/", server.uri())); - let template_args = TemplateArgs::evm_wallet_filter(&EvmWalletFilterTemplate { + let template_args = TemplateArgs::EvmWalletFilter(EvmWalletFilterTemplate { wallets: vec!["0xabc".to_string()], - }) - .unwrap(); + }); let params = CreateWebhookFromTemplateParams { name: "test-webhook".to_string(), network: "ethereum-mainnet".to_string(), @@ -682,10 +664,9 @@ mod tests { .mount(&server) .await; let sdk = make_sdk(format!("{}/", server.uri())); - let template_args = TemplateArgs::evm_wallet_filter(&EvmWalletFilterTemplate { + let template_args = TemplateArgs::EvmWalletFilter(EvmWalletFilterTemplate { wallets: vec!["0xabc".to_string()], - }) - .unwrap(); + }); let params = CreateWebhookFromTemplateParams { name: "test-webhook".to_string(), network: "ethereum-mainnet".to_string(), @@ -714,10 +695,9 @@ mod tests { .mount(&server) .await; let sdk = make_sdk(format!("{}/", server.uri())); - let template_args = TemplateArgs::evm_wallet_filter(&EvmWalletFilterTemplate { + let template_args = TemplateArgs::EvmWalletFilter(EvmWalletFilterTemplate { wallets: vec!["0xabc".to_string()], - }) - .unwrap(); + }); let params = UpdateWebhookTemplateParams { name: None, notification_email: None, @@ -741,10 +721,9 @@ mod tests { .mount(&server) .await; let sdk = make_sdk(format!("{}/", server.uri())); - let template_args = TemplateArgs::evm_wallet_filter(&EvmWalletFilterTemplate { + let template_args = TemplateArgs::EvmWalletFilter(EvmWalletFilterTemplate { wallets: vec!["0xabc".to_string()], - }) - .unwrap(); + }); let params = UpdateWebhookTemplateParams { name: None, notification_email: None, diff --git a/crates/core/src/webhooks/webhook.rs b/crates/core/src/webhooks/webhook.rs index b1b0493..42eac0f 100644 --- a/crates/core/src/webhooks/webhook.rs +++ b/crates/core/src/webhooks/webhook.rs @@ -3,13 +3,11 @@ use bon::Builder; #[cfg(feature = "node")] use napi_derive::napi; #[cfg(feature = "python")] -use pyo3::{exceptions::PyValueError, pyclass, pymethods, PyResult}; +use pyo3::{pyclass, pymethods}; #[cfg(feature = "python")] use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; use serde::{Deserialize, Deserializer, Serialize}; -use crate::errors::SdkError; - fn deserialize_as_optional_json_string<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, @@ -256,165 +254,51 @@ impl StellarWalletTransactionsFilterTemplate { // ── Template Args ────────────────────────────────────────────────────────── -// The API expects a `template_id` string and a `template_args` object whose -// shape depends on which template is selected. The natural Rust model would be -// an enum with per-variant data, but napi-rs and PyO3 cannot represent Rust -// discriminated unions at the FFI boundary — they require flat structs. -// Instead, `TemplateArgs` is a flat wrapper struct that bundles the template -// variant with its pre-serialized JSON value. Callers construct it via typed -// static factory methods (one per template), so they never interact with raw -// JSON. - -/// Template identifier paired with its arguments, consumed by -/// `create_webhook_from_template` and `update_webhook_template`. Construct via -/// the typed static factory methods (one per template); do not set fields -/// directly. -#[cfg_attr(feature = "python", gen_stub_pyclass)] -#[cfg_attr(feature = "python", pyclass)] -#[cfg_attr(feature = "node", napi(object))] +/// Template identifier paired with its arguments. Exactly one variant selects +/// which filter is applied. Consumed by `create_webhook_from_template` and +/// `update_webhook_template`. +// Pure-Rust discriminated union; no #[pyclass] / #[napi(object)] because PyO3 +// and napi-rs cannot represent enum-with-data. Each language binding crate +// wraps this type for its own FFI surface. +// The serde tag/content pair matches the API wire format when flattened into +// a request struct. #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TemplateArgs { - /// Which filter template these arguments correspond to. - // pub fields required for napi(object) to expose them in TypeScript. - // Callers should use the typed factory methods rather than setting fields - // directly — the value field is a pre-serialized JSON string. - pub template_id: WebhookTemplateId, - /// Template arguments, pre-serialized as a JSON string. - // Stored as a JSON string so napi(object) can represent it (serde_json::Value - // is not supported by napi-rs). Parsed back to Value in the client. - pub value: String, -} - -// napi(object) on params structs requires all fields to implement Default so -// napi can handle cases where the field is absent in JS. In practice, -// template_args is always required — the default is never used. -impl Default for TemplateArgs { - fn default() -> Self { - Self { - template_id: WebhookTemplateId::EvmWalletFilter, - value: "null".to_string(), - } - } +#[serde(tag = "templateId", content = "templateArgs", rename_all = "camelCase")] +pub enum TemplateArgs { + /// EVM wallet filter: matches activity for a list of wallet addresses. + EvmWalletFilter(EvmWalletFilterTemplate), + /// EVM contract events filter, optionally scoped to specific event topic hashes. + EvmContractEvents(EvmContractEventsTemplate), + /// EVM ABI filter: decodes and filters events using a provided ABI. + EvmAbiFilter(EvmAbiFilterTemplate), + /// Solana wallet filter. + SolanaWalletFilter(SolanaWalletFilterTemplate), + /// Bitcoin wallet filter. + BitcoinWalletFilter(BitcoinWalletFilterTemplate), + /// XRPL wallet filter. + XrplWalletFilter(XrplWalletFilterTemplate), + /// Hyperliquid wallet-events filter. + HyperliquidWalletEventsFilter(HyperliquidWalletEventsFilterTemplate), + /// Stellar wallet-transactions filter (source-account match). + StellarWalletTransactionsSourceAccountFilter(StellarWalletTransactionsFilterTemplate), } impl TemplateArgs { - pub fn evm_wallet_filter(attrs: &EvmWalletFilterTemplate) -> Result { - Ok(Self { - template_id: WebhookTemplateId::EvmWalletFilter, - value: serde_json::to_string(attrs).map_err(|e| SdkError::Config(e.to_string()))?, - }) - } - - pub fn evm_contract_events(attrs: &EvmContractEventsTemplate) -> Result { - Ok(Self { - template_id: WebhookTemplateId::EvmContractEvents, - value: serde_json::to_string(attrs).map_err(|e| SdkError::Config(e.to_string()))?, - }) - } - - pub fn evm_abi_filter(attrs: &EvmAbiFilterTemplate) -> Result { - Ok(Self { - template_id: WebhookTemplateId::EvmAbiFilter, - value: serde_json::to_string(attrs).map_err(|e| SdkError::Config(e.to_string()))?, - }) - } - - pub fn solana_wallet_filter(attrs: &SolanaWalletFilterTemplate) -> Result { - Ok(Self { - template_id: WebhookTemplateId::SolanaWalletFilter, - value: serde_json::to_string(attrs).map_err(|e| SdkError::Config(e.to_string()))?, - }) - } - - pub fn bitcoin_wallet_filter(attrs: &BitcoinWalletFilterTemplate) -> Result { - Ok(Self { - template_id: WebhookTemplateId::BitcoinWalletFilter, - value: serde_json::to_string(attrs).map_err(|e| SdkError::Config(e.to_string()))?, - }) - } - - pub fn xrpl_wallet_filter(attrs: &XrplWalletFilterTemplate) -> Result { - Ok(Self { - template_id: WebhookTemplateId::XrplWalletFilter, - value: serde_json::to_string(attrs).map_err(|e| SdkError::Config(e.to_string()))?, - }) - } - - pub fn hyperliquid_wallet_events_filter( - attrs: &HyperliquidWalletEventsFilterTemplate, - ) -> Result { - Ok(Self { - template_id: WebhookTemplateId::HyperliquidWalletEventsFilter, - value: serde_json::to_string(attrs).map_err(|e| SdkError::Config(e.to_string()))?, - }) - } - - pub fn stellar_wallet_transactions_filter( - attrs: &StellarWalletTransactionsFilterTemplate, - ) -> Result { - Ok(Self { - template_id: WebhookTemplateId::StellarWalletTransactionsSourceAccountFilter, - value: serde_json::to_string(attrs).map_err(|e| SdkError::Config(e.to_string()))?, - }) - } -} - -#[cfg(feature = "python")] -#[gen_stub_pymethods] -#[pymethods] -impl TemplateArgs { - #[staticmethod] - #[pyo3(name = "evm_wallet_filter")] - fn py_evm_wallet_filter(attrs: &EvmWalletFilterTemplate) -> PyResult { - Self::evm_wallet_filter(attrs).map_err(|e| PyValueError::new_err(e.to_string())) - } - - #[staticmethod] - #[pyo3(name = "evm_contract_events")] - fn py_evm_contract_events(attrs: &EvmContractEventsTemplate) -> PyResult { - Self::evm_contract_events(attrs).map_err(|e| PyValueError::new_err(e.to_string())) - } - - #[staticmethod] - #[pyo3(name = "evm_abi_filter")] - fn py_evm_abi_filter(attrs: &EvmAbiFilterTemplate) -> PyResult { - Self::evm_abi_filter(attrs).map_err(|e| PyValueError::new_err(e.to_string())) - } - - #[staticmethod] - #[pyo3(name = "solana_wallet_filter")] - fn py_solana_wallet_filter(attrs: &SolanaWalletFilterTemplate) -> PyResult { - Self::solana_wallet_filter(attrs).map_err(|e| PyValueError::new_err(e.to_string())) - } - - #[staticmethod] - #[pyo3(name = "bitcoin_wallet_filter")] - fn py_bitcoin_wallet_filter(attrs: &BitcoinWalletFilterTemplate) -> PyResult { - Self::bitcoin_wallet_filter(attrs).map_err(|e| PyValueError::new_err(e.to_string())) - } - - #[staticmethod] - #[pyo3(name = "xrpl_wallet_filter")] - fn py_xrpl_wallet_filter(attrs: &XrplWalletFilterTemplate) -> PyResult { - Self::xrpl_wallet_filter(attrs).map_err(|e| PyValueError::new_err(e.to_string())) - } - - #[staticmethod] - #[pyo3(name = "hyperliquid_wallet_events_filter")] - fn py_hyperliquid_wallet_events_filter( - attrs: &HyperliquidWalletEventsFilterTemplate, - ) -> PyResult { - Self::hyperliquid_wallet_events_filter(attrs) - .map_err(|e| PyValueError::new_err(e.to_string())) - } - - #[staticmethod] - #[pyo3(name = "stellar_wallet_transactions_filter")] - fn py_stellar_wallet_transactions_filter( - attrs: &StellarWalletTransactionsFilterTemplate, - ) -> PyResult { - Self::stellar_wallet_transactions_filter(attrs) - .map_err(|e| PyValueError::new_err(e.to_string())) + pub fn tag(&self) -> WebhookTemplateId { + match self { + Self::EvmWalletFilter(_) => WebhookTemplateId::EvmWalletFilter, + Self::EvmContractEvents(_) => WebhookTemplateId::EvmContractEvents, + Self::EvmAbiFilter(_) => WebhookTemplateId::EvmAbiFilter, + Self::SolanaWalletFilter(_) => WebhookTemplateId::SolanaWalletFilter, + Self::BitcoinWalletFilter(_) => WebhookTemplateId::BitcoinWalletFilter, + Self::XrplWalletFilter(_) => WebhookTemplateId::XrplWalletFilter, + Self::HyperliquidWalletEventsFilter(_) => { + WebhookTemplateId::HyperliquidWalletEventsFilter + } + Self::StellarWalletTransactionsSourceAccountFilter(_) => { + WebhookTemplateId::StellarWalletTransactionsSourceAccountFilter + } + } } } @@ -529,9 +413,7 @@ pub struct ActivateWebhookParams { /// Parameters for `create_webhook_from_template`. #[cfg_attr(feature = "rust", derive(Builder))] -#[cfg_attr(feature = "node", napi(object))] -#[cfg_attr(not(feature = "node"), derive(Clone))] -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct CreateWebhookFromTemplateParams { /// Human-readable label for the webhook. pub name: String, @@ -543,18 +425,13 @@ pub struct CreateWebhookFromTemplateParams { /// Destination configuration for delivered payloads. pub destination_attributes: WebhookDestinationAttributes, /// Filter template identifier and its arguments. - // template_args is skipped here and inserted manually into the request body - // in the client, so serde doesn't try to serialize it as a field of this struct. - #[serde(skip)] + // Flattening the enum's tag/content produces { templateId, templateArgs }. + #[serde(flatten)] pub template_args: TemplateArgs, } /// Parameters for `update_webhook_template`. -#[cfg_attr(feature = "python", gen_stub_pyclass)] -#[cfg_attr(feature = "python", pyclass(get_all, set_all))] -#[cfg_attr(feature = "node", napi(object))] -#[cfg_attr(not(feature = "node"), derive(Clone))] -#[derive(Debug, Default, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct UpdateWebhookTemplateParams { /// New human-readable name. #[serde(skip_serializing_if = "Option::is_none")] @@ -566,33 +443,11 @@ pub struct UpdateWebhookTemplateParams { #[serde(skip_serializing_if = "Option::is_none")] pub destination_attributes: Option, /// New template identifier and arguments. - // template_id and template_args are skipped here and inserted manually into - // the request body in the client. - #[serde(skip)] + // Flattening the enum's tag/content produces { templateId, templateArgs }. + #[serde(flatten)] pub template_args: TemplateArgs, } -#[cfg(feature = "python")] -#[gen_stub_pymethods] -#[pymethods] -impl UpdateWebhookTemplateParams { - #[new] - #[pyo3(signature = (template_args, name=None, notification_email=None, destination_attributes=None))] - pub fn new( - template_args: TemplateArgs, - name: Option, - notification_email: Option, - destination_attributes: Option, - ) -> Self { - Self { - name, - notification_email, - destination_attributes, - template_args, - } - } -} - // ── Response Types ───────────────────────────────────────────────────────── /// A webhook's full configuration and current state. @@ -629,6 +484,20 @@ pub struct Webhook { pub destination_attributes: Option, } +/// Pagination metadata returned alongside a paginated webhooks list. +#[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 WebhookPageInfo { + /// Page size used for this response. + pub limit: i64, + /// Starting index of this page within the full result set. + pub offset: i64, + /// Total number of webhooks matching the query across all pages. + pub total: i64, +} + /// Response from `list_webhooks`. #[cfg_attr(feature = "python", gen_stub_pyclass)] #[cfg_attr(feature = "python", pyclass(get_all, set_all))] @@ -637,6 +506,9 @@ pub struct Webhook { pub struct ListWebhooksResponse { /// Webhooks on the current page. pub data: Vec, + /// Pagination metadata for the response. + #[serde(rename = "pageInfo")] + pub page_info: WebhookPageInfo, } /// Response from `get_enabled_count` for webhooks. @@ -648,3 +520,135 @@ pub struct WebhookEnabledCountResponse { /// Total count of enabled webhooks on the account. pub total: i64, } + +#[cfg(test)] +#[allow(clippy::unwrap_used)] +mod template_args_tests { + use super::*; + + #[test] + fn evm_wallet_filter_roundtrip() { + let args = TemplateArgs::EvmWalletFilter(EvmWalletFilterTemplate { + wallets: vec!["0xabc".to_string()], + }); + let json = serde_json::to_string(&args).unwrap(); + assert!(json.contains(r#""templateId":"evmWalletFilter""#)); + assert!(json.contains(r#""wallets":["0xabc"]"#)); + let parsed: TemplateArgs = serde_json::from_str(&json).unwrap(); + assert!(matches!(parsed, TemplateArgs::EvmWalletFilter(_))); + assert!(matches!(parsed.tag(), WebhookTemplateId::EvmWalletFilter)); + } + + #[test] + fn evm_contract_events_roundtrip() { + let args = TemplateArgs::EvmContractEvents(EvmContractEventsTemplate { + contracts: vec!["0xdef".to_string()], + event_hashes: Some(vec!["0x1234".to_string()]), + }); + let json = serde_json::to_string(&args).unwrap(); + assert!(json.contains(r#""templateId":"evmContractEvents""#)); + let parsed: TemplateArgs = serde_json::from_str(&json).unwrap(); + assert!(matches!(parsed, TemplateArgs::EvmContractEvents(_))); + } + + #[test] + fn evm_abi_filter_roundtrip() { + let args = TemplateArgs::EvmAbiFilter(EvmAbiFilterTemplate { + abi: "[]".to_string(), + contracts: vec!["0xdef".to_string()], + }); + let json = serde_json::to_string(&args).unwrap(); + assert!(json.contains(r#""templateId":"evmAbiFilter""#)); + let parsed: TemplateArgs = serde_json::from_str(&json).unwrap(); + assert!(matches!(parsed, TemplateArgs::EvmAbiFilter(_))); + } + + #[test] + fn solana_wallet_filter_roundtrip() { + let args = TemplateArgs::SolanaWalletFilter(SolanaWalletFilterTemplate { + accounts: vec!["acc".to_string()], + }); + let json = serde_json::to_string(&args).unwrap(); + assert!(json.contains(r#""templateId":"solanaWalletFilter""#)); + let parsed: TemplateArgs = serde_json::from_str(&json).unwrap(); + assert!(matches!(parsed, TemplateArgs::SolanaWalletFilter(_))); + } + + #[test] + fn bitcoin_wallet_filter_roundtrip() { + let args = TemplateArgs::BitcoinWalletFilter(BitcoinWalletFilterTemplate { + wallets: vec!["bc1".to_string()], + }); + let json = serde_json::to_string(&args).unwrap(); + assert!(json.contains(r#""templateId":"bitcoinWalletFilter""#)); + let parsed: TemplateArgs = serde_json::from_str(&json).unwrap(); + assert!(matches!(parsed, TemplateArgs::BitcoinWalletFilter(_))); + } + + #[test] + fn xrpl_wallet_filter_roundtrip() { + let args = TemplateArgs::XrplWalletFilter(XrplWalletFilterTemplate { + wallets: vec!["r1".to_string()], + }); + let json = serde_json::to_string(&args).unwrap(); + assert!(json.contains(r#""templateId":"xrplWalletFilter""#)); + let parsed: TemplateArgs = serde_json::from_str(&json).unwrap(); + assert!(matches!(parsed, TemplateArgs::XrplWalletFilter(_))); + } + + #[test] + fn hyperliquid_wallet_events_filter_roundtrip() { + let args = + TemplateArgs::HyperliquidWalletEventsFilter(HyperliquidWalletEventsFilterTemplate { + wallets: vec!["0xhl".to_string()], + }); + let json = serde_json::to_string(&args).unwrap(); + assert!(json.contains(r#""templateId":"hyperliquidWalletEventsFilter""#)); + let parsed: TemplateArgs = serde_json::from_str(&json).unwrap(); + assert!(matches!( + parsed, + TemplateArgs::HyperliquidWalletEventsFilter(_) + )); + } + + #[test] + fn stellar_wallet_transactions_filter_roundtrip() { + let args = TemplateArgs::StellarWalletTransactionsSourceAccountFilter( + StellarWalletTransactionsFilterTemplate { + wallets: vec!["G...".to_string()], + }, + ); + let json = serde_json::to_string(&args).unwrap(); + assert!(json.contains(r#""templateId":"stellarWalletTransactionsSourceAccountFilter""#)); + let parsed: TemplateArgs = serde_json::from_str(&json).unwrap(); + assert!(matches!( + parsed, + TemplateArgs::StellarWalletTransactionsSourceAccountFilter(_) + )); + } + + #[test] + fn create_params_flattens_template_args() { + let params = CreateWebhookFromTemplateParams { + name: "n".to_string(), + network: "ethereum-mainnet".to_string(), + notification_email: None, + destination_attributes: WebhookDestinationAttributes { + url: "https://x".to_string(), + security_token: None, + compression: None, + }, + template_args: TemplateArgs::EvmWalletFilter(EvmWalletFilterTemplate { + wallets: vec!["0xabc".to_string()], + }), + }; + let json = serde_json::to_value(¶ms).unwrap(); + let obj = json.as_object().unwrap(); + assert_eq!( + obj.get("templateId").and_then(|v| v.as_str()), + Some("evmWalletFilter") + ); + assert!(obj.get("templateArgs").unwrap().is_object()); + assert_eq!(obj["templateArgs"]["wallets"][0].as_str(), Some("0xabc")); + } +} diff --git a/crates/node/src/key_case.rs b/crates/node/src/key_case.rs new file mode 100644 index 0000000..51edb24 --- /dev/null +++ b/crates/node/src/key_case.rs @@ -0,0 +1,49 @@ +// Case conversion helpers shared by napi object rewrites. Used when a +// binding surface accepts/returns `serde_json::Value` (because napi cannot +// represent a flattened enum), so per-field case conversion that napi would +// normally do for `#[napi(object)]` structs has to be done manually. + +pub fn camel_to_snake(s: &str) -> String { + let mut out = String::with_capacity(s.len() + 2); + for (i, c) in s.chars().enumerate() { + if c.is_ascii_uppercase() { + if i != 0 { + out.push('_'); + } + out.push(c.to_ascii_lowercase()); + } else { + out.push(c); + } + } + out +} + +pub fn snake_to_camel(s: &str) -> String { + let mut out = String::with_capacity(s.len()); + let mut upper_next = false; + for c in s.chars() { + if c == '_' { + upper_next = true; + } else if upper_next { + out.push(c.to_ascii_uppercase()); + upper_next = false; + } else { + out.push(c); + } + } + out +} + +pub fn convert_keys String + Copy>(v: serde_json::Value, f: F) -> serde_json::Value { + match v { + serde_json::Value::Object(o) => serde_json::Value::Object( + o.into_iter() + .map(|(k, v)| (f(&k), convert_keys(v, f))) + .collect(), + ), + serde_json::Value::Array(a) => { + serde_json::Value::Array(a.into_iter().map(|v| convert_keys(v, f)).collect()) + } + other => other, + } +} diff --git a/crates/node/src/lib.rs b/crates/node/src/lib.rs index e64ad25..7d282a3 100644 --- a/crates/node/src/lib.rs +++ b/crates/node/src/lib.rs @@ -3,7 +3,9 @@ use napi_derive::napi; use quicknode_sdk as core; mod errors; +mod key_case; mod streams_destination; +mod webhooks_template; // ── Top-level SDK ────────────────────────────────────────────── @@ -1146,8 +1148,9 @@ impl WebhooksApiClient { #[napi] pub async fn create_webhook_from_template( &self, - params: core::webhooks::CreateWebhookFromTemplateParams, + params: webhooks_template::CreateWebhookFromTemplateParamsNode, ) -> Result { + let params = params.into_core()?; self.inner .create_webhook_from_template(¶ms) .await @@ -1164,8 +1167,9 @@ impl WebhooksApiClient { pub async fn update_webhook_template( &self, webhook_id: String, - params: core::webhooks::UpdateWebhookTemplateParams, + params: webhooks_template::UpdateWebhookTemplateParamsNode, ) -> Result { + let params = params.into_core()?; self.inner .update_webhook_template(&webhook_id, ¶ms) .await diff --git a/crates/node/src/streams_destination.rs b/crates/node/src/streams_destination.rs index c0f523a..82c243d 100644 --- a/crates/node/src/streams_destination.rs +++ b/crates/node/src/streams_destination.rs @@ -2,6 +2,8 @@ use napi::bindgen_prelude::*; use napi_derive::napi; use quicknode_sdk as core; +use crate::key_case::{camel_to_snake, convert_keys, snake_to_camel}; + // napi(object) cannot represent the flattened DestinationAttributes enum on // core's stream types, so these wrappers carry it as serde_json::Value. // @@ -17,51 +19,6 @@ use quicknode_sdk as core; // structs, but a raw serde_json::Value bypasses that — so we walk the inner // object here. -fn camel_to_snake(s: &str) -> String { - let mut out = String::with_capacity(s.len() + 2); - for (i, c) in s.chars().enumerate() { - if c.is_ascii_uppercase() { - if i != 0 { - out.push('_'); - } - out.push(c.to_ascii_lowercase()); - } else { - out.push(c); - } - } - out -} - -fn snake_to_camel(s: &str) -> String { - let mut out = String::with_capacity(s.len()); - let mut upper_next = false; - for c in s.chars() { - if c == '_' { - upper_next = true; - } else if upper_next { - out.push(c.to_ascii_uppercase()); - upper_next = false; - } else { - out.push(c); - } - } - out -} - -fn convert_keys String + Copy>(v: serde_json::Value, f: F) -> serde_json::Value { - match v { - serde_json::Value::Object(o) => serde_json::Value::Object( - o.into_iter() - .map(|(k, v)| (f(&k), convert_keys(v, f))) - .collect(), - ), - serde_json::Value::Array(a) => { - serde_json::Value::Array(a.into_iter().map(|v| convert_keys(v, f)).collect()) - } - other => other, - } -} - fn node_da_to_core(v: serde_json::Value) -> Result { let mut obj = match v { serde_json::Value::Object(o) => o, diff --git a/crates/node/src/webhooks_template.rs b/crates/node/src/webhooks_template.rs new file mode 100644 index 0000000..c629647 --- /dev/null +++ b/crates/node/src/webhooks_template.rs @@ -0,0 +1,83 @@ +use napi::bindgen_prelude::*; +use napi_derive::napi; +use quicknode_sdk as core; + +use crate::key_case::{camel_to_snake, convert_keys}; + +// napi(object) cannot represent the flattened TemplateArgs enum on core's +// webhook params, so these node-facing params carry template_args as +// serde_json::Value. +// +// The Node input shape is `{ templateId, args: {...} }` — the inner key is +// renamed from the API's `templateArgs` to `args` to avoid +// `templateArgs.templateArgs.wallets` in TypeScript. node_ta_to_core() +// renames it back before deserializing. +// +// Keys inside `args` also need case conversion: TypeScript callers write +// camelCase (eventHashes), but core's serde structs expect snake_case +// (event_hashes). napi does this automatically for #[napi(object)] structs, +// but a raw serde_json::Value bypasses that — so we walk the inner object +// here. + +fn node_ta_to_core(v: serde_json::Value) -> Result { + let mut obj = match v { + serde_json::Value::Object(o) => o, + _ => { + return Err(Error::from_reason( + "template_args must be an object".to_string(), + )) + } + }; + let args = obj + .remove("args") + .ok_or_else(|| Error::from_reason("templateArgs.args is required".to_string()))?; + let args = convert_keys(args, camel_to_snake); + // Core's tag key is `templateId`, content key is `templateArgs`. Input + // already has `templateId`; rename `args` -> `templateArgs`. + obj.insert("templateArgs".to_string(), args); + let wire = serde_json::Value::Object(obj); + serde_json::from_value::(wire) + .map_err(|e| Error::from_reason(format!("invalid template_args: {e}"))) +} + +#[napi(object)] +pub struct CreateWebhookFromTemplateParamsNode { + pub name: String, + pub network: String, + pub notification_email: Option, + pub destination_attributes: core::webhooks::WebhookDestinationAttributes, + pub template_args: serde_json::Value, +} + +impl CreateWebhookFromTemplateParamsNode { + pub fn into_core(self) -> Result { + let template_args = node_ta_to_core(self.template_args)?; + Ok(core::webhooks::CreateWebhookFromTemplateParams { + name: self.name, + network: self.network, + notification_email: self.notification_email, + destination_attributes: self.destination_attributes, + template_args, + }) + } +} + +#[napi(object)] +pub struct UpdateWebhookTemplateParamsNode { + pub name: Option, + pub notification_email: Option, + pub destination_attributes: Option, + pub template_args: serde_json::Value, +} + +impl UpdateWebhookTemplateParamsNode { + pub fn into_core(self) -> Result { + let template_args = node_ta_to_core(self.template_args)?; + Ok(core::webhooks::UpdateWebhookTemplateParams { + name: self.name, + notification_email: self.notification_email, + destination_attributes: self.destination_attributes, + template_args, + }) + } +} diff --git a/crates/python/src/lib.rs b/crates/python/src/lib.rs index 70aa4c2..43738f1 100644 --- a/crates/python/src/lib.rs +++ b/crates/python/src/lib.rs @@ -7,6 +7,7 @@ use quicknode_sdk as core; mod errors; mod streams_destination; +mod webhooks_template; // ── Top-level SDK ────────────────────────────────────────────── @@ -1966,10 +1967,14 @@ impl WebhooksApiClient { name: String, network: String, destination_attributes: core::webhooks::WebhookDestinationAttributes, - template_args: core::webhooks::TemplateArgs, + #[gen_stub(override_type( + type_repr = "typing.Union[EvmWalletFilterArgs, EvmContractEventsArgs, EvmAbiFilterArgs, SolanaWalletFilterArgs, BitcoinWalletFilterArgs, XrplWalletFilterArgs, HyperliquidWalletEventsFilterArgs, StellarWalletTransactionsFilterArgs]" + ))] + template_args: &Bound<'py, PyAny>, notification_email: Option, ) -> PyResult> { let client = self.inner.clone(); + let template_args = webhooks_template::extract_template_args(template_args)?; let params = core::webhooks::CreateWebhookFromTemplateParams { name, network, @@ -1999,12 +2004,16 @@ impl WebhooksApiClient { &self, py: Python<'py>, webhook_id: String, - template_args: core::webhooks::TemplateArgs, + #[gen_stub(override_type( + type_repr = "typing.Union[EvmWalletFilterArgs, EvmContractEventsArgs, EvmAbiFilterArgs, SolanaWalletFilterArgs, BitcoinWalletFilterArgs, XrplWalletFilterArgs, HyperliquidWalletEventsFilterArgs, StellarWalletTransactionsFilterArgs]" + ))] + template_args: &Bound<'py, PyAny>, name: Option, notification_email: Option, destination_attributes: Option, ) -> PyResult> { let client = self.inner.clone(); + let template_args = webhooks_template::extract_template_args(template_args)?; let params = core::webhooks::UpdateWebhookTemplateParams { name, notification_email, @@ -2434,10 +2443,18 @@ 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::()?; 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/python/src/webhooks_template.rs b/crates/python/src/webhooks_template.rs new file mode 100644 index 0000000..a350d4f --- /dev/null +++ b/crates/python/src/webhooks_template.rs @@ -0,0 +1,116 @@ +use pyo3::prelude::*; +use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; +use quicknode_sdk::webhooks::{ + BitcoinWalletFilterTemplate, EvmAbiFilterTemplate, EvmContractEventsTemplate, + EvmWalletFilterTemplate, HyperliquidWalletEventsFilterTemplate, SolanaWalletFilterTemplate, + StellarWalletTransactionsFilterTemplate, TemplateArgs, XrplWalletFilterTemplate, +}; + +// Per-template typed wrappers. PyO3 cannot represent a Rust enum-with-data, +// so each variant of core's TemplateArgs is exposed as its own #[pyclass] +// here. extract_template_args() reassembles the enum at the FFI boundary. + +macro_rules! template_wrapper { + ($name:ident, $attrs:ident, $variant:ident) => { + #[gen_stub_pyclass] + #[pyclass] + #[derive(Clone)] + pub struct $name { + pub(crate) attrs: $attrs, + } + + #[gen_stub_pymethods] + #[pymethods] + impl $name { + #[new] + pub fn new(attrs: $attrs) -> Self { + Self { attrs } + } + + #[getter] + pub fn attributes(&self) -> $attrs { + self.attrs.clone() + } + } + + impl $name { + pub fn to_core(&self) -> TemplateArgs { + TemplateArgs::$variant(self.attrs.clone()) + } + } + }; +} + +template_wrapper!( + EvmWalletFilterArgs, + EvmWalletFilterTemplate, + EvmWalletFilter +); +template_wrapper!( + EvmContractEventsArgs, + EvmContractEventsTemplate, + EvmContractEvents +); +template_wrapper!(EvmAbiFilterArgs, EvmAbiFilterTemplate, EvmAbiFilter); +template_wrapper!( + SolanaWalletFilterArgs, + SolanaWalletFilterTemplate, + SolanaWalletFilter +); +template_wrapper!( + BitcoinWalletFilterArgs, + BitcoinWalletFilterTemplate, + BitcoinWalletFilter +); +template_wrapper!( + XrplWalletFilterArgs, + XrplWalletFilterTemplate, + XrplWalletFilter +); +template_wrapper!( + HyperliquidWalletEventsFilterArgs, + HyperliquidWalletEventsFilterTemplate, + HyperliquidWalletEventsFilter +); +template_wrapper!( + StellarWalletTransactionsFilterArgs, + StellarWalletTransactionsFilterTemplate, + StellarWalletTransactionsSourceAccountFilter +); + +pub fn extract_template_args(obj: &Bound<'_, PyAny>) -> PyResult { + if let Ok(v) = obj.extract::() { + return Ok(v.to_core()); + } + if let Ok(v) = obj.extract::() { + return Ok(v.to_core()); + } + if let Ok(v) = obj.extract::() { + return Ok(v.to_core()); + } + if let Ok(v) = obj.extract::() { + return Ok(v.to_core()); + } + if let Ok(v) = obj.extract::() { + return Ok(v.to_core()); + } + if let Ok(v) = obj.extract::() { + return Ok(v.to_core()); + } + if let Ok(v) = obj.extract::() { + return Ok(v.to_core()); + } + if let Ok(v) = obj.extract::() { + return Ok(v.to_core()); + } + let received = obj + .get_type() + .name() + .map_or_else(|_| "".to_string(), |n| n.to_string()); + Err(PyErr::new::(format!( + "template_args must be one of EvmWalletFilterArgs, EvmContractEventsArgs, \ + EvmAbiFilterArgs, SolanaWalletFilterArgs, BitcoinWalletFilterArgs, \ + XrplWalletFilterArgs, HyperliquidWalletEventsFilterArgs, \ + StellarWalletTransactionsFilterArgs — got {received}" + ))) +} diff --git a/npm/examples/webhooks_e2e.ts b/npm/examples/webhooks_e2e.ts index 6d47c69..1080ec7 100644 --- a/npm/examples/webhooks_e2e.ts +++ b/npm/examples/webhooks_e2e.ts @@ -10,7 +10,7 @@ async function main() { const qn = QuicknodeSdk.fromEnv(); const before = await qn.webhooks.listWebhooks(); - console.log(`webhooks before: ${before.data.length}`); + console.log(`webhooks before: ${before.data.length} (total=${before.pageInfo.total})`); const count = await qn.webhooks.getEnabledCount(); console.log(`enabled count: ${count.total}`); @@ -33,7 +33,9 @@ async function main() { const fetched = await qn.webhooks.getWebhook(id); console.log(`fetched: ${fetched.id} | ${fetched.name}`); - const updateParams: UpdateWebhookParams = { name: "E2E Test Webhook Updated" }; + const updateParams: UpdateWebhookParams = { + name: "E2E Test Webhook Updated", + }; const updated = await qn.webhooks.updateWebhook(id, updateParams); console.log(`updated name: ${updated.name}`); @@ -48,7 +50,7 @@ async function main() { await sleep(1000); const after = await qn.webhooks.listWebhooks(); - console.log(`webhooks after: ${after.data.length}`); + console.log(`webhooks after: ${after.data.length} (total=${after.pageInfo.total})`); } main(); diff --git a/npm/index.d.ts b/npm/index.d.ts index 4f5d40d..0bd4e72 100644 --- a/npm/index.d.ts +++ b/npm/index.d.ts @@ -1638,18 +1638,6 @@ export interface StellarWalletTransactionsFilterTemplate { /** Stellar wallet addresses to match against. */ wallets: Array } -/** - * Template identifier paired with its arguments, consumed by - * `create_webhook_from_template` and `update_webhook_template`. Construct via - * the typed static factory methods (one per template); do not set fields - * directly. - */ -export interface TemplateArgs { - /** Which filter template these arguments correspond to. */ - templateId: WebhookTemplateId - /** Template arguments, pre-serialized as a JSON string. */ - value: string -} /** Destination configuration for a webhook. */ export interface WebhookDestinationAttributes { /** Target URL that receives webhook payloads. */ @@ -1683,30 +1671,6 @@ export interface ActivateWebhookParams { /** Position to begin (or resume) delivery from. */ startFrom: WebhookStartFrom } -/** Parameters for `create_webhook_from_template`. */ -export interface CreateWebhookFromTemplateParams { - /** Human-readable label for the webhook. */ - name: string - /** Blockchain network to watch (e.g. `ethereum-mainnet`). */ - network: string - /** Optional email that receives alerts if the webhook terminates. */ - notificationEmail?: string - /** Destination configuration for delivered payloads. */ - destinationAttributes: WebhookDestinationAttributes - /** Filter template identifier and its arguments. */ - templateArgs: TemplateArgs -} -/** Parameters for `update_webhook_template`. */ -export interface UpdateWebhookTemplateParams { - /** New human-readable name. */ - name?: string - /** New notification email. */ - notificationEmail?: string - /** New destination configuration. */ - destinationAttributes?: WebhookDestinationAttributes - /** New template identifier and arguments. */ - templateArgs: TemplateArgs -} /** A webhook's full configuration and current state. */ export interface Webhook { /** Unique webhook identifier. */ @@ -1728,10 +1692,21 @@ export interface Webhook { /** Destination-specific configuration as a JSON string. */ destinationAttributes?: string } +/** Pagination metadata returned alongside a paginated webhooks list. */ +export interface WebhookPageInfo { + /** Page size used for this response. */ + limit: number + /** Starting index of this page within the full result set. */ + offset: number + /** Total number of webhooks matching the query across all pages. */ + total: number +} /** Response from `list_webhooks`. */ export interface ListWebhooksResponse { /** Webhooks on the current page. */ data: Array + /** Pagination metadata for the response. */ + pageInfo: WebhookPageInfo } /** Response from `get_enabled_count` for webhooks. */ export interface WebhookEnabledCountResponse { @@ -1830,6 +1805,19 @@ export interface ListStreamsResponseNode { data: Array pageInfo: PageInfo } +export interface CreateWebhookFromTemplateParamsNode { + name: string + network: string + notificationEmail?: string + destinationAttributes: WebhookDestinationAttributes + templateArgs: any +} +export interface UpdateWebhookTemplateParamsNode { + name?: string + notificationEmail?: string + destinationAttributes?: WebhookDestinationAttributes + templateArgs: any +} export declare class QuicknodeSdk { /** Creates a new SDK instance from an explicit configuration. */ constructor(config: SdkFullConfig) @@ -2250,7 +2238,7 @@ export declare class WebhooksApiClient { * filters. An optional `notification_email` receives alerts if the * webhook terminates. */ - createWebhookFromTemplate(params: CreateWebhookFromTemplateParams): Promise + createWebhookFromTemplate(params: CreateWebhookFromTemplateParamsNode): Promise /** * Updates an existing template-backed webhook, modifying its template * arguments and optionally its name, notification email, and destination @@ -2259,7 +2247,7 @@ export declare class WebhooksApiClient { * generated automatically if not provided. Templates cover EVM chains, * Solana, Bitcoin, XRPL, Hyperliquid, and Stellar. */ - updateWebhookTemplate(webhookId: string, params: UpdateWebhookTemplateParams): Promise + updateWebhookTemplate(webhookId: string, params: UpdateWebhookTemplateParamsNode): Promise } export declare class KvStoreApiClient { /** Creates a new set, storing a single string value under the given key. */ diff --git a/npm/sdk.d.ts b/npm/sdk.d.ts index 66a79e6..a43e35d 100644 --- a/npm/sdk.d.ts +++ b/npm/sdk.d.ts @@ -72,6 +72,36 @@ export type ListStreamsResponse = Omit<_ListStreamsResponseNode, "data"> & { data: Stream[]; }; +// Webhook template args (input). The inner key is `args` rather than the +// wire's `templateArgs` to avoid `templateArgs.templateArgs.wallets`; the +// Node binding renames it back before the request. +export type TemplateArgsInput = + | { templateId: "evmWalletFilter"; args: EvmWalletFilterTemplate } + | { templateId: "evmContractEvents"; args: EvmContractEventsTemplate } + | { templateId: "evmAbiFilter"; args: EvmAbiFilterTemplate } + | { templateId: "solanaWalletFilter"; args: SolanaWalletFilterTemplate } + | { templateId: "bitcoinWalletFilter"; args: BitcoinWalletFilterTemplate } + | { templateId: "xrplWalletFilter"; args: XrplWalletFilterTemplate } + | { templateId: "hyperliquidWalletEventsFilter"; args: HyperliquidWalletEventsFilterTemplate } + | { + templateId: "stellarWalletTransactionsSourceAccountFilter"; + args: StellarWalletTransactionsFilterTemplate; + }; + +// Replace the napi-generated JSON-blob templateArgs with typed unions. +type _CreateWebhookFromTemplateParamsNode = import("./index").CreateWebhookFromTemplateParamsNode; +type _UpdateWebhookTemplateParamsNode = import("./index").UpdateWebhookTemplateParamsNode; + +export type CreateWebhookFromTemplateParams = + Omit<_CreateWebhookFromTemplateParamsNode, "templateArgs"> & { + templateArgs: TemplateArgsInput; + }; + +export type UpdateWebhookTemplateParams = + Omit<_UpdateWebhookTemplateParamsNode, "templateArgs"> & { + templateArgs: TemplateArgsInput; + }; + export type { SdkFullConfig, HttpConfig, @@ -236,11 +266,10 @@ export type { UpdateWebhookParams, Webhook, ListWebhooksResponse, + WebhookPageInfo, WebhookEnabledCountResponse, WebhookDestinationAttributes, ActivateWebhookParams, - CreateWebhookFromTemplateParams, - UpdateWebhookTemplateParams, EvmWalletFilterTemplate, EvmContractEventsTemplate, EvmAbiFilterTemplate, @@ -301,27 +330,48 @@ export interface StreamsApiClientTyped { getEnabledCount(streamType?: string | undefined | null): Promise; } +// Retypes napi's `any` templateArgs to the discriminated union. Keep method +// signatures in sync with the napi-generated WebhooksApiClient in ./index.d.ts. +export interface WebhooksApiClientTyped { + listWebhooks(params?: import("./index").GetWebhooksParams | undefined | null): Promise; + deleteAllWebhooks(): Promise; + getWebhook(id: string): Promise; + updateWebhook( + id: string, + params: import("./index").UpdateWebhookParams + ): Promise; + deleteWebhook(id: string): Promise; + pauseWebhook(id: string): Promise; + activateWebhook(id: string, params: import("./index").ActivateWebhookParams): Promise; + getEnabledCount(): Promise; + createWebhookFromTemplate(params: CreateWebhookFromTemplateParams): Promise; + updateWebhookTemplate( + webhookId: string, + params: UpdateWebhookTemplateParams + ): Promise; +} + export class QuicknodeSdk { constructor(config: SdkFullConfig); static fromEnv(): QuicknodeSdk; admin: _QuicknodeSdk["admin"]; streams: StreamsApiClientTyped; - webhooks: _QuicknodeSdk["webhooks"]; + webhooks: WebhooksApiClientTyped; kvstore: _QuicknodeSdk["kvstore"]; } -export class TemplateArgs { - templateId: WebhookTemplateId; - value: string; - static evmWalletFilter(attrs: EvmWalletFilterTemplate): TemplateArgs; - static evmContractEvents(attrs: EvmContractEventsTemplate): TemplateArgs; - static evmAbiFilter(attrs: EvmAbiFilterTemplate): TemplateArgs; - static solanaWalletFilter(attrs: SolanaWalletFilterTemplate): TemplateArgs; - static bitcoinWalletFilter(attrs: BitcoinWalletFilterTemplate): TemplateArgs; - static xrplWalletFilter(attrs: XrplWalletFilterTemplate): TemplateArgs; - static hyperliquidWalletEventsFilter(attrs: HyperliquidWalletEventsFilterTemplate): TemplateArgs; - static stellarWalletTransactionsFilter(attrs: StellarWalletTransactionsFilterTemplate): TemplateArgs; -} +// Typed static factory methods producing each discriminated variant of +// TemplateArgsInput. Consumers can also construct the object literal directly. +export const TemplateArgs: { + evmWalletFilter(attrs: EvmWalletFilterTemplate): Extract; + evmContractEvents(attrs: EvmContractEventsTemplate): Extract; + evmAbiFilter(attrs: EvmAbiFilterTemplate): Extract; + solanaWalletFilter(attrs: SolanaWalletFilterTemplate): Extract; + bitcoinWalletFilter(attrs: BitcoinWalletFilterTemplate): Extract; + xrplWalletFilter(attrs: XrplWalletFilterTemplate): Extract; + hyperliquidWalletEventsFilter(attrs: HyperliquidWalletEventsFilterTemplate): Extract; + stellarWalletTransactionsFilter(attrs: StellarWalletTransactionsFilterTemplate): Extract; +}; // Typed error hierarchy. Any SDK call can throw one of these; catch // QuicknodeError to handle them all, or a specific subclass for finer control. diff --git a/npm/sdk.js b/npm/sdk.js index 71d9b48..8ef76e1 100644 --- a/npm/sdk.js +++ b/npm/sdk.js @@ -32,59 +32,45 @@ class QuicknodeSdk { } } -// TemplateArgs.value is a pre-serialized JSON string; napi forwards camelCase -// keys from JS but the API expects snake_case, so stringify through these. -function toSnakeCase(str) { - return str.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`); -} - -function keysToSnakeCase(obj) { - if (obj === null || typeof obj !== "object" || Array.isArray(obj)) return obj; - return Object.fromEntries( - Object.entries(obj).map(([k, v]) => [toSnakeCase(k), keysToSnakeCase(v)]) - ); -} - -// TemplateArgs wraps the napi-rs generated plain object with typed -// static factory methods. The underlying object has `templateId` (string enum) -// and `value` (JSON string) fields — callers should use the factory methods -// rather than constructing the object directly. +// TemplateArgs wraps the webhook template input with typed static factory +// methods. The underlying object is `{ templateId, args }` — callers should +// use the factory methods rather than constructing the object directly. class TemplateArgs { - constructor(templateId, value) { + constructor(templateId, args) { this.templateId = templateId; - this.value = value; + this.args = args; } static evmWalletFilter(attrs) { - return new TemplateArgs("EvmWalletFilter", JSON.stringify(keysToSnakeCase(attrs))); + return new TemplateArgs("evmWalletFilter", attrs); } static evmContractEvents(attrs) { - return new TemplateArgs("EvmContractEvents", JSON.stringify(keysToSnakeCase(attrs))); + return new TemplateArgs("evmContractEvents", attrs); } static evmAbiFilter(attrs) { - return new TemplateArgs("EvmAbiFilter", JSON.stringify(keysToSnakeCase(attrs))); + return new TemplateArgs("evmAbiFilter", attrs); } static solanaWalletFilter(attrs) { - return new TemplateArgs("SolanaWalletFilter", JSON.stringify(keysToSnakeCase(attrs))); + return new TemplateArgs("solanaWalletFilter", attrs); } static bitcoinWalletFilter(attrs) { - return new TemplateArgs("BitcoinWalletFilter", JSON.stringify(keysToSnakeCase(attrs))); + return new TemplateArgs("bitcoinWalletFilter", attrs); } static xrplWalletFilter(attrs) { - return new TemplateArgs("XrplWalletFilter", JSON.stringify(keysToSnakeCase(attrs))); + return new TemplateArgs("xrplWalletFilter", attrs); } static hyperliquidWalletEventsFilter(attrs) { - return new TemplateArgs("HyperliquidWalletEventsFilter", JSON.stringify(keysToSnakeCase(attrs))); + return new TemplateArgs("hyperliquidWalletEventsFilter", attrs); } static stellarWalletTransactionsFilter(attrs) { - return new TemplateArgs("StellarWalletTransactionsSourceAccountFilter", JSON.stringify(keysToSnakeCase(attrs))); + return new TemplateArgs("stellarWalletTransactionsSourceAccountFilter", attrs); } } diff --git a/python/examples/webhooks_e2e.py b/python/examples/webhooks_e2e.py index 131c14c..ef17e50 100644 --- a/python/examples/webhooks_e2e.py +++ b/python/examples/webhooks_e2e.py @@ -1,9 +1,9 @@ import asyncio from sdk import ( + EvmWalletFilterArgs, EvmWalletFilterTemplate, QuicknodeSdk, - TemplateArgs, WebhookDestinationAttributes, ) @@ -12,7 +12,7 @@ async def main(): qn = QuicknodeSdk.from_env() before = await qn.webhooks.list_webhooks() - print(f"webhooks before: {len(before.data)}") + print(f"webhooks before: {len(before.data)} (total={before.page_info.total})") count = await qn.webhooks.get_enabled_count() print(f"enabled count: {count.total}") @@ -23,7 +23,7 @@ async def main(): destination_attributes=WebhookDestinationAttributes( url="https://webhook.site/ae19071a-2dcc-4035-9cdf-406dcb4719ef", ), - template_args=TemplateArgs.evm_wallet_filter( + template_args=EvmWalletFilterArgs( EvmWalletFilterTemplate(wallets=["0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"]) ), ) @@ -46,7 +46,7 @@ async def main(): print(f"deleted: {webhook_id}") after = await qn.webhooks.list_webhooks() - print(f"webhooks after: {len(after.data)}") + print(f"webhooks after: {len(after.data)} (total={after.page_info.total})") asyncio.run(main()) diff --git a/python/sdk/__init__.py b/python/sdk/__init__.py index 6a96a36..9c246b9 100644 --- a/python/sdk/__init__.py +++ b/python/sdk/__init__.py @@ -170,9 +170,9 @@ WebhooksApiClient, Webhook, ListWebhooksResponse, + WebhookPageInfo, WebhookEnabledCountResponse, WebhookDestinationAttributes, - TemplateArgs, WebhooksConfig, GetWebhooksParams, UpdateWebhookParams, @@ -184,6 +184,14 @@ XrplWalletFilterTemplate, HyperliquidWalletEventsFilterTemplate, StellarWalletTransactionsFilterTemplate, + EvmWalletFilterArgs, + EvmContractEventsArgs, + EvmAbiFilterArgs, + SolanaWalletFilterArgs, + BitcoinWalletFilterArgs, + XrplWalletFilterArgs, + HyperliquidWalletEventsFilterArgs, + StellarWalletTransactionsFilterArgs, QuicknodeError, ConfigError, HttpError, @@ -364,9 +372,9 @@ "WebhooksApiClient", "Webhook", "ListWebhooksResponse", + "WebhookPageInfo", "WebhookEnabledCountResponse", "WebhookDestinationAttributes", - "TemplateArgs", "WebhooksConfig", "GetWebhooksParams", "UpdateWebhookParams", @@ -378,6 +386,14 @@ "XrplWalletFilterTemplate", "HyperliquidWalletEventsFilterTemplate", "StellarWalletTransactionsFilterTemplate", + "EvmWalletFilterArgs", + "EvmContractEventsArgs", + "EvmAbiFilterArgs", + "SolanaWalletFilterArgs", + "BitcoinWalletFilterArgs", + "XrplWalletFilterArgs", + "HyperliquidWalletEventsFilterArgs", + "StellarWalletTransactionsFilterArgs", "QuicknodeError", "ConfigError", "HttpError", diff --git a/python/sdk/__init__.pyi b/python/sdk/__init__.pyi index f1f25ec..b0adb7f 100644 --- a/python/sdk/__init__.pyi +++ b/python/sdk/__init__.pyi @@ -172,9 +172,9 @@ from sdk._core import ( WebhooksApiClient, Webhook, ListWebhooksResponse, + WebhookPageInfo, WebhookEnabledCountResponse, WebhookDestinationAttributes, - TemplateArgs, WebhooksConfig, GetWebhooksParams, UpdateWebhookParams, @@ -186,6 +186,14 @@ from sdk._core import ( XrplWalletFilterTemplate, HyperliquidWalletEventsFilterTemplate, StellarWalletTransactionsFilterTemplate, + EvmWalletFilterArgs, + EvmContractEventsArgs, + EvmAbiFilterArgs, + SolanaWalletFilterArgs, + BitcoinWalletFilterArgs, + XrplWalletFilterArgs, + HyperliquidWalletEventsFilterArgs, + StellarWalletTransactionsFilterArgs, QuicknodeError, ConfigError, HttpError, @@ -366,9 +374,9 @@ __all__ = [ "WebhooksApiClient", "Webhook", "ListWebhooksResponse", + "WebhookPageInfo", "WebhookEnabledCountResponse", "WebhookDestinationAttributes", - "TemplateArgs", "WebhooksConfig", "GetWebhooksParams", "UpdateWebhookParams", @@ -380,6 +388,14 @@ __all__ = [ "XrplWalletFilterTemplate", "HyperliquidWalletEventsFilterTemplate", "StellarWalletTransactionsFilterTemplate", + "EvmWalletFilterArgs", + "EvmContractEventsArgs", + "EvmAbiFilterArgs", + "SolanaWalletFilterArgs", + "BitcoinWalletFilterArgs", + "XrplWalletFilterArgs", + "HyperliquidWalletEventsFilterArgs", + "StellarWalletTransactionsFilterArgs", "QuicknodeError", "ConfigError", "HttpError", diff --git a/python/sdk/_core/__init__.pyi b/python/sdk/_core/__init__.pyi index 1defe12..dd5fb20 100644 --- a/python/sdk/_core/__init__.pyi +++ b/python/sdk/_core/__init__.pyi @@ -9,6 +9,7 @@ __all__ = [ "AdminApiClient", "AdminConfig", "AzureAttributes", + "BitcoinWalletFilterArgs", "BitcoinWalletFilterTemplate", "BulkAddTagData", "BulkAddTagRequest", @@ -63,8 +64,11 @@ __all__ = [ "EndpointTag", "EndpointToken", "EndpointUsage", + "EvmAbiFilterArgs", "EvmAbiFilterTemplate", + "EvmContractEventsArgs", "EvmContractEventsTemplate", + "EvmWalletFilterArgs", "EvmWalletFilterTemplate", "GetAccountMetricsRequest", "GetAccountMetricsResponse", @@ -94,6 +98,7 @@ __all__ = [ "GetUsageResponse", "GetWebhooksParams", "HttpConfig", + "HyperliquidWalletEventsFilterArgs", "HyperliquidWalletEventsFilterTemplate", "InviteTeamMemberRequest", "InviteTeamMemberResponse", @@ -140,7 +145,9 @@ __all__ = [ "ShowEndpointResponse", "SingleEndpoint", "SnowflakeAttributes", + "SolanaWalletFilterArgs", "SolanaWalletFilterTemplate", + "StellarWalletTransactionsFilterArgs", "StellarWalletTransactionsFilterTemplate", "Stream", "StreamAzureDestination", @@ -161,7 +168,6 @@ __all__ = [ "TeamMessageData", "TeamSummary", "TeamUser", - "TemplateArgs", "TestFilterResponse", "UpdateEndpointRequest", "UpdateEndpointStatusRequest", @@ -176,7 +182,6 @@ __all__ = [ "UpdateTeamEndpointsRequest", "UpdateTeamEndpointsResponse", "UpdateWebhookParams", - "UpdateWebhookTemplateParams", "UsageByChainData", "UsageByEndpointData", "UsageByMethodData", @@ -186,8 +191,10 @@ __all__ = [ "WebhookAttributes", "WebhookDestinationAttributes", "WebhookEnabledCountResponse", + "WebhookPageInfo", "WebhooksApiClient", "WebhooksConfig", + "XrplWalletFilterArgs", "XrplWalletFilterTemplate", ] @@ -680,6 +687,12 @@ class AzureAttributes: """ def __new__(cls, storage_account: builtins.str, sas_token: builtins.str, container: builtins.str, compression: builtins.str, file_type: builtins.str, max_retry: builtins.int, retry_interval_sec: builtins.int, blob_prefix: typing.Optional[builtins.str] = None) -> AzureAttributes: ... +@typing.final +class BitcoinWalletFilterArgs: + @property + def attributes(self) -> BitcoinWalletFilterTemplate: ... + def __new__(cls, attrs: BitcoinWalletFilterTemplate) -> BitcoinWalletFilterArgs: ... + @typing.final class BitcoinWalletFilterTemplate: r""" @@ -2588,6 +2601,12 @@ class EndpointUsage: Request count during the window. """ +@typing.final +class EvmAbiFilterArgs: + @property + def attributes(self) -> EvmAbiFilterTemplate: ... + def __new__(cls, attrs: EvmAbiFilterTemplate) -> EvmAbiFilterArgs: ... + @typing.final class EvmAbiFilterTemplate: r""" @@ -2616,6 +2635,12 @@ class EvmAbiFilterTemplate: """ def __new__(cls, abi: builtins.str, contracts: typing.Sequence[builtins.str]) -> EvmAbiFilterTemplate: ... +@typing.final +class EvmContractEventsArgs: + @property + def attributes(self) -> EvmContractEventsTemplate: ... + def __new__(cls, attrs: EvmContractEventsTemplate) -> EvmContractEventsArgs: ... + @typing.final class EvmContractEventsTemplate: r""" @@ -2644,6 +2669,12 @@ class EvmContractEventsTemplate: """ def __new__(cls, contracts: typing.Sequence[builtins.str], event_hashes: typing.Optional[typing.Sequence[builtins.str]] = None) -> EvmContractEventsTemplate: ... +@typing.final +class EvmWalletFilterArgs: + @property + def attributes(self) -> EvmWalletFilterTemplate: ... + def __new__(cls, attrs: EvmWalletFilterTemplate) -> EvmWalletFilterArgs: ... + @typing.final class EvmWalletFilterTemplate: r""" @@ -3484,6 +3515,12 @@ class HttpConfig: def pool_max_idle_per_host(self, value: typing.Optional[builtins.int]) -> None: ... def __new__(cls, timeout_secs: typing.Optional[builtins.int] = None, pool_max_idle_per_host: typing.Optional[builtins.int] = None) -> HttpConfig: ... +@typing.final +class HyperliquidWalletEventsFilterArgs: + @property + def attributes(self) -> HyperliquidWalletEventsFilterTemplate: ... + def __new__(cls, attrs: HyperliquidWalletEventsFilterTemplate) -> HyperliquidWalletEventsFilterArgs: ... + @typing.final class HyperliquidWalletEventsFilterTemplate: r""" @@ -4186,6 +4223,16 @@ class ListWebhooksResponse: r""" Webhooks on the current page. """ + @property + def page_info(self) -> WebhookPageInfo: + r""" + Pagination metadata for the response. + """ + @page_info.setter + def page_info(self, value: WebhookPageInfo) -> None: + r""" + Pagination metadata for the response. + """ @typing.final class LogDetails: @@ -5524,6 +5571,12 @@ class SnowflakeAttributes: """ def __new__(cls, account: builtins.str, host: builtins.str, port: builtins.int, protocol: builtins.str, database: builtins.str, schema: builtins.str, warehouse: builtins.str, username: builtins.str, password: builtins.str, max_retry: builtins.int, retry_interval_sec: builtins.int, table_name: typing.Optional[builtins.str] = None) -> SnowflakeAttributes: ... +@typing.final +class SolanaWalletFilterArgs: + @property + def attributes(self) -> SolanaWalletFilterTemplate: ... + def __new__(cls, attrs: SolanaWalletFilterTemplate) -> SolanaWalletFilterArgs: ... + @typing.final class SolanaWalletFilterTemplate: r""" @@ -5542,6 +5595,12 @@ class SolanaWalletFilterTemplate: """ def __new__(cls, accounts: typing.Sequence[builtins.str]) -> SolanaWalletFilterTemplate: ... +@typing.final +class StellarWalletTransactionsFilterArgs: + @property + def attributes(self) -> StellarWalletTransactionsFilterTemplate: ... + def __new__(cls, attrs: StellarWalletTransactionsFilterTemplate) -> StellarWalletTransactionsFilterArgs: ... + @typing.final class StellarWalletTransactionsFilterTemplate: r""" @@ -6063,31 +6122,6 @@ class TeamUser: Whether this user is the primary user on the account. """ -@typing.final -class TemplateArgs: - r""" - Template identifier paired with its arguments, consumed by - `create_webhook_from_template` and `update_webhook_template`. Construct via - the typed static factory methods (one per template); do not set fields - directly. - """ - @staticmethod - def evm_wallet_filter(attrs: EvmWalletFilterTemplate) -> TemplateArgs: ... - @staticmethod - def evm_contract_events(attrs: EvmContractEventsTemplate) -> TemplateArgs: ... - @staticmethod - def evm_abi_filter(attrs: EvmAbiFilterTemplate) -> TemplateArgs: ... - @staticmethod - def solana_wallet_filter(attrs: SolanaWalletFilterTemplate) -> TemplateArgs: ... - @staticmethod - def bitcoin_wallet_filter(attrs: BitcoinWalletFilterTemplate) -> TemplateArgs: ... - @staticmethod - def xrpl_wallet_filter(attrs: XrplWalletFilterTemplate) -> TemplateArgs: ... - @staticmethod - def hyperliquid_wallet_events_filter(attrs: HyperliquidWalletEventsFilterTemplate) -> TemplateArgs: ... - @staticmethod - def stellar_wallet_transactions_filter(attrs: StellarWalletTransactionsFilterTemplate) -> TemplateArgs: ... - @typing.final class TestFilterResponse: r""" @@ -6404,53 +6438,6 @@ class UpdateWebhookParams: """ def __new__(cls, name: typing.Optional[builtins.str] = None, notification_email: typing.Optional[builtins.str] = None, destination_attributes: typing.Optional[WebhookDestinationAttributes] = None) -> UpdateWebhookParams: ... -@typing.final -class UpdateWebhookTemplateParams: - r""" - Parameters for `update_webhook_template`. - """ - @property - def name(self) -> typing.Optional[builtins.str]: - r""" - New human-readable name. - """ - @name.setter - def name(self, value: typing.Optional[builtins.str]) -> None: - r""" - New human-readable name. - """ - @property - def notification_email(self) -> typing.Optional[builtins.str]: - r""" - New notification email. - """ - @notification_email.setter - def notification_email(self, value: typing.Optional[builtins.str]) -> None: - r""" - New notification email. - """ - @property - def destination_attributes(self) -> typing.Optional[WebhookDestinationAttributes]: - r""" - New destination configuration. - """ - @destination_attributes.setter - def destination_attributes(self, value: typing.Optional[WebhookDestinationAttributes]) -> None: - r""" - New destination configuration. - """ - @property - def template_args(self) -> TemplateArgs: - r""" - New template identifier and arguments. - """ - @template_args.setter - def template_args(self, value: TemplateArgs) -> None: - r""" - New template identifier and arguments. - """ - def __new__(cls, template_args: TemplateArgs, name: typing.Optional[builtins.str] = None, notification_email: typing.Optional[builtins.str] = None, destination_attributes: typing.Optional[WebhookDestinationAttributes] = None) -> UpdateWebhookTemplateParams: ... - @typing.final class UsageByChainData: r""" @@ -6877,6 +6864,42 @@ class WebhookEnabledCountResponse: Total count of enabled webhooks on the account. """ +@typing.final +class WebhookPageInfo: + r""" + Pagination metadata returned alongside a paginated webhooks list. + """ + @property + def limit(self) -> builtins.int: + r""" + Page size used for this response. + """ + @limit.setter + def limit(self, value: builtins.int) -> None: + r""" + Page size used for this response. + """ + @property + def offset(self) -> builtins.int: + r""" + Starting index of this page within the full result set. + """ + @offset.setter + def offset(self, value: builtins.int) -> None: + r""" + Starting index of this page within the full result set. + """ + @property + def total(self) -> builtins.int: + r""" + Total number of webhooks matching the query across all pages. + """ + @total.setter + def total(self, value: builtins.int) -> None: + r""" + Total number of webhooks matching the query across all pages. + """ + @typing.final class WebhooksApiClient: def list_webhooks(self, limit: typing.Optional[builtins.int] = None, offset: typing.Optional[builtins.int] = None) -> typing.Coroutine[typing.Any, typing.Any, ListWebhooksResponse]: @@ -6929,7 +6952,7 @@ class WebhooksApiClient: Returns the total number of enabled webhooks currently configured on the account. """ - def create_webhook_from_template(self, name: builtins.str, network: builtins.str, destination_attributes: WebhookDestinationAttributes, template_args: TemplateArgs, notification_email: typing.Optional[builtins.str] = None) -> typing.Coroutine[typing.Any, typing.Any, Webhook]: + def create_webhook_from_template(self, name: builtins.str, network: builtins.str, destination_attributes: WebhookDestinationAttributes, template_args: typing.Union[EvmWalletFilterArgs, EvmContractEventsArgs, EvmAbiFilterArgs, SolanaWalletFilterArgs, BitcoinWalletFilterArgs, XrplWalletFilterArgs, HyperliquidWalletEventsFilterArgs, StellarWalletTransactionsFilterArgs], notification_email: typing.Optional[builtins.str] = None) -> typing.Coroutine[typing.Any, typing.Any, Webhook]: r""" Creates a new webhook from a predefined filter template. Requires a descriptive name, a target blockchain network, and destination @@ -6939,7 +6962,7 @@ class WebhooksApiClient: filters. An optional `notification_email` receives alerts if the webhook terminates. """ - def update_webhook_template(self, webhook_id: builtins.str, template_args: TemplateArgs, name: typing.Optional[builtins.str] = None, notification_email: typing.Optional[builtins.str] = None, destination_attributes: typing.Optional[WebhookDestinationAttributes] = None) -> typing.Coroutine[typing.Any, typing.Any, Webhook]: + def update_webhook_template(self, webhook_id: builtins.str, template_args: typing.Union[EvmWalletFilterArgs, EvmContractEventsArgs, EvmAbiFilterArgs, SolanaWalletFilterArgs, BitcoinWalletFilterArgs, XrplWalletFilterArgs, HyperliquidWalletEventsFilterArgs, StellarWalletTransactionsFilterArgs], name: typing.Optional[builtins.str] = None, notification_email: typing.Optional[builtins.str] = None, destination_attributes: typing.Optional[WebhookDestinationAttributes] = None) -> typing.Coroutine[typing.Any, typing.Any, Webhook]: r""" Updates an existing template-backed webhook, modifying its template arguments and optionally its name, notification email, and destination @@ -6957,6 +6980,12 @@ class WebhooksConfig: def base_url(self, value: typing.Optional[builtins.str]) -> None: ... def __new__(cls, base_url: typing.Optional[builtins.str] = None) -> WebhooksConfig: ... +@typing.final +class XrplWalletFilterArgs: + @property + def attributes(self) -> XrplWalletFilterTemplate: ... + def __new__(cls, attrs: XrplWalletFilterTemplate) -> XrplWalletFilterArgs: ... + @typing.final class XrplWalletFilterTemplate: r""" diff --git a/python/sdk/init_manual_override.pyi b/python/sdk/init_manual_override.pyi index f1f25ec..b0adb7f 100644 --- a/python/sdk/init_manual_override.pyi +++ b/python/sdk/init_manual_override.pyi @@ -172,9 +172,9 @@ from sdk._core import ( WebhooksApiClient, Webhook, ListWebhooksResponse, + WebhookPageInfo, WebhookEnabledCountResponse, WebhookDestinationAttributes, - TemplateArgs, WebhooksConfig, GetWebhooksParams, UpdateWebhookParams, @@ -186,6 +186,14 @@ from sdk._core import ( XrplWalletFilterTemplate, HyperliquidWalletEventsFilterTemplate, StellarWalletTransactionsFilterTemplate, + EvmWalletFilterArgs, + EvmContractEventsArgs, + EvmAbiFilterArgs, + SolanaWalletFilterArgs, + BitcoinWalletFilterArgs, + XrplWalletFilterArgs, + HyperliquidWalletEventsFilterArgs, + StellarWalletTransactionsFilterArgs, QuicknodeError, ConfigError, HttpError, @@ -366,9 +374,9 @@ __all__ = [ "WebhooksApiClient", "Webhook", "ListWebhooksResponse", + "WebhookPageInfo", "WebhookEnabledCountResponse", "WebhookDestinationAttributes", - "TemplateArgs", "WebhooksConfig", "GetWebhooksParams", "UpdateWebhookParams", @@ -380,6 +388,14 @@ __all__ = [ "XrplWalletFilterTemplate", "HyperliquidWalletEventsFilterTemplate", "StellarWalletTransactionsFilterTemplate", + "EvmWalletFilterArgs", + "EvmContractEventsArgs", + "EvmAbiFilterArgs", + "SolanaWalletFilterArgs", + "BitcoinWalletFilterArgs", + "XrplWalletFilterArgs", + "HyperliquidWalletEventsFilterArgs", + "StellarWalletTransactionsFilterArgs", "QuicknodeError", "ConfigError", "HttpError", diff --git a/ruby/examples/webhooks_e2e.rb b/ruby/examples/webhooks_e2e.rb index 4c366e0..d008f80 100644 --- a/ruby/examples/webhooks_e2e.rb +++ b/ruby/examples/webhooks_e2e.rb @@ -4,7 +4,7 @@ qn = QuicknodeSdk::SDK.from_env before = JSON.parse(qn.webhooks.list_webhooks({})) -puts "webhooks before: #{before["data"].length}" +puts "webhooks before: #{before["data"].length} (total=#{before["pageInfo"]["total"]})" count = JSON.parse(qn.webhooks.get_enabled_count) puts "enabled count: #{count["total"]}" @@ -15,8 +15,8 @@ }) template_args = JSON.generate({ - template_id: "evmWalletFilter", - value: JSON.generate({ wallets: ["0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"] }) + templateId: "evmWalletFilter", + templateArgs: { wallets: ["0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"] } }) webhook = JSON.parse(qn.webhooks.create_webhook_from_template( @@ -45,4 +45,4 @@ sleep 1 after = JSON.parse(qn.webhooks.list_webhooks({})) -puts "webhooks after: #{after["data"].length}" +puts "webhooks after: #{after["data"].length} (total=#{after["pageInfo"]["total"]})"